From 39b3950da7771bc0eabaee739a7b0f7b3a3d027d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 20 Apr 2021 17:27:52 -0400 Subject: [PATCH 01/25] Starting rbac for comments --- .../plugins/cases/common/api/cases/comment.ts | 3 + .../cases/server/authorization/index.ts | 59 +++++++++- .../cases/server/authorization/types.ts | 7 ++ .../cases/server/authorization/utils.ts | 14 ++- .../cases/server/client/attachments/add.ts | 106 +++++++++++------- .../cases/server/client/attachments/client.ts | 32 ++---- .../cases/server/client/attachments/delete.ts | 22 ++++ .../cases/server/client/attachments/get.ts | 40 ++++++- x-pack/plugins/cases/server/client/utils.ts | 61 +++++----- .../server/common/models/commentable_case.ts | 12 ++ .../plugins/cases/server/common/utils.test.ts | 16 ++- x-pack/plugins/cases/server/common/utils.ts | 1 + .../api/cases/comments/get_all_comment.ts | 2 + .../server/services/attachments/index.ts | 4 +- .../feature_privilege_builder/cases.ts | 18 ++- 15 files changed, 293 insertions(+), 104 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 4eb2ad1eadd6c..118af8a44a08e 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -27,6 +27,7 @@ export const CommentAttributesBasicRt = rt.type({ ]), created_at: rt.string, created_by: UserRT, + owner: rt.string, pushed_at: rt.union([rt.string, rt.null]), pushed_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), @@ -42,6 +43,7 @@ export enum CommentType { export const ContextTypeUserRt = rt.type({ comment: rt.string, type: rt.literal(CommentType.user), + owner: rt.string, }); /** @@ -57,6 +59,7 @@ export const AlertCommentRequestRt = rt.type({ id: rt.union([rt.string, rt.null]), name: rt.union([rt.string, rt.null]), }), + owner: rt.string, }); const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 3203398ff51a5..4fa256080a9c3 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -6,7 +6,7 @@ */ import { EventType } from '../../../security/server'; -import { CASE_SAVED_OBJECT } from '../../common/constants'; +import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; export * from './authorization'; @@ -82,4 +82,61 @@ export const Operations: Record Promise; export enum ReadOperations { GetCase = 'getCase', FindCases = 'findCases', + GetComment = 'getComment', + GetAllComments = 'getAllComments', + FindComments = 'findComments', } // TODO: comments @@ -33,6 +36,10 @@ export enum WriteOperations { CreateCase = 'createCase', DeleteCase = 'deleteCase', UpdateCase = 'updateCase', + CreateComment = 'createComment', + DeleteAllComments = 'deleteAllComments', + DeleteComment = 'deleteComment', + UpdateComments = 'updateComments', } /** diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index a7e210d07d214..187591c705112 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -19,10 +19,18 @@ export const getOwnersFilter = (savedObjectType: string, owners: string[]): Kuer }; export const combineFilterWithAuthorizationFilter = ( - filter: KueryNode, - authorizationFilter: KueryNode + filter: KueryNode | undefined, + authorizationFilter: KueryNode | undefined ) => { - return nodeBuilder.and([filter, authorizationFilter]); + if (!filter && !authorizationFilter) { + return; + } + + const kueries = [ + ...(filter !== undefined ? [filter] : []), + ...(authorizationFilter !== undefined ? [authorizationFilter] : []), + ]; + return nodeBuilder.and(kueries); }; export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean => { diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index e77115ba4e228..b58f01c08368a 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -10,7 +10,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; +import { + SavedObject, + SavedObjectsClientContract, + Logger, + SavedObjectsUtils, +} from '../../../../../../src/core/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { @@ -20,11 +25,11 @@ import { CaseStatuses, CaseType, SubCaseAttributes, - CommentRequest, CaseResponse, User, CommentRequestAlertType, AlertCommentRequestRt, + CommentRequest, } from '../../../common/api'; import { buildCaseUserActionItem, @@ -45,7 +50,8 @@ import { ENABLE_CASE_CONNECTOR, } from '../../../common/constants'; -import { decodeCommentRequest } from '../utils'; +import { decodeCommentRequest, ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; async function getSubCase({ caseService, @@ -106,27 +112,21 @@ async function getSubCase({ return newSubCase; } -interface AddCommentFromRuleArgs { - casesClientInternal: CasesClientInternal; - caseId: string; - comment: CommentRequestAlertType; - savedObjectsClient: SavedObjectsClientContract; - attachmentService: AttachmentService; - caseService: CaseService; - userActionService: CaseUserActionService; - logger: Logger; -} +const addGeneratedAlerts = async ( + { caseId, comment }: AddArgs, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { + const { + savedObjectsClient, + attachmentService, + caseService, + userActionService, + logger, + auditLogger, + authorization, + } = clientArgs; -const addGeneratedAlerts = async ({ - savedObjectsClient, - attachmentService, - caseService, - userActionService, - casesClientInternal, - caseId, - comment, - logger, -}: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -141,6 +141,15 @@ const addGeneratedAlerts = async ({ try { const createdDate = new Date().toISOString(); + const savedObjectID = SavedObjectsUtils.generateId(); + + await ensureAuthorized({ + authorization, + auditLogger, + owners: [comment.owner], + savedObjectIDs: [savedObjectID], + operation: Operations.createComment, + }); const caseInfo = await caseService.getCase({ soClient: savedObjectsClient, @@ -181,7 +190,12 @@ const addGeneratedAlerts = async ({ const { comment: newComment, commentableCase: updatedCase, - } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); + } = await commentableCase.createComment({ + createdDate, + user: userDetails, + commentReq: query, + id: savedObjectID, + }); if ( (newComment.attributes.type === CommentType.alert || @@ -283,18 +297,20 @@ async function getCombinedCase({ } } -interface AddCommentArgs { +/** + * The arguments needed for creating a new attachment to a case. + */ +export interface AddArgs { caseId: string; comment: CommentRequest; - casesClientInternal: CasesClientInternal; } -export const addComment = async ({ - caseId, - comment, - casesClientInternal, - ...rest -}: AddCommentArgs & CasesClientArgs): Promise => { +export const addComment = async ( + addArgs: AddArgs, + clientArgs: CasesClientArgs, + casesClientInternal: CasesClientInternal +): Promise => { + const { comment, caseId } = addArgs; const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -307,7 +323,9 @@ export const addComment = async ({ attachmentService, user, logger, - } = rest; + authorization, + auditLogger, + } = clientArgs; if (isCommentRequestTypeGenAlert(comment)) { if (!ENABLE_CASE_CONNECTOR) { @@ -316,20 +334,21 @@ export const addComment = async ({ ); } - return addGeneratedAlerts({ - caseId, - comment, - casesClientInternal, - savedObjectsClient, - userActionService, - caseService, - attachmentService, - logger, - }); + return addGeneratedAlerts(addArgs, clientArgs, casesClientInternal); } decodeCommentRequest(comment); try { + const savedObjectID = SavedObjectsUtils.generateId(); + + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.createComment, + owners: [comment.owner], + savedObjectIDs: [savedObjectID], + }); + const createdDate = new Date().toISOString(); const combinedCase = await getCombinedCase({ @@ -352,6 +371,7 @@ export const addComment = async ({ createdDate, user: userInfo, commentReq: query, + id: savedObjectID, }); if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index 27fb5e1cf61f0..41f1db81719fc 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -8,25 +8,19 @@ import { AllCommentsResponse, CaseResponse, - CommentRequest as AttachmentsRequest, CommentResponse, CommentsResponse, } from '../../../common/api'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; -import { addComment } from './add'; +import { AddArgs, addComment } from './add'; import { DeleteAllArgs, deleteAll, DeleteArgs, deleteComment } from './delete'; import { find, FindArgs, get, getAll, GetAllArgs, GetArgs } from './get'; import { update, UpdateArgs } from './update'; -interface AttachmentsAdd { - caseId: string; - comment: AttachmentsRequest; -} - export interface AttachmentsSubClient { - add(args: AttachmentsAdd): Promise; + add(params: AddArgs): Promise; deleteAll(deleteAllArgs: DeleteAllArgs): Promise; delete(deleteArgs: DeleteArgs): Promise; find(findArgs: FindArgs): Promise; @@ -36,23 +30,17 @@ export interface AttachmentsSubClient { } export const createAttachmentsSubClient = ( - args: CasesClientArgs, + clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): AttachmentsSubClient => { const attachmentSubClient: AttachmentsSubClient = { - add: ({ caseId, comment }: AttachmentsAdd) => - addComment({ - ...args, - casesClientInternal, - caseId, - comment, - }), - deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, args), - delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, args), - find: (findArgs: FindArgs) => find(findArgs, args), - getAll: (getAllArgs: GetAllArgs) => getAll(getAllArgs, args), - get: (getArgs: GetArgs) => get(getArgs, args), - update: (updateArgs: UpdateArgs) => update(updateArgs, args), + add: (params: AddArgs) => addComment(params, clientArgs, casesClientInternal), + deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, clientArgs), + delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, clientArgs), + find: (findArgs: FindArgs) => find(findArgs, clientArgs), + getAll: (getAllArgs: GetAllArgs) => getAll(getAllArgs, clientArgs), + get: (getArgs: GetArgs) => get(getArgs, clientArgs), + update: (updateArgs: UpdateArgs) => update(updateArgs, clientArgs), }; return Object.freeze(attachmentSubClient); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 37069b94df7cb..1c9c2e0b68d2e 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -13,6 +13,8 @@ import { CasesClientArgs } from '../types'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; /** * Parameters for deleting all comments of a case or sub case. @@ -45,6 +47,8 @@ export async function deleteAll( attachmentService, userActionService, logger, + authorization, + auditLogger, } = clientArgs; try { @@ -57,6 +61,14 @@ export async function deleteAll( associationType: subCaseID ? AssociationType.subCase : AssociationType.case, }); + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.deleteAllComments, + savedObjectIDs: comments.saved_objects.map((comment) => comment.id), + owners: comments.saved_objects.map((comment) => comment.attributes.owner), + }); + await Promise.all( comments.saved_objects.map((comment) => attachmentService.delete({ @@ -101,6 +113,8 @@ export async function deleteComment( attachmentService, userActionService, logger, + authorization, + auditLogger, } = clientArgs; try { @@ -117,6 +131,14 @@ export async function deleteComment( throw Boom.notFound(`This comment ${attachmentID} does not exist anymore.`); } + await ensureAuthorized({ + authorization, + auditLogger, + owners: [myComment.attributes.owner], + savedObjectIDs: [myComment.id], + operation: Operations.deleteComment, + }); + const type = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const id = subCaseID ?? caseID; diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 70aeb5a3df2aa..b8f131083e200 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -31,6 +31,8 @@ import { import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; +import { ensureAuthorized, getAuthorizationFilter } from '../utils'; +import { Operations } from '../../authorization'; const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, @@ -48,6 +50,7 @@ export interface GetAllArgs { caseID: string; includeSubCaseComments?: boolean; subCaseID?: string; + owner?: string; } export interface GetArgs { @@ -115,7 +118,13 @@ export async function get( { attachmentID, caseID }: GetArgs, clientArgs: CasesClientArgs ): Promise { - const { attachmentService, savedObjectsClient: soClient, logger } = clientArgs; + const { + attachmentService, + savedObjectsClient: soClient, + logger, + authorization, + auditLogger, + } = clientArgs; try { const comment = await attachmentService.get({ @@ -123,6 +132,14 @@ export async function get( attachmentId: attachmentID, }); + await ensureAuthorized({ + authorization, + auditLogger, + owners: [comment.attributes.owner], + savedObjectIDs: [comment.id], + operation: Operations.getComment, + }); + return CommentResponseRt.encode(flattenCommentSavedObject(comment)); } catch (error) { throw createCaseError({ @@ -138,10 +155,16 @@ export async function get( * collections. If the entity is a sub case, pass in the subCaseID. */ export async function getAll( - { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, + { caseID, includeSubCaseComments, subCaseID, owner }: GetAllArgs, clientArgs: CasesClientArgs ): Promise { - const { savedObjectsClient: soClient, caseService, logger } = clientArgs; + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; try { let comments: SavedObjectsFindResponse; @@ -155,6 +178,17 @@ export async function getAll( ); } + // TODO: finish this call combineFieldWithKueryNodeFilter + const { + filter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = getAuthorizationFilter({ + authorization, + auditLogger, + operation: Operations.getAllComments, + }); + if (subCaseID) { comments = await caseService.getAllSubCaseComments({ soClient, diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 0dcbf61fa0894..df28d2d1f9aa3 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -118,21 +118,27 @@ export const addStatusFilter = ({ return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; }; +interface FilterField { + filters?: string | string[]; + field: string; + operator: 'and' | 'or'; + type?: string; +} + export const buildFilter = ({ filters, field, operator, type = CASE_SAVED_OBJECT, -}: { - filters: string | string[]; - field: string; - operator: 'or' | 'and'; - type?: string; -}): KueryNode | null => { +}: FilterField): KueryNode | undefined => { + if (filters === undefined) { + return; + } + const filtersAsArray = Array.isArray(filters) ? filters : [filters]; if (filtersAsArray.length === 0) { - return null; + return; } return nodeBuilder[operator]( @@ -220,10 +226,7 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null && caseFilters != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, }; @@ -245,17 +248,11 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, subCase: { - filter: - authorizationFilter != null && subCaseFilters != null - ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) - : subCaseFilters, + filter: combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter), sortField, }, }; @@ -296,17 +293,11 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, subCase: { - filter: - authorizationFilter != null && subCaseFilters != null - ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) - : subCaseFilters, + filter: combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter), sortField, }, }; @@ -314,6 +305,14 @@ export const constructQueryOptions = ({ } }; +/** + * Combines a string field with a kuery node to build a complete kuery node for use in the find API. + */ +export function combineFieldWithKueryNodeFilter(filterField: FilterField, kueryNode?: KueryNode) { + const filter = buildFilter(filterField); + return combineFilterWithAuthorizationFilter(filter, kueryNode); +} + interface CompareArrays { addedItems: string[]; deletedItems: string[]; @@ -484,6 +483,12 @@ interface OwnerEntity { id: string; } +interface AuthFilterHelpers { + filter?: KueryNode; + ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void; + logSuccessfulAuthorization: () => void; +} + /** * Wraps the Authorization class' method for determining which found saved objects the user making the request * is authorized to interact with. @@ -496,7 +501,7 @@ export async function getAuthorizationFilter({ operation: OperationDetails; authorization: PublicMethodsOf; auditLogger?: AuditLogger; -}) { +}): Promise { try { const { filter, diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index d2276c0027ece..44ba3acf4131f 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -119,6 +119,11 @@ export class CommentableCase { return this.subCase?.id; } + private get owner(): string { + // TODO: check for subCase?.attributes.owner here + return this.collection.attributes.owner; + } + private buildRefsToCase(): SavedObjectReference[] { const subCaseSOType = SUB_CASE_SAVED_OBJECT; const caseSOType = CASE_SAVED_OBJECT; @@ -244,10 +249,12 @@ export class CommentableCase { createdDate, user, commentReq, + id, }: { createdDate: string; user: User; commentReq: CommentRequest; + id: string; }): Promise { try { if (commentReq.type === CommentType.alert) { @@ -260,6 +267,10 @@ export class CommentableCase { } } + if (commentReq.owner !== this.owner) { + throw Boom.badRequest('The owner field of the comment must match the case'); + } + const [comment, commentableCase] = await Promise.all([ this.attachmentService.create({ soClient: this.soClient, @@ -270,6 +281,7 @@ export class CommentableCase { ...user, }), references: this.buildRefsToCase(), + id, }), this.update({ date: createdDate, user }), ]); diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index e7dcbf0111f55..08a177ee04f9d 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -586,6 +586,7 @@ describe('common utils', () => { full_name: 'Elastic', username: 'elastic', associationType: AssociationType.case, + owner: 'securitySolution', }; const res = transformNewComment(comment); @@ -613,6 +614,7 @@ describe('common utils', () => { comment: 'A comment', type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', + owner: 'securitySolution', associationType: AssociationType.case, }; @@ -645,6 +647,7 @@ describe('common utils', () => { email: null, full_name: null, username: null, + owner: 'securitySolution', associationType: AssociationType.case, }; @@ -675,7 +678,10 @@ describe('common utils', () => { expect( countAlerts( createCommentFindResponse([ - { ids: ['1'], comments: [{ comment: '', type: CommentType.user }] }, + { + ids: ['1'], + comments: [{ comment: '', type: CommentType.user, owner: 'securitySolution' }], + }, ]).saved_objects[0] ) ).toBe(0); @@ -696,6 +702,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, + owner: 'securitySolution', }, ], }, @@ -719,6 +726,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, + owner: 'securitySolution', }, ], }, @@ -739,6 +747,7 @@ describe('common utils', () => { { alertId: ['a', 'b'], index: '', + owner: 'securitySolution', type: CommentType.alert, rule: { id: 'rule-id-1', @@ -747,6 +756,7 @@ describe('common utils', () => { }, { comment: '', + owner: 'securitySolution', type: CommentType.user, }, ], @@ -766,6 +776,7 @@ describe('common utils', () => { ids: ['1'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -780,6 +791,7 @@ describe('common utils', () => { ids: ['2'], comments: [ { + owner: 'securitySolution', comment: '', type: CommentType.user, }, @@ -803,6 +815,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -834,6 +847,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index c4cad60f4d465..7f38be2ba806d 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -286,6 +286,7 @@ export const getAlertInfoFromComments = (comments: CommentRequest[] | undefined) type NewCommentArgs = CommentRequest & { associationType: AssociationType; createdDate: string; + owner: string; email?: string | null; full_name?: string | null; username?: string | null; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 7777a0b36a1f1..b8167e742d55e 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -23,6 +23,7 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { schema.object({ includeSubCaseComments: schema.maybe(schema.boolean()), subCaseId: schema.maybe(schema.string()), + owner: schema.maybe(schema.string()), }) ), }, @@ -36,6 +37,7 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { caseID: request.params.case_id, includeSubCaseComments: request.query?.includeSubCaseComments, subCaseID: request.query?.subCaseId, + owner: request.query?.owner, }), }); } catch (error) { diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index fdfa722d18def..2308e90320c62 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -21,6 +21,7 @@ interface GetAttachmentArgs extends ClientArgs { interface CreateAttachmentArgs extends ClientArgs { attributes: AttachmentAttributes; references: SavedObjectReference[]; + id: string; } interface UpdateArgs { @@ -61,11 +62,12 @@ export class AttachmentService { } } - public async create({ soClient, attributes, references }: CreateAttachmentArgs) { + public async create({ soClient, attributes, references, id }: CreateAttachmentArgs) { try { this.log.debug(`Attempting to POST a new comment`); return await soClient.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references, + id, }); } catch (error) { this.log.error(`Error on POST a new comment: ${error}`); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 8608653c41b34..221fc576a726d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -12,8 +12,22 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; // if you add a value here you'll likely also need to make changes here: // x-pack/plugins/cases/server/authorization/index.ts -const readOperations: string[] = ['getCase', 'findCases']; -const writeOperations: string[] = ['createCase', 'deleteCase', 'updateCase']; +const readOperations: string[] = [ + 'getCase', + 'findCases', + 'getComment', + 'getAllComments', + 'findComments', +]; +const writeOperations: string[] = [ + 'createCase', + 'deleteCase', + 'updateCase', + 'createComment', + 'deleteAllComments', + 'deleteComment', + 'updateComments', +]; const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { From 6fb28ce383377be8323fd412de8aaf6559ec80dd Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 21 Apr 2021 16:11:50 -0400 Subject: [PATCH 02/25] Adding authorization to rest of comment apis --- .../cases/server/authorization/index.ts | 4 +- .../cases/server/authorization/types.ts | 2 +- .../cases/server/authorization/utils.ts | 10 ++- .../cases/server/client/attachments/add.ts | 1 - .../cases/server/client/attachments/get.ts | 70 +++++++++++++++++-- .../cases/server/client/attachments/update.ts | 17 ++++- .../plugins/cases/server/client/cases/find.ts | 4 +- .../plugins/cases/server/client/cases/mock.ts | 3 + .../cases/server/client/cases/utils.ts | 4 ++ x-pack/plugins/cases/server/client/utils.ts | 38 +++++++--- .../plugins/cases/server/common/utils.test.ts | 3 + .../server/connectors/case/index.test.ts | 2 + .../cases/server/connectors/case/index.ts | 1 + .../cases/server/connectors/case/schema.ts | 3 + .../api/__fixtures__/mock_saved_objects.ts | 6 ++ .../api/cases/comments/get_all_comment.ts | 2 +- .../cases/server/scripts/sub_cases/index.ts | 1 + .../feature_privilege_builder/cases.test.ts | 40 +++++++++++ .../feature_privilege_builder/cases.ts | 2 +- .../components/add_comment/index.test.tsx | 1 + .../cases/components/add_comment/index.tsx | 3 +- .../components/case_view/helpers.test.tsx | 2 + .../timeline_actions/add_to_case_action.tsx | 2 + .../public/cases/containers/api.test.tsx | 1 + .../public/cases/containers/mock.ts | 3 + .../containers/use_post_comment.test.tsx | 1 + 26 files changed, 198 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 4fa256080a9c3..748b3e77febeb 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -107,9 +107,9 @@ export const Operations: Record - uniq([...fields, 'owner']); +export const includeFieldsRequiredForAuthentication = ( + fields: string[] | undefined +): string[] | undefined => { + if (fields === undefined) { + return; + } + return uniq([...fields, 'owner']); +}; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index b58f01c08368a..4cc9ca7f868ec 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -27,7 +27,6 @@ import { SubCaseAttributes, CaseResponse, User, - CommentRequestAlertType, AlertCommentRequestRt, CommentRequest, } from '../../../common/api'; diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index b8f131083e200..e4a26d102352f 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -31,12 +31,19 @@ import { import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; -import { ensureAuthorized, getAuthorizationFilter } from '../utils'; +import { + combineAuthorizedAndOwnerFilter, + combineFilters, + ensureAuthorized, + getAuthorizationFilter, +} from '../utils'; import { Operations } from '../../authorization'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, subCaseId: rt.string, + owner: rt.union([rt.array(rt.string), rt.string]), }); type FindQueryParams = rt.TypeOf; @@ -50,7 +57,7 @@ export interface GetAllArgs { caseID: string; includeSubCaseComments?: boolean; subCaseID?: string; - owner?: string; + owner?: string | string[]; } export interface GetArgs { @@ -63,14 +70,42 @@ export interface GetArgs { */ export async function find( { caseID, queryParams }: FindArgs, - { savedObjectsClient: soClient, caseService, logger }: CasesClientArgs + clientArgs: CasesClientArgs ): Promise { + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; + try { checkEnabledCaseConnectorOrThrow(queryParams?.subCaseId); + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + auditLogger, + operation: Operations.findComments, + }); + const id = queryParams?.subCaseId ?? caseID; const associationType = queryParams?.subCaseId ? AssociationType.subCase : AssociationType.case; const { filter, ...queryWithoutFilter } = queryParams ?? {}; + + // if the fields property was defined, make sure we include the 'owner' field in the response + const fields = includeFieldsRequiredForAuthentication(queryWithoutFilter.fields); + + // combine any passed in filter property and the filter for the appropriate owner + const combinedFilter = combineFilters([ + esKuery.fromKueryExpression(filter), + combineAuthorizedAndOwnerFilter(queryParams?.owner, authorizationFilter), + ]); + const args = queryParams ? { caseService, @@ -83,8 +118,9 @@ export async function find( page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', - filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, + filter: combinedFilter, ...queryWithoutFilter, + fields, }, associationType, } @@ -96,11 +132,22 @@ export async function find( page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', + filter: combinedFilter, }, associationType, }; const theComments = await caseService.getCommentsByAssociation(args); + + ensureSavedObjectsAreAuthorized( + theComments.saved_objects.map((comment) => ({ + owner: comment.attributes.owner, + id: comment.id, + })) + ); + + logSuccessfulAuthorization(); + return CommentsResponseRt.encode(transformComments(theComments)); } catch (error) { throw createCaseError({ @@ -178,22 +225,24 @@ export async function getAll( ); } - // TODO: finish this call combineFieldWithKueryNodeFilter const { - filter, + filter: authFilter, ensureSavedObjectsAreAuthorized, logSuccessfulAuthorization, - } = getAuthorizationFilter({ + } = await getAuthorizationFilter({ authorization, auditLogger, operation: Operations.getAllComments, }); + const filter = combineAuthorizedAndOwnerFilter(owner, authFilter); + if (subCaseID) { comments = await caseService.getAllSubCaseComments({ soClient, id: subCaseID, options: { + filter, sortField: defaultSortField, }, }); @@ -203,11 +252,18 @@ export async function getAll( id: caseID, includeSubCaseComments, options: { + filter, sortField: defaultSortField, }, }); } + ensureSavedObjectsAreAuthorized( + comments.saved_objects.map((comment) => ({ id: comment.id, owner: comment.attributes.owner })) + ); + + logSuccessfulAuthorization(); + return AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)); } catch (error) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 79b1f5bfc0225..1e82d593586b4 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -15,8 +15,9 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/consta import { AttachmentService, CaseService } from '../../services'; import { CaseResponse, CommentPatchRequest } from '../../../common/api'; import { CasesClientArgs } from '..'; -import { decodeCommentRequest } from '../utils'; +import { decodeCommentRequest, ensureAuthorized } from '../utils'; import { createCaseError } from '../../common/error'; +import { Operations } from '../../authorization'; export interface UpdateArgs { caseID: string; @@ -89,6 +90,8 @@ export async function update( logger, user, userActionService, + authorization, + auditLogger, } = clientArgs; try { @@ -116,6 +119,14 @@ export async function update( attachmentId: queryCommentId, }); + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.updateComment, + savedObjectIDs: [myComment.id], + owners: [myComment.attributes.owner], + }); + if (myComment == null) { throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); } @@ -124,6 +135,10 @@ export async function update( throw Boom.badRequest(`You cannot change the type of the comment.`); } + if (myComment.attributes.owner !== queryRestAttributes.owner) { + throw Boom.badRequest(`You cannot change the owner of the comment.`); + } + const saveObjType = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const caseRef = myComment.references.find((c) => c.type === saveObjType); diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 8334beb102cb9..56df537b98968 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -88,9 +88,7 @@ export const find = async ({ ? queryParams.searchFields : [queryParams.searchFields] : queryParams.searchFields, - fields: queryParams.fields - ? includeFieldsRequiredForAuthentication(queryParams.fields) - : queryParams.fields, + fields: includeFieldsRequiredForAuthentication(queryParams.fields), }, subCaseOptions: caseQueries.subCase, }); diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 490519187f49e..1d46f5715c4ba 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -39,6 +39,7 @@ export const comment: CommentResponse = { email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -66,6 +67,7 @@ export const commentAlert: CommentResponse = { email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -83,6 +85,7 @@ export const commentAlertMultipleIds: CommentResponseAlertsType = { alertId: ['alert-id-1', 'alert-id-2'], index: 'alert-index-1', type: CommentType.alert as const, + owner: 'securitySolution', }; export const commentGeneratedAlert: CommentResponseAlertsType = { diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 8bac4956a9e5f..c45f976e680c5 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -329,11 +329,13 @@ export const isCommentAlertType = ( export const getCommentContextFromAttributes = ( attributes: CommentAttributes ): CommentRequestUserType | CommentRequestAlertType => { + const owner = attributes.owner; switch (attributes.type) { case CommentType.user: return { type: CommentType.user, comment: attributes.comment, + owner, }; case CommentType.generatedAlert: case CommentType.alert: @@ -342,11 +344,13 @@ export const getCommentContextFromAttributes = ( alertId: attributes.alertId, index: attributes.index, rule: attributes.rule, + owner, }; default: return { type: CommentType.user, comment: '', + owner, }; } }; diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index df28d2d1f9aa3..0a52b7ba47332 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -146,6 +146,36 @@ export const buildFilter = ({ ); }; +/** + * Combines the authorized filters with the requested owners. + */ +export const combineAuthorizedAndOwnerFilter = ( + owner?: string[] | string, + authorizationFilter?: KueryNode, + savedObjectType?: string +): KueryNode | undefined => { + const ownerFilter = buildFilter({ + filters: owner, + field: 'owner', + operator: 'or', + type: savedObjectType, + }); + + return combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter); +}; + +/** + * Combines Kuery nodes and accepts an array with a mixture of undefined and KueryNodes. This will filter out the undefined + * filters and return a KueryNode with the filters and'd together. + */ +export function combineFilters(nodes: Array): KueryNode | undefined { + const filters = nodes.filter((node): node is KueryNode => node !== undefined); + if (filters.length <= 0) { + return; + } + return nodeBuilder.and(filters); +} + /** * Constructs the filters used for finding cases and sub cases. * There are a few scenarios that this function tries to handle when constructing the filters used for finding cases @@ -305,14 +335,6 @@ export const constructQueryOptions = ({ } }; -/** - * Combines a string field with a kuery node to build a complete kuery node for use in the find API. - */ -export function combineFieldWithKueryNodeFilter(filterField: FilterField, kueryNode?: KueryNode) { - const filter = buildFilter(filterField); - return combineFilterWithAuthorizationFilter(filter, kueryNode); -} - interface CompareArrays { addedItems: string[]; deletedItems: string[]; diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 08a177ee04f9d..bc1cef9cc4f38 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -600,6 +600,7 @@ describe('common utils', () => { "full_name": "Elastic", "username": "elastic", }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -630,6 +631,7 @@ describe('common utils', () => { "full_name": undefined, "username": undefined, }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -663,6 +665,7 @@ describe('common utils', () => { "full_name": null, "username": null, }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", 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 876b8909b9317..7d3767ec7fe42 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -1134,6 +1134,7 @@ describe('case connector', () => { username: 'awesome', }, id: 'mock-comment', + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: null, @@ -1157,6 +1158,7 @@ describe('case connector', () => { comment: { comment: 'a comment', type: CommentType.user, + owner: 'securitySolution', }, }, }; diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index 6f8132d77a05f..f647c67d286d9 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -178,6 +178,7 @@ export const transformConnectorComment = ( alertId: ids, index: indices, rule, + owner: comment.owner, }; } catch (e) { throw createCaseError({ diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 1637cec7520be..e44d9d9774c96 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -15,11 +15,13 @@ export const CaseConfigurationSchema = schema.object({}); const ContextTypeUserSchema = schema.object({ type: schema.literal(CommentType.user), comment: schema.string(), + owner: schema.string(), }); const ContextTypeAlertGroupSchema = schema.object({ type: schema.literal(CommentType.generatedAlert), alerts: schema.string(), + owner: schema.string(), }); export type ContextTypeGeneratedAlertType = typeof ContextTypeAlertGroupSchema.type; @@ -33,6 +35,7 @@ const ContextTypeAlertSchema = schema.object({ id: schema.nullable(schema.string()), name: schema.nullable(schema.string()), }), + owner: schema.string(), }); export type ContextTypeAlertSchemaType = typeof ContextTypeAlertSchema.type; 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 bb4e529192df3..bfc274a9fce5b 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 @@ -250,6 +250,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:00.177Z', @@ -282,6 +283,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T21:55:14.633Z', @@ -315,6 +317,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: '2019-11-25T22:32:30.608Z', @@ -348,6 +351,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, rule: { @@ -385,6 +389,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, rule: { @@ -422,6 +427,7 @@ export const mockCaseComments: Array> = [ email: 'testemail@elastic.co', username: 'elastic', }, + owner: 'securitySolution', pushed_at: null, pushed_by: null, rule: { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index b8167e742d55e..dcb1019c07799 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -23,7 +23,7 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { schema.object({ includeSubCaseComments: schema.maybe(schema.boolean()), subCaseId: schema.maybe(schema.string()), - owner: schema.maybe(schema.string()), + owner: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), }) ), }, diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index ba3bcaa65091c..b76c7ac06eff3 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -103,6 +103,7 @@ async function handleGenGroupAlerts(argv: any) { console.log('Case id: ', caseID); const comment: ContextTypeGeneratedAlertType = { + owner: 'securitySolution', type: CommentType.generatedAlert, alerts: createAlertsString( argv.ids.map((id: string) => ({ diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index 1b1932f864090..5dc3c04ef83d4 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -72,6 +72,9 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:observability/getCase", "cases:1.0.0-zeta1:observability/findCases", + "cases:1.0.0-zeta1:observability/getComment", + "cases:1.0.0-zeta1:observability/getAllComments", + "cases:1.0.0-zeta1:observability/findComments", ] `); }); @@ -107,9 +110,16 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:security/getCase", "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getComment", + "cases:1.0.0-zeta1:security/getAllComments", + "cases:1.0.0-zeta1:security/findComments", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/createComment", + "cases:1.0.0-zeta1:security/deleteAllComments", + "cases:1.0.0-zeta1:security/deleteComment", + "cases:1.0.0-zeta1:security/updateComment", ] `); }); @@ -146,11 +156,21 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:security/getCase", "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getComment", + "cases:1.0.0-zeta1:security/getAllComments", + "cases:1.0.0-zeta1:security/findComments", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/createComment", + "cases:1.0.0-zeta1:security/deleteAllComments", + "cases:1.0.0-zeta1:security/deleteComment", + "cases:1.0.0-zeta1:security/updateComment", "cases:1.0.0-zeta1:obs/getCase", "cases:1.0.0-zeta1:obs/findCases", + "cases:1.0.0-zeta1:obs/getComment", + "cases:1.0.0-zeta1:obs/getAllComments", + "cases:1.0.0-zeta1:obs/findComments", ] `); }); @@ -187,18 +207,38 @@ describe(`cases`, () => { Array [ "cases:1.0.0-zeta1:security/getCase", "cases:1.0.0-zeta1:security/findCases", + "cases:1.0.0-zeta1:security/getComment", + "cases:1.0.0-zeta1:security/getAllComments", + "cases:1.0.0-zeta1:security/findComments", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/createComment", + "cases:1.0.0-zeta1:security/deleteAllComments", + "cases:1.0.0-zeta1:security/deleteComment", + "cases:1.0.0-zeta1:security/updateComment", "cases:1.0.0-zeta1:other-security/getCase", "cases:1.0.0-zeta1:other-security/findCases", + "cases:1.0.0-zeta1:other-security/getComment", + "cases:1.0.0-zeta1:other-security/getAllComments", + "cases:1.0.0-zeta1:other-security/findComments", "cases:1.0.0-zeta1:other-security/createCase", "cases:1.0.0-zeta1:other-security/deleteCase", "cases:1.0.0-zeta1:other-security/updateCase", + "cases:1.0.0-zeta1:other-security/createComment", + "cases:1.0.0-zeta1:other-security/deleteAllComments", + "cases:1.0.0-zeta1:other-security/deleteComment", + "cases:1.0.0-zeta1:other-security/updateComment", "cases:1.0.0-zeta1:obs/getCase", "cases:1.0.0-zeta1:obs/findCases", + "cases:1.0.0-zeta1:obs/getComment", + "cases:1.0.0-zeta1:obs/getAllComments", + "cases:1.0.0-zeta1:obs/findComments", "cases:1.0.0-zeta1:other-obs/getCase", "cases:1.0.0-zeta1:other-obs/findCases", + "cases:1.0.0-zeta1:other-obs/getComment", + "cases:1.0.0-zeta1:other-obs/getAllComments", + "cases:1.0.0-zeta1:other-obs/findComments", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 221fc576a726d..22b821c8a6602 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -26,7 +26,7 @@ const writeOperations: string[] = [ 'createComment', 'deleteAllComments', 'deleteComment', - 'updateComments', + 'updateComment', ]; const allOperations: string[] = [...readOperations, ...writeOperations]; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 9c06fc032f819..db55072090129 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -45,6 +45,7 @@ const defaultPostComment = { const sampleData: CommentRequest = { comment: 'what a cool comment', type: CommentType.user, + owner: 'securitySolution', }; describe('AddComment ', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index acd27e99a857f..57b717c11bb35 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -86,7 +86,8 @@ export const AddComment = React.memo( } postComment({ caseId, - data: { ...data, type: CommentType.user }, + // TODO: get plugin name + data: { ...data, type: CommentType.user, owner: 'securitySolution' }, updateCase: onCommentPosted, subCaseId, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx index 18a76e2766d8d..d0385b1a45f52 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx @@ -19,6 +19,7 @@ const comments: Comment[] = [ id: 'comment-id', createdAt: '2020-02-19T23:06:33.798Z', createdBy: { username: 'elastic' }, + owner: 'securitySolution', rule: { id: null, name: null, @@ -37,6 +38,7 @@ const comments: Comment[] = [ id: 'comment-id', createdAt: '2020-02-19T23:06:33.798Z', createdBy: { username: 'elastic' }, + owner: 'securitySolution', pushedAt: null, pushedBy: null, rule: { diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 45c1355cecfa7..09af79ba0b147 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -91,6 +91,8 @@ const AddToCaseActionComponent: React.FC = ({ id: rule?.id != null ? rule.id[0] : null, name: rule?.name != null ? rule.name[0] : null, }, + // TODO: get plugin name + owner: 'securitySolution', }, updateCase, }); 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 8f0fb3ea5a1d0..c8b5eb5674a12 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 @@ -420,6 +420,7 @@ describe('Case Configuration API', () => { }); const data = { comment: 'comment', + owner: 'securitySolution', type: CommentType.user as const, }; 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 947de140ccbb0..6880a105b1ce6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -47,6 +47,7 @@ export const basicComment: Comment = { id: basicCommentId, createdAt: basicCreatedAt, createdBy: elasticUser, + owner: 'securitySolution', pushedAt: null, pushedBy: null, updatedAt: null, @@ -62,6 +63,7 @@ export const alertComment: Comment = { id: 'alert-comment-id', createdAt: basicCreatedAt, createdBy: elasticUser, + owner: 'securitySolution', pushedAt: null, pushedBy: null, rule: { @@ -232,6 +234,7 @@ export const basicCommentSnake: CommentResponse = { id: basicCommentId, created_at: basicCreatedAt, created_by: elasticUserSnake, + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: null, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index 4d4ac5d071fa5..d0bab3e6f241b 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -19,6 +19,7 @@ describe('usePostComment', () => { const samplePost = { comment: 'a comment', type: CommentType.user as const, + owner: 'securitySolution', }; const updateCaseCallback = jest.fn(); beforeEach(() => { From 2e0239cf604a230eaa74de96e3cfc6fe2040de96 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 21 Apr 2021 18:11:34 -0400 Subject: [PATCH 03/25] Starting the comment rbac tests --- .../case_api_integration/common/lib/mock.ts | 3 + .../case_api_integration/common/lib/utils.ts | 37 ++- .../tests/common/cases/delete_cases.ts | 6 +- .../tests/common/cases/find_cases.ts | 24 +- .../tests/common/cases/get_case.ts | 2 +- .../tests/common/cases/patch_cases.ts | 148 ++++++---- .../tests/common/comments/delete_comment.ts | 12 +- .../tests/common/comments/find_comments.ts | 278 +++++++++++++++++- .../tests/common/comments/get_all_comments.ts | 12 +- .../tests/common/comments/get_comment.ts | 6 +- .../tests/common/comments/patch_comment.ts | 79 ++++- .../tests/common/comments/post_comment.ts | 142 +++++---- .../tests/trial/cases/push_case.ts | 2 +- 13 files changed, 602 insertions(+), 149 deletions(-) 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 c3a6cb8714115..002d672288ec4 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -74,6 +74,7 @@ export const userActionPostResp: CasesClientPostRequest = { export const postCommentUserReq: CommentRequestUserType = { comment: 'This is a cool comment', type: CommentType.user, + owner: 'securitySolution', }; export const postCommentAlertReq: CommentRequestAlertType = { @@ -81,6 +82,7 @@ export const postCommentAlertReq: CommentRequestAlertType = { index: 'test-index', rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, + owner: 'securitySolution', }; export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { @@ -89,6 +91,7 @@ export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { { _id: 'test-id2', _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, ]), type: CommentType.generatedAlert, + owner: 'securitySolution', }; export const postCaseResp = ( 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 2ff5e9d71985b..cd88157a07e41 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -47,6 +47,7 @@ import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/ import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types'; import { User } from './authentication/types'; +import { superUser } from './authentication/users'; function toArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -527,7 +528,7 @@ export const deleteMappings = async (es: KibanaClient): Promise => { }); }; -export const getSpaceUrlPrefix = (spaceId: string) => { +export const getSpaceUrlPrefix = (spaceId?: string) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; @@ -577,13 +578,17 @@ export const findCasesAsUser = async ({ return res; }; +interface OwnerEntity { + owner: string; +} + export const ensureSavedObjectIsAuthorized = ( - cases: CaseResponse[], + entities: OwnerEntity[], numberOfExpectedCases: number, owners: string[] ) => { - expect(cases.length).to.eql(numberOfExpectedCases); - cases.forEach((theCase) => expect(owners.includes(theCase.owner)).to.be(true)); + expect(entities.length).to.eql(numberOfExpectedCases); + entities.forEach((entity) => expect(owners.includes(entity.owner)).to.be(true)); }; export const createCase = async ( @@ -624,15 +629,25 @@ export const deleteCases = async ({ return body; }; -export const createComment = async ( - supertest: st.SuperTest, - caseId: string, - params: CommentRequest, - expectedHttpCode: number = 200 -): Promise => { +export const createComment = async ({ + supertest, + caseId, + params, + space, + user = superUser, + expectedHttpCode = 200, +}: { + supertest: st.SuperTest; + caseId: string; + params: CommentRequest; + space?: string; + user?: User; + expectedHttpCode?: number; +}): Promise => { const { body: theCase } = await supertest - .post(`${CASES_URL}/${caseId}/comments`) + .post(`${getSpaceUrlPrefix(space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') + .auth(user.username, user.password) .send(params) .expect(expectedHttpCode); 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 2c50ac8a453f9..8c65817598cf2 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 @@ -47,7 +47,11 @@ export default ({ getService }: FtrProviderContext): void => { it(`should delete a case's comments when that case gets deleted`, async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); // ensure that we can get the comment before deleting the case await getComment(supertest, postedCase.id, patchedCase.comments![0].id); 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 ca3b0201c1454..b170396b3e447 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 @@ -134,8 +134,12 @@ export default ({ getService }: FtrProviderContext): void => { const postedCase = await createCase(supertest, postCaseReq); // post 2 comments - await createComment(supertest, postedCase.id, postCommentUserReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const cases = await findCases(supertest); expect(cases).to.eql({ @@ -542,14 +546,14 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases', async () => { await Promise.all([ // Create case owned by the security solution user - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: secOnly, space: 'space1', owner: 'securitySolutionFixture', }), // Create case owned by the observability user - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: obsOnly, space: 'space1', @@ -614,14 +618,14 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { await Promise.all([ // super user creates a case with owner securitySolutionFixture - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: superUser, space: 'space1', owner: 'securitySolutionFixture', }), // super user creates a case with owner observabilityFixture - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: superUser, space: 'space1', @@ -677,13 +681,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { await Promise.all([ - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: obsSec, space: 'space1', owner: 'securitySolutionFixture', }), - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: obsSec, space: 'space1', @@ -703,13 +707,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { await Promise.all([ - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: obsSec, space: 'space1', owner: 'securitySolutionFixture', }), - await createCaseAsUser({ + createCaseAsUser({ supertestWithoutAuth, user: obsSec, space: 'space1', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts index 8239cbadbaa2f..7f1e346b3754c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a case with comments', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); const theCase = await getCase(supertest, postedCase.id, true); const comment = removeServerGeneratedPropertiesFromSavedObject( diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 1d7baabaf93b0..76d1c7a44620b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -181,7 +181,11 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); await updateCase(supertest, { cases: [ { @@ -394,7 +398,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); await updateCase( supertest, { @@ -471,11 +479,16 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const updatedInd1WithComment = await createComment(supertest, individualCase1.id, { - alertId: signalID, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedInd1WithComment = await createComment({ + supertest, + caseId: individualCase1.id, + params: { + alertId: signalID, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolution', + }, }); const individualCase2 = await createCase(supertest, { @@ -485,11 +498,16 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const updatedInd2WithComment = await createComment(supertest, individualCase2.id, { - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedInd2WithComment = await createComment({ + supertest, + caseId: individualCase2.id, + params: { + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolution', + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -604,18 +622,28 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const updatedIndWithComment = await createComment(supertest, individualCase.id, { - alertId: signalIDInFirstIndex, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedIndWithComment = await createComment({ + supertest, + caseId: individualCase.id, + params: { + alertId: signalIDInFirstIndex, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolution', + }, }); - const updatedIndWithComment2 = await createComment(supertest, updatedIndWithComment.id, { - alertId: signalIDInSecondIndex, - index: signalsIndex2, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, + const updatedIndWithComment2 = await createComment({ + supertest, + caseId: updatedIndWithComment.id, + params: { + alertId: signalIDInSecondIndex, + index: signalsIndex2, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolution', + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -706,14 +734,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolution', }, - type: CommentType.alert, }); await es.indices.refresh({ index: alert._index }); @@ -756,13 +789,18 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolution', }, }); @@ -801,14 +839,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolution', }, - type: CommentType.alert, }); // Update the status of the case with sync alerts off @@ -857,13 +900,18 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - const caseUpdated = await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolution', }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index cd4e72f6f9315..840c3a746adca 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -38,7 +38,11 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should delete a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const comment = await deleteComment(supertest, postedCase.id, patchedCase.comments![0].id); expect(comment).to.eql({}); @@ -48,7 +52,11 @@ export default ({ getService }: FtrProviderContext): void => { describe('unhappy path', () => { it('404s when comment belongs to different case', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const error = (await deleteComment( supertest, 'fake-id', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 43e128c1e41fa..672572c62eb56 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -6,21 +6,37 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentAlertReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, + createCaseAsUser, + createComment, createSubCase, deleteAllCaseItems, deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, } from '../../../../common/lib/utils'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); @@ -151,5 +167,263 @@ export default ({ getService }: FtrProviderContext): void => { expect(subCaseComments.comments[1].type).to.be(CommentType.user); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct comments', async () => { + const space1 = 'space1'; + + const [secCase, obsCase] = await Promise.all([ + // Create case owned by the security solution user + createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: space1, + owner: 'securitySolutionFixture', + }), + // Create case owned by the observability user + createCaseAsUser({ + supertestWithoutAuth, + user: obsOnly, + space: space1, + owner: 'observabilityFixture', + }), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + user: secOnly, + space: space1, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + user: obsOnly, + space: space1, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyRead, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments } = await supertest + .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); + + ensureSavedObjectIsAuthorized(caseComments, scenario.numExpectedEntites, scenario.owners); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: scenario.space, + owner: 'securitySolutionFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + user: superUser, + space: scenario.space, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // user should not be able to read comments + await supertest + .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(403); + }); + } + + it('should return no comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'observabilityFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + user: superUser, + space: 'space1', + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: comments } = await supertest + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture&searchFields=owner` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(comments, 0, ['securitySolutionFixture']); + }); + + // TODO: create test that checks that you can't create a comment with a different owner than the case + // TODO: create test that checks that you can't modify the owner of a comment + + // TODO: Finish these + // 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/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 736d04f43ed05..64cada8f36436 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -33,8 +33,16 @@ export default ({ getService }: FtrProviderContext): void => { it('should get multiple comments for a single case', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment(supertest, postedCase.id, postCommentUserReq); - await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const comments = await getAllComments(supertest, postedCase.id); expect(comments.length).to.eql(2); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 441f01843f865..c2ed65c9bf9ba 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -32,7 +32,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should get a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const comment = await getComment(supertest, postedCase.id, patchedCase.comments![0].id); expect(comment).to.eql(patchedCase.comments![0]); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index b73b89d33e9c6..557eac55b3e97 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -147,7 +147,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; const updatedCase = await updateComment(supertest, postedCase.id, { @@ -155,6 +159,7 @@ export default ({ getService }: FtrProviderContext): void => { version: patchedCase.comments![0].version, comment: newComment, type: CommentType.user, + owner: 'securitySolution', }); const userComment = updatedCase.comments![0] as AttributesTypeUser; @@ -165,7 +170,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch an alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); const updatedCase = await updateComment(supertest, postedCase.id, { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, @@ -176,6 +185,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolution', }); const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; @@ -199,6 +209,7 @@ export default ({ getService }: FtrProviderContext): void => { version: 'version', type: CommentType.user, comment: 'comment', + owner: 'securitySolution', }, 404 ); @@ -213,6 +224,7 @@ export default ({ getService }: FtrProviderContext): void => { version: 'version', type: CommentType.user, comment: 'comment', + owner: 'securitySolution', }, 404 ); @@ -220,7 +232,11 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 400s when trying to change comment type', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); await updateComment( supertest, @@ -235,6 +251,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolution', }, 400 ); @@ -242,7 +259,11 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 400s when missing attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); await updateComment( supertest, @@ -258,7 +279,11 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 400s when adding excess attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); for (const attribute of ['alertId', 'index']) { await updateComment( @@ -270,6 +295,7 @@ export default ({ getService }: FtrProviderContext): void => { comment: 'a comment', type: CommentType.user, [attribute]: attribute, + owner: 'securitySolution', }, 400 ); @@ -278,7 +304,11 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 400s when missing attributes for type alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); const allRequestAttributes = { type: CommentType.alert, @@ -308,7 +338,11 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 400s when adding excess attributes for type alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); for (const attribute of ['comment']) { await updateComment( @@ -324,6 +358,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolution', [attribute]: attribute, }, 400 @@ -333,7 +368,11 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path - 409s when conflict', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; await updateComment( @@ -344,6 +383,7 @@ export default ({ getService }: FtrProviderContext): void => { version: 'version-mismatch', type: CommentType.user, comment: newComment, + owner: 'securitySolution', }, 409 ); @@ -359,7 +399,11 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); await updateComment( supertest, @@ -370,6 +414,7 @@ export default ({ getService }: FtrProviderContext): void => { type: type as AlertComment, alertId, index, + owner: 'securitySolution', rule: postCommentAlertReq.rule, }, 400 @@ -383,11 +428,16 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, { - ...postCommentAlertReq, - alertId, - index, - type: type as AlertComment, + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: { + ...postCommentAlertReq, + alertId, + index, + owner: 'securitySolution', + type: type as AlertComment, + }, }); await updateComment(supertest, postedCase.id, { @@ -396,6 +446,7 @@ export default ({ getService }: FtrProviderContext): void => { type: type as AlertComment, alertId, index, + owner: 'securitySolution', rule: postCommentAlertReq.rule, }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index b63e21eea201a..a75105f59a801 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -67,7 +67,11 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should post a comment', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const comment = removeServerGeneratedPropertiesFromSavedObject( patchedCase.comments![0] as AttributesTypeUser ); @@ -89,7 +93,11 @@ export default ({ getService }: FtrProviderContext): void => { it('should post an alert', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentAlertReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); const comment = removeServerGeneratedPropertiesFromSavedObject( patchedCase.comments![0] as AttributesTypeAlerts ); @@ -113,7 +121,11 @@ export default ({ getService }: FtrProviderContext): void => { it('creates a user action', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment(supertest, postedCase.id, postCommentUserReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); const userActions = await getAllUserAction(supertest, postedCase.id); const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); @@ -133,44 +145,45 @@ export default ({ getService }: FtrProviderContext): void => { describe('unhappy path', () => { it('400s when type is missing', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { // @ts-expect-error bad: 'comment', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when missing attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, + caseId: postedCase.id, // @ts-expect-error - { + params: { type: CommentType.user, }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when adding excess attributes for type user', async () => { const postedCase = await createCase(supertest, postCaseReq); for (const attribute of ['alertId', 'index']) { - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { type: CommentType.user, [attribute]: attribute, comment: 'a comment', + owner: 'securitySolution', }, - 400 - ); + expectedHttpCode: 400, + }); } }); @@ -198,10 +211,10 @@ export default ({ getService }: FtrProviderContext): void => { const postedCase = await createCase(supertest, postCaseReq); for (const attribute of ['comment']) { - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { type: CommentType.alert, [attribute]: attribute, alertId: 'test-id', @@ -210,22 +223,23 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolution', }, - 400 - ); + expectedHttpCode: 400, + }); } }); it('400s when case is missing', async () => { - await createComment( + await createComment({ supertest, - 'not-exists', - { + caseId: 'not-exists', + params: { // @ts-expect-error bad: 'comment', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when adding an alert to a closed case', async () => { @@ -245,13 +259,23 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - await createComment(supertest, postedCase.id, postCommentAlertReq, 400); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('400s when adding an alert to a collection case', async () => { const postedCase = await createCase(supertest, postCollectionReq); - await createComment(supertest, postedCase.id, postCommentAlertReq, 400); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); }); it('400s when adding a generated alert to an individual case', async () => { @@ -312,14 +336,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolution', + type: CommentType.alert, }, - type: CommentType.alert, }); const { body: updatedAlert } = await supertest @@ -360,14 +389,19 @@ export default ({ getService }: FtrProviderContext): void => { const alert = signals.hits.hits[0]; expect(alert._source.signal.status).eql('open'); - await createComment(supertest, postedCase.id, { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolution', + type: CommentType.alert, }, - type: CommentType.alert, }); const { body: updatedAlert } = await supertest @@ -391,12 +425,12 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, - { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, - 400 - ); + caseId: postedCase.id, + params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, + expectedHttpCode: 400, + }); }); } @@ -406,17 +440,17 @@ export default ({ getService }: FtrProviderContext): void => { ]) { it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + await createComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment, }, - 200 - ); + expectedHttpCode: 200, + }); }); } }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 67773067ad2d4..88f7c15f4a5fe 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -129,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => { it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector(); - await createComment(supertest, postedCase.id, postCommentUserReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); const theCase = await pushCase(supertest, postedCase.id, connector.id); expect(theCase.comments![0].pushed_by).to.eql(defaultUser); From 5a63e2e28ce7cef41b0359ab5df1811ecec8b9c0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 22 Apr 2021 18:36:32 -0400 Subject: [PATCH 04/25] Fixing some of the rbac tests --- .../plugins/cases/common/api/cases/comment.ts | 8 + .../cases/server/client/attachments/get.ts | 23 +-- x-pack/plugins/cases/server/client/utils.ts | 12 ++ x-pack/plugins/cases/server/plugin.ts | 2 +- .../api/cases/comments/find_comments.ts | 11 +- .../common/lib/authentication/roles.ts | 3 +- .../case_api_integration/common/lib/mock.ts | 6 +- .../case_api_integration/common/lib/utils.ts | 14 +- .../tests/common/cases/find_cases.ts | 2 +- .../tests/common/comments/find_comments.ts | 163 ++++++++++-------- .../tests/common/comments/patch_comment.ts | 2 + .../tests/common/comments/post_comment.ts | 2 + 12 files changed, 145 insertions(+), 103 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 118af8a44a08e..942bdbdb75cb1 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { SavedObjectFindOptionsRt } from '../saved_object'; import { UserRT } from '../user'; @@ -115,6 +116,13 @@ export const CommentsResponseRt = rt.type({ export const AllCommentsResponseRt = rt.array(CommentResponseRt); +export const FindQueryParamsRt = rt.partial({ + ...SavedObjectFindOptionsRt.props, + subCaseId: rt.string, + owner: rt.union([rt.array(rt.string), rt.string]), +}); + +export type FindQueryParams = rt.TypeOf; export type AttributesTypeAlerts = rt.TypeOf; export type AttributesTypeUser = rt.TypeOf; export type CommentAttributes = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index e4a26d102352f..fbbf46e0132be 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -5,11 +5,9 @@ * 2.0. */ import Boom from '@hapi/boom'; -import * as rt from 'io-ts'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { CASE_COMMENT_SAVED_OBJECT, ENABLE_CASE_CONNECTOR } from '../../../common/constants'; -import { esKuery } from '../../../../../../src/plugins/data/server'; import { AllCommentsResponse, AllCommentsResponseRt, @@ -19,7 +17,7 @@ import { CommentResponseRt, CommentsResponse, CommentsResponseRt, - SavedObjectFindOptionsRt, + FindQueryParams, } from '../../../common/api'; import { checkEnabledCaseConnectorOrThrow, @@ -36,18 +34,11 @@ import { combineFilters, ensureAuthorized, getAuthorizationFilter, + stringToKueryNode, } from '../utils'; import { Operations } from '../../authorization'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; -const FindQueryParamsRt = rt.partial({ - ...SavedObjectFindOptionsRt.props, - subCaseId: rt.string, - owner: rt.union([rt.array(rt.string), rt.string]), -}); - -type FindQueryParams = rt.TypeOf; - export interface FindArgs { caseID: string; queryParams?: FindQueryParams; @@ -102,8 +93,12 @@ export async function find( // combine any passed in filter property and the filter for the appropriate owner const combinedFilter = combineFilters([ - esKuery.fromKueryExpression(filter), - combineAuthorizedAndOwnerFilter(queryParams?.owner, authorizationFilter), + stringToKueryNode(filter), + combineAuthorizedAndOwnerFilter( + queryParams?.owner, + authorizationFilter, + CASE_COMMENT_SAVED_OBJECT + ), ]); const args = queryParams diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 0a52b7ba47332..6e69f2c6fc406 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -15,6 +15,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsFindResponse } from 'kibana/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; +import { esKuery } from '../../../../../src/plugins/data/server'; import { CaseConnector, ESCasesConfigureAttributes, @@ -176,6 +177,17 @@ export function combineFilters(nodes: Array): KueryNode | return nodeBuilder.and(filters); } +/** + * Creates a KueryNode from a string expression. Returns undefined if the expression is undefined. + */ +export function stringToKueryNode(expression: string | undefined): KueryNode | undefined { + if (!expression) { + return; + } + + return esKuery.fromKueryExpression(expression); +} + /** * Constructs the filters used for finding cases and sub cases. * There are a few scenarios that this function tries to handle when constructing the filters used for finding cases diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 4493e04f307c4..ad601e132535b 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -68,7 +68,7 @@ export class CasePlugin { private securityPluginSetup?: SecurityPluginSetup; constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get('plugins', 'cases'); + this.log = this.initializerContext.logger.get(); this.clientFactory = new CasesClientFactory(this.log); } 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 b7b8a3b44146f..1fd84f5699913 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 @@ -5,8 +5,6 @@ * 2.0. */ -import * as rt from 'io-ts'; - import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; @@ -14,16 +12,11 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectFindOptionsRt, throwErrors } from '../../../../../common/api'; +import { FindQueryParamsRt, throwErrors, excess } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -const FindQueryParamsRt = rt.partial({ - ...SavedObjectFindOptionsRt.props, - subCaseId: rt.string, -}); - export function initFindCaseCommentsApi({ router, logger }: RouteDeps) { router.get( { @@ -38,7 +31,7 @@ export function initFindCaseCommentsApi({ router, logger }: RouteDeps) { async (context, request, response) => { try { const query = pipe( - FindQueryParamsRt.decode(request.query), + excess(FindQueryParamsRt).decode(request.query), fold(throwErrors(Boom.badRequest), identity) ); 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 cf21b01c3967e..57fc304e35bf5 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 @@ -36,7 +36,8 @@ export const globalRead: Role = { { feature: { securitySolutionFixture: ['read'], - observabilityFixture: ['all'], + // TODO: is this supposed to be all or read here? + observabilityFixture: ['read'], }, spaces: ['*'], }, 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 002d672288ec4..20511f8daab64 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -74,7 +74,7 @@ export const userActionPostResp: CasesClientPostRequest = { export const postCommentUserReq: CommentRequestUserType = { comment: 'This is a cool comment', type: CommentType.user, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }; export const postCommentAlertReq: CommentRequestAlertType = { @@ -82,7 +82,7 @@ export const postCommentAlertReq: CommentRequestAlertType = { index: 'test-index', rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }; export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { @@ -91,7 +91,7 @@ export const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { { _id: 'test-id2', _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, ]), type: CommentType.generatedAlert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }; export const postCaseResp = ( 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 cd88157a07e41..8263bb7836085 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -452,9 +452,9 @@ export const deleteAllCaseItems = async (es: KibanaClient) => { deleteCasesByESQuery(es), deleteSubCases(es), deleteCasesUserActions(es), - deleteComments(es), deleteConfiguration(es), deleteMappings(es), + deleteComments(es), ]); }; @@ -466,6 +466,7 @@ export const deleteCasesUserActions = async (es: KibanaClient): Promise => wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -477,6 +478,7 @@ export const deleteCasesByESQuery = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -492,6 +494,7 @@ export const deleteSubCases = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -503,6 +506,7 @@ export const deleteComments = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -514,6 +518,7 @@ export const deleteConfiguration = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -525,6 +530,7 @@ export const deleteMappings = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -534,14 +540,14 @@ export const getSpaceUrlPrefix = (spaceId?: string) => { export const createCaseAsUser = async ({ supertestWithoutAuth, - user, + user = superUser, space, owner, expectedHttpCode = 200, }: { supertestWithoutAuth: st.SuperTest; - user: User; - space: string; + user?: User; + space?: string; owner?: string; expectedHttpCode?: number; }): Promise => { 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 b170396b3e447..ca586ff957a7a 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 @@ -648,7 +648,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should NOT allow to pass a filter query parameter', async () => { await supertest .get( - `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner=observabilityFixture` + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` ) .set('kbn-xsrf', 'true') .send() diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 672572c62eb56..8df42fddb4b25 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -262,12 +262,16 @@ export default ({ getService }: FtrProviderContext): void => { caseID: obsCase.id, }, ]) { - const { body: caseComments } = await supertest + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) .auth(scenario.user.username, scenario.user.password) .expect(200); - ensureSavedObjectIsAuthorized(caseComments, scenario.numExpectedEntites, scenario.owners); + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); } }); @@ -295,7 +299,7 @@ export default ({ getService }: FtrProviderContext): void => { }); // user should not be able to read comments - await supertest + await supertestWithoutAuth .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) .auth(scenario.user.username, scenario.user.password) .expect(403); @@ -318,111 +322,130 @@ export default ({ getService }: FtrProviderContext): void => { caseId: obsCase.id, }); - const { body: comments } = await supertest + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth .get( `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ obsCase.id - }/comments/_find?search=securitySolutionFixture+observabilityFixture&searchFields=owner` + }/comments/_find?search=securitySolutionFixture+observabilityFixture` ) + // passing owner twice here because if you only place a single value it won't be treated as an array + // and it will fail the query parameter validation + .query({ searchFields: ['owner', 'owner'] }) .auth(secOnly.username, secOnly.password) .expect(200); // shouldn't find any comments since they were created under the observability ownership - ensureSavedObjectIsAuthorized(comments, 0, ['securitySolutionFixture']); + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); }); - // TODO: create test that checks that you can't create a comment with a different owner than the case - // TODO: create test that checks that you can't modify the owner of a comment + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'observabilityFixture', + }); - // TODO: Finish these - // 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 + await createComment({ + supertest: supertestWithoutAuth, + user: superUser, + space: 'space1', + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res } = await supertestWithoutAuth .get( - `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner=observabilityFixture` + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` ) - .set('kbn-xsrf', 'true') - .send() - .expect(400); + .auth(secOnly.username, secOnly.password) + .expect(200); + expect(res.comments.length).to.be(0); }); // This test ensures that the user is not allowed to define the namespaces query param // so she cannot search across spaces it('should NOT allow to pass a namespaces query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); + const obsCase = await createCaseAsUser({ + supertestWithoutAuth: supertest, + owner: 'observabilityFixture', + }); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) - .set('kbn-xsrf', 'true') - .send() + .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}/_find?notExists=papa`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).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({ + const obsCase = await createCaseAsUser({ supertestWithoutAuth, - user: obsSec, + user: superUser, + space: 'space1', + owner: 'observabilityFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + user: superUser, space: 'space1', - appendToUrl: 'owner=securitySolutionFixture', + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, }); - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?owner=observabilityFixture` + ) + .auth(obsOnly.username, obsOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 1, ['observabilityFixture']); }); 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({ + const obsCase = await createCaseAsUser({ supertestWithoutAuth, - user: secOnly, + user: superUser, + space: 'space1', + owner: 'observabilityFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + user: superUser, space: 'space1', - appendToUrl: 'owner=securitySolutionFixture&owner=observabilityFixture', + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, }); - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?owner=observabilityFixture` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['observabilityFixture']); }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index 557eac55b3e97..41ebf3f879996 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -199,6 +199,8 @@ export default ({ getService }: FtrProviderContext): void => { expect(alertComment.updated_by).to.eql(defaultUser); }); + // TODO: create test that checks that you can't modify the owner of a comment + it('unhappy path - 404s when comment is not there', async () => { const postedCase = await createCase(supertest, postCaseReq); await updateComment( diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index a75105f59a801..a9e4f46a42ee2 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -143,6 +143,8 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('unhappy path', () => { + // TODO: create test that checks that you can't create a comment with a different owner than the case + it('400s when type is missing', async () => { const postedCase = await createCase(supertest, postCaseReq); await createComment({ From a2c78422abae941e568dbb30df9efa6798cf155b Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 23 Apr 2021 17:25:46 -0400 Subject: [PATCH 05/25] Adding some integration tests --- .../plugins/cases/common/api/cases/comment.ts | 1 + .../cases/server/client/attachments/delete.ts | 6 +- .../cases/server/client/attachments/get.ts | 4 +- .../plugins/cases/server/common/utils.test.ts | 1 + .../server/connectors/case/index.test.ts | 2 + .../api/cases/comments/get_all_comment.ts | 1 + .../cases/server/services/cases/index.ts | 8 +- .../case_api_integration/common/lib/utils.ts | 95 ++++++--- .../tests/common/cases/delete_cases.ts | 13 +- .../tests/common/comments/delete_comment.ts | 200 +++++++++++++++++- .../tests/common/comments/find_comments.ts | 3 +- .../tests/common/comments/get_all_comments.ts | 115 +++++++++- .../tests/common/comments/get_comment.ts | 115 +++++++++- .../tests/common/comments/patch_comment.ts | 22 +- 14 files changed, 535 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 942bdbdb75cb1..54d01dd396e6f 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -119,6 +119,7 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt); export const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, subCaseId: rt.string, + // TODO: remove this owner: rt.union([rt.array(rt.string), rt.string]), }); diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 1c9c2e0b68d2e..f600aef64d1b6 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -61,6 +61,10 @@ export async function deleteAll( associationType: subCaseID ? AssociationType.subCase : AssociationType.case, }); + if (comments.total <= 0) { + throw Boom.notFound(`No comments found for ${id}.`); + } + await ensureAuthorized({ authorization, auditLogger, @@ -168,7 +172,7 @@ export async function deleteComment( }); } catch (error) { throw createCaseError({ - message: `Failed to delete comment in route case id: ${caseID} comment id: ${attachmentID} sub case id: ${subCaseID}: ${error}`, + message: `Failed to delete comment: ${caseID} comment id: ${attachmentID} sub case id: ${subCaseID}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index fbbf46e0132be..0f856f5f06dc8 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -95,6 +95,7 @@ export async function find( const combinedFilter = combineFilters([ stringToKueryNode(filter), combineAuthorizedAndOwnerFilter( + // TODO: remove this queryParams?.owner, authorizationFilter, CASE_COMMENT_SAVED_OBJECT @@ -230,7 +231,8 @@ export async function getAll( operation: Operations.getAllComments, }); - const filter = combineAuthorizedAndOwnerFilter(owner, authFilter); + // TODO: remove this + const filter = combineAuthorizedAndOwnerFilter(owner, authFilter, CASE_COMMENT_SAVED_OBJECT); if (subCaseID) { comments = await caseService.getAllSubCaseComments({ diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index bc1cef9cc4f38..4057cf4f3f52d 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -412,6 +412,7 @@ describe('common utils', () => { "username": "elastic", }, "id": "mock-comment-1", + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", 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 7d3767ec7fe42..a2afc1df4ecf7 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -753,6 +753,7 @@ describe('case connector', () => { comment: { comment: 'a comment', type: CommentType.user, + owner: 'securitySolution', }, }, }; @@ -773,6 +774,7 @@ describe('case connector', () => { id: null, name: null, }, + owner: 'securitySolution', }, }, }; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index dcb1019c07799..971b4947deeb8 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -23,6 +23,7 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { schema.object({ includeSubCaseComments: schema.maybe(schema.boolean()), subCaseId: schema.maybe(schema.string()), + // TODO: remove this owner: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), }) ), diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index c7d94b3c66329..236eb97e273e4 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -787,7 +787,7 @@ export class CaseService { options, }: FindCommentsArgs): Promise> { try { - this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); + this.log.debug(`Attempting to GET all comments internal for id ${JSON.stringify(id)}`); if (options?.page !== undefined || options?.perPage !== undefined) { return soClient.find({ type: CASE_COMMENT_SAVED_OBJECT, @@ -814,7 +814,7 @@ export class CaseService { ...cloneDeep(options), }); } catch (error) { - this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); + this.log.error(`Error on GET all comments internal for ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -858,7 +858,7 @@ export class CaseService { } this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ + return await this.getAllComments({ soClient, id, options: { @@ -891,7 +891,7 @@ export class CaseService { } this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); - return this.getAllComments({ + return await this.getAllComments({ soClient, id, options: { 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 8263bb7836085..dd9085dd658b3 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -650,14 +650,14 @@ export const createComment = async ({ user?: User; expectedHttpCode?: number; }): Promise => { - const { body: theCase } = await supertest + const { body: comment } = await supertest .post(`${getSpaceUrlPrefix(space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') .auth(user.username, user.password) .send(params) .expect(expectedHttpCode); - return theCase; + return comment; }; export const getAllUserAction = async ( @@ -688,45 +688,88 @@ export const updateCase = async ( return cases; }; -export const deleteComment = async ( - supertest: st.SuperTest, - caseId: string, - commentId: string, - expectedHttpCode: number = 204 -): Promise<{} | Error> => { +export const deleteComment = async ({ + supertest, + caseId, + commentId, + expectedHttpCode = 204, + auth = { user: superUser }, +}: { + supertest: st.SuperTest; + caseId: string; + commentId: string; + expectedHttpCode?: number; + auth?: { user: User; space?: string }; +}): Promise<{} | Error> => { const { body: comment } = await supertest - .delete(`${CASES_URL}/${caseId}/comments/${commentId}`) + .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode) .send(); return comment; }; -export const getAllComments = async ( - supertest: st.SuperTest, - caseId: string, - expectedHttpCode: number = 200 -): Promise => { - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseId}/comments`) +export const deleteAllComments = async ({ + supertest, + caseId, + expectedHttpCode = 204, + auth = { user: superUser }, +}: { + supertest: st.SuperTest; + caseId: string; + expectedHttpCode?: number; + auth?: { user: User; space?: string }; +}): Promise<{} | Error> => { + const { body: comment } = await supertest + .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') - .send() + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode) + .send(); + + return comment; +}; + +export const getAllComments = async ({ + supertest, + caseId, + expectedHttpCode = 200, + auth = { user: superUser }, + query = {}, +}: { + supertest: st.SuperTest; + caseId: string; + auth?: { user: User; space?: string }; + expectedHttpCode?: number; + query?: Record | string; +}): Promise => { + const { body: comments } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) + .auth(auth.user.username, auth.user.password) + .query(query) .expect(expectedHttpCode); return comments; }; -export const getComment = async ( - supertest: st.SuperTest, - caseId: string, - commentId: string, - expectedHttpCode: number = 200 -): Promise => { +export const getComment = async ({ + supertest, + caseId, + commentId, + expectedHttpCode = 200, + auth = { user: superUser }, +}: { + supertest: st.SuperTest; + caseId: string; + commentId: string; + expectedHttpCode?: number; + auth?: { user: User; space?: string }; +}): Promise => { const { body: comment } = await supertest - .get(`${CASES_URL}/${caseId}/comments/${commentId}`) - .set('kbn-xsrf', 'true') - .send() + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return comment; 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 8c65817598cf2..d101008937a90 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 @@ -53,12 +53,21 @@ export default ({ getService }: FtrProviderContext): void => { params: postCommentUserReq, }); // ensure that we can get the comment before deleting the case - await getComment(supertest, postedCase.id, patchedCase.comments![0].id); + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); await deleteCases({ supertest, caseIDs: [postedCase.id] }); // make sure the comment is now gone - await getComment(supertest, postedCase.id, patchedCase.comments![0].id, 404); + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + }); }); it('unhappy path - 404s when case is not there', async () => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 840c3a746adca..61e10e5140eb8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; @@ -21,7 +21,19 @@ import { createCase, createComment, deleteComment, + createCaseAsUser, + deleteAllComments, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -43,7 +55,11 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - const comment = await deleteComment(supertest, postedCase.id, patchedCase.comments![0].id); + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); expect(comment).to.eql({}); }); @@ -57,12 +73,12 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - const error = (await deleteComment( + const error = (await deleteComment({ supertest, - 'fake-id', - patchedCase.comments![0].id, - 404 - )) as Error; + caseId: 'fake-id', + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + })) as Error; expect(error.message).to.be( `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` @@ -70,7 +86,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('404s when comment is not there', async () => { - await deleteComment(supertest, 'fake-id', 'fake-id', 404); + await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); }); it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { @@ -158,5 +179,168 @@ export default ({ getService }: FtrProviderContext): void => { expect(allComments.length).to.eql(0); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a comment from the appropriate owner', async () => { + const secCase = await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + user: secOnly, + space: 'space1', + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should delete multiple comments from the appropriate owner', async () => { + const secCase = await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + user: secOnly, + space: 'space1', + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + user: secOnly, + space: 'space1', + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not delete a comment from a different owner', async () => { + const secCase = await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + user: secOnly, + space: 'space1', + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { + const postedCase = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + user: superUser, + space: 'space1', + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT delete a comment in a space with where the user does not have permissions', async () => { + const postedCase = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space2', + owner: 'securitySolutionFixture', + }); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + user: superUser, + space: 'space2', + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 8df42fddb4b25..98dd500f079ef 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -34,7 +34,6 @@ import { superUser, globalRead, obsSecRead, - obsSec, } from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export @@ -95,7 +94,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: patchedCase } = await supertest .post(`${CASES_URL}/${postedCase.id}/comments`) .set('kbn-xsrf', 'true') - .send({ comment: 'unique', type: CommentType.user }) + .send({ ...postCommentUserReq, comment: 'unique' }) .expect(200); const { body: caseComments } = await supertest diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 64cada8f36436..169fbf6db257a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; @@ -18,8 +18,20 @@ import { createCase, createComment, getAllComments, + createCaseAsUser, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -43,7 +55,7 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - const comments = await getAllComments(supertest, postedCase.id); + const comments = await getAllComments({ supertest, caseId: postedCase.id }); expect(comments.length).to.eql(2); }); @@ -121,5 +133,104 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.length).to.eql(0); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space1', + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space1', + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + let comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(comments.length).to.eql(2); + + // should retrieve the same number using the owner query param + comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(comments.length).to.eql(2); + } + }); + + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space1', + }); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space2', + owner: 'securitySolutionFixture', + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space2', + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index c2ed65c9bf9ba..82d4713e9cdae 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { @@ -17,8 +17,20 @@ import { createCase, createComment, getComment, + createCaseAsUser, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -37,13 +49,22 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - const comment = await getComment(supertest, postedCase.id, patchedCase.comments![0].id); + const comment = await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); expect(comment).to.eql(patchedCase.comments![0]); }); it('unhappy path - 404s when comment is not there', async () => { - await getComment(supertest, 'fake-id', 'fake-id', 404); + await getComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests @@ -57,9 +78,95 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get a sub case comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const comment = await getComment(supertest, caseInfo.id, caseInfo.comments![0].id); + const comment = await getComment({ + supertest, + caseId: caseInfo.id, + commentId: caseInfo.comments![0].id, + }); expect(comment.type).to.be(CommentType.generatedAlert); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space1', + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space1', + }); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const caseInfo = await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space2', + owner: 'securitySolutionFixture', + }); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + user: superUser, + space: 'space2', + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index 41ebf3f879996..f121c88be58ae 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -199,7 +199,27 @@ export default ({ getService }: FtrProviderContext): void => { expect(alertComment.updated_by).to.eql(defaultUser); }); - // TODO: create test that checks that you can't modify the owner of a comment + it('should not allow updating the owner of a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment( + supertest, + postedCase.id, + { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: postCommentUserReq.comment, + owner: 'changedOwner', + }, + 400 + ); + }); it('unhappy path - 404s when comment is not there', async () => { const postedCase = await createCase(supertest, postCaseReq); From f8dd4252d200fd4e4ccb4f58da511571f0e162ac Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 26 Apr 2021 12:51:19 -0400 Subject: [PATCH 06/25] Starting patch tests --- .../plugins/cases/common/api/cases/comment.ts | 2 - .../cases/server/client/attachments/get.ts | 21 ++---- .../api/cases/comments/get_all_comment.ts | 3 - .../case_api_integration/common/lib/utils.ts | 3 - .../tests/common/comments/find_comments.ts | 64 +------------------ .../tests/common/comments/get_all_comments.ts | 14 +--- .../tests/common/comments/patch_comment.ts | 31 +++++++++ .../tests/common/connectors/case.ts | 1 - 8 files changed, 39 insertions(+), 100 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 54d01dd396e6f..089bba8615725 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -119,8 +119,6 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt); export const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, subCaseId: rt.string, - // TODO: remove this - owner: rt.union([rt.array(rt.string), rt.string]), }); export type FindQueryParams = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 0f856f5f06dc8..f6f5bcfb4f046 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { CASE_COMMENT_SAVED_OBJECT, ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { AllCommentsResponse, @@ -30,7 +30,6 @@ import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; import { - combineAuthorizedAndOwnerFilter, combineFilters, ensureAuthorized, getAuthorizationFilter, @@ -48,7 +47,6 @@ export interface GetAllArgs { caseID: string; includeSubCaseComments?: boolean; subCaseID?: string; - owner?: string | string[]; } export interface GetArgs { @@ -92,15 +90,7 @@ export async function find( const fields = includeFieldsRequiredForAuthentication(queryWithoutFilter.fields); // combine any passed in filter property and the filter for the appropriate owner - const combinedFilter = combineFilters([ - stringToKueryNode(filter), - combineAuthorizedAndOwnerFilter( - // TODO: remove this - queryParams?.owner, - authorizationFilter, - CASE_COMMENT_SAVED_OBJECT - ), - ]); + const combinedFilter = combineFilters([stringToKueryNode(filter), authorizationFilter]); const args = queryParams ? { @@ -198,7 +188,7 @@ export async function get( * collections. If the entity is a sub case, pass in the subCaseID. */ export async function getAll( - { caseID, includeSubCaseComments, subCaseID, owner }: GetAllArgs, + { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, clientArgs: CasesClientArgs ): Promise { const { @@ -222,7 +212,7 @@ export async function getAll( } const { - filter: authFilter, + filter, ensureSavedObjectsAreAuthorized, logSuccessfulAuthorization, } = await getAuthorizationFilter({ @@ -231,9 +221,6 @@ export async function getAll( operation: Operations.getAllComments, }); - // TODO: remove this - const filter = combineAuthorizedAndOwnerFilter(owner, authFilter, CASE_COMMENT_SAVED_OBJECT); - if (subCaseID) { comments = await caseService.getAllSubCaseComments({ soClient, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 971b4947deeb8..7777a0b36a1f1 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -23,8 +23,6 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { schema.object({ includeSubCaseComments: schema.maybe(schema.boolean()), subCaseId: schema.maybe(schema.string()), - // TODO: remove this - owner: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), }) ), }, @@ -38,7 +36,6 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) { caseID: request.params.case_id, includeSubCaseComments: request.query?.includeSubCaseComments, subCaseID: request.query?.subCaseId, - owner: request.query?.owner, }), }); } catch (error) { 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 dd9085dd658b3..66478c4ba5692 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -737,18 +737,15 @@ export const getAllComments = async ({ caseId, expectedHttpCode = 200, auth = { user: superUser }, - query = {}, }: { supertest: st.SuperTest; caseId: string; auth?: { user: User; space?: string }; expectedHttpCode?: number; - query?: Record | string; }): Promise => { const { body: comments } = await supertest .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .auth(auth.user.username, auth.user.password) - .query(query) .expect(expectedHttpCode); return comments; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 98dd500f079ef..2f870b0cf698c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -305,7 +305,7 @@ export default ({ getService }: FtrProviderContext): void => { }); } - it('should return no comments when trying to exploit RBAC through the search query parameter', async () => { + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { const obsCase = await createCaseAsUser({ supertestWithoutAuth, user: superUser, @@ -327,9 +327,6 @@ export default ({ getService }: FtrProviderContext): void => { obsCase.id }/comments/_find?search=securitySolutionFixture+observabilityFixture` ) - // passing owner twice here because if you only place a single value it won't be treated as an array - // and it will fail the query parameter validation - .query({ searchFields: ['owner', 'owner'] }) .auth(secOnly.username, secOnly.password) .expect(200); @@ -387,64 +384,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should NOT allow to pass a non supported query parameter', async () => { await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); - }); - - it('should respect the owner filter when having permissions', async () => { - const obsCase = await createCaseAsUser({ - supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'observabilityFixture', - }); - - await createComment({ - supertest: supertestWithoutAuth, - user: superUser, - space: 'space1', - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ - obsCase.id - }/comments/_find?owner=observabilityFixture` - ) - .auth(obsOnly.username, obsOnly.password) - .expect(200); - - // shouldn't find any comments since they were created under the observability ownership - ensureSavedObjectIsAuthorized(res.comments, 1, ['observabilityFixture']); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - const obsCase = await createCaseAsUser({ - supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'observabilityFixture', - }); - - await createComment({ - supertest: supertestWithoutAuth, - user: superUser, - space: 'space1', - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ - obsCase.id - }/comments/_find?owner=observabilityFixture` - ) - .auth(secOnly.username, secOnly.password) - .expect(200); - - // shouldn't find any comments since they were created under the observability ownership - ensureSavedObjectIsAuthorized(res.comments, 0, ['observabilityFixture']); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 169fbf6db257a..2a482cb7e3e41 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -162,23 +162,13 @@ export default ({ getService }: FtrProviderContext): void => { }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - let comments = await getAllComments({ + const comments = await getAllComments({ supertest: supertestWithoutAuth, caseId: caseInfo.id, auth: { user, space: 'space1' }, }); expect(comments.length).to.eql(2); - - // should retrieve the same number using the owner query param - comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user, space: 'space1' }, - query: { owner: 'securitySolutionFixture' }, - }); - - expect(comments.length).to.eql(2); } }); @@ -208,7 +198,7 @@ export default ({ getService }: FtrProviderContext): void => { } }); - it('should NOT get a case in a space with no permissions', async () => { + it('should NOT get a comment in a space with no permissions', async () => { const caseInfo = await createCaseAsUser({ supertestWithoutAuth, user: superUser, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index f121c88be58ae..1f153d344883d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -474,5 +474,36 @@ export default ({ getService }: FtrProviderContext): void => { }); } }); + + describe('rbac', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should update a comment that the user has permissions for', async () => { + it('should patch a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment(supertest, postedCase.id, { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolution', + }); + + 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); + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts index 9be413015c051..fd9ec8142b49f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/connectors/case.ts @@ -718,7 +718,6 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should add a comment of type alert', async () => { - // TODO: don't do all this stuff const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); From ba380e9c8dbb9979f92d5f27929d81a4cd6a3037 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 26 Apr 2021 15:27:12 -0400 Subject: [PATCH 07/25] Working tests for comments --- .../cases/server/authorization/index.ts | 55 ++-- .../cases/server/client/attachments/update.ts | 8 +- x-pack/plugins/cases/server/client/utils.ts | 18 -- .../case_api_integration/common/lib/utils.ts | 88 ++--- .../tests/common/comments/delete_comment.ts | 71 ++-- .../tests/common/comments/find_comments.ts | 85 ++--- .../tests/common/comments/get_all_comments.ts | 45 ++- .../tests/common/comments/get_comment.ts | 42 ++- .../tests/common/comments/patch_comment.ts | 304 +++++++++++++----- .../tests/common/comments/post_comment.ts | 128 +++++++- 10 files changed, 509 insertions(+), 335 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index a7321a1d53556..01e03ceb9b5aa 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -6,7 +6,11 @@ */ import { EventType } from '../../../security/server'; -import { CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../common/constants'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_CONFIGURE_SAVED_OBJECT, + CASE_SAVED_OBJECT, +} from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; export * from './authorization'; @@ -82,6 +86,14 @@ export const Operations: Record { - const filters = Array.isArray(owner) ? owner : owner != null ? [owner] : []; - const ownerFilter = buildFilter({ - filters, - field: 'owner', - operator: 'or', - type: savedObjectType, - }); - - return authorizationFilter != null && ownerFilter != null - ? combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter) - : authorizationFilter ?? ownerFilter ?? undefined; -}; - /** * Combines the authorized filters with the requested owners. */ 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 29cf220495fe2..7a3bd966357c8 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -44,7 +44,7 @@ import { CasesStatusResponse, CasesConfigurationsResponse, } from '../../../../plugins/cases/common/api'; -import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; +import { 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'; @@ -538,56 +538,10 @@ export const deleteMappings = async (es: KibanaClient): Promise => { }); }; -export const getSpaceUrlPrefix = (spaceId?: string) => { +export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; -export const createCaseAsUser = async ({ - supertestWithoutAuth, - user = superUser, - 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; -}; - interface OwnerEntity { owner: string; } @@ -648,13 +602,13 @@ export const createComment = async ({ supertest, caseId, params, - auth = { user: superUser, space: undefined }, + auth = { user: superUser, space: null }, expectedHttpCode = 200, }: { supertest: st.SuperTest; caseId: string; params: CommentRequest; - auth?: { user: User; space: string | undefined }; + auth?: { user: User; space: string | null }; expectedHttpCode?: number; }): Promise => { const { body: comment } = await supertest @@ -699,13 +653,13 @@ export const deleteComment = async ({ caseId, commentId, expectedHttpCode = 204, - auth = { user: superUser }, + auth = { user: superUser, space: null }, }: { supertest: st.SuperTest; caseId: string; commentId: string; expectedHttpCode?: number; - auth?: { user: User; space?: string }; + auth?: { user: User; space: string | null }; }): Promise<{} | Error> => { const { body: comment } = await supertest .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) @@ -721,12 +675,12 @@ export const deleteAllComments = async ({ supertest, caseId, expectedHttpCode = 204, - auth = { user: superUser }, + auth = { user: superUser, space: null }, }: { supertest: st.SuperTest; caseId: string; expectedHttpCode?: number; - auth?: { user: User; space?: string }; + auth?: { user: User; space: string | null }; }): Promise<{} | Error> => { const { body: comment } = await supertest .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) @@ -742,11 +696,11 @@ export const getAllComments = async ({ supertest, caseId, expectedHttpCode = 200, - auth = { user: superUser, space: undefined }, + auth = { user: superUser, space: null }, }: { supertest: st.SuperTest; caseId: string; - auth?: { user: User; space: string | undefined }; + auth?: { user: User; space: string | null }; expectedHttpCode?: number; }): Promise => { const { body: comments } = await supertest @@ -778,16 +732,24 @@ export const getComment = async ({ return comment; }; -export const updateComment = async ( - supertest: st.SuperTest, - caseId: string, - req: CommentPatchRequest, - expectedHttpCode: number = 200 -): Promise => { +export const updateComment = async ({ + supertest, + caseId, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + req: CommentPatchRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: res } = await supertest - .patch(`${CASES_URL}/${caseId}/comments`) + .patch(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') .send(req) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return res; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 61e10e5140eb8..0f95ff6694359 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -21,7 +21,6 @@ import { createCase, createComment, deleteComment, - createCaseAsUser, deleteAllComments, } from '../../../../common/lib/utils'; import { @@ -188,19 +187,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should delete a comment from the appropriate owner', async () => { - const secCase = await createCaseAsUser({ + const secCase = await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - user: secOnly, - space: 'space1', + auth: { user: secOnly, space: 'space1' }, }); await deleteComment({ @@ -212,27 +210,25 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should delete multiple comments from the appropriate owner', async () => { - const secCase = await createCaseAsUser({ + const secCase = await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - user: secOnly, - space: 'space1', + auth: { user: secOnly, space: 'space1' }, }); await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - user: secOnly, - space: 'space1', + auth: { user: secOnly, space: 'space1' }, }); await deleteAllComments({ @@ -243,19 +239,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not delete a comment from a different owner', async () => { - const secCase = await createCaseAsUser({ + const secCase = await createCase( supertestWithoutAuth, - user: secOnly, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - user: secOnly, - space: 'space1', + auth: { user: secOnly, space: 'space1' }, }); await deleteComment({ @@ -278,19 +273,18 @@ export default ({ getService }: FtrProviderContext): void => { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { - const postedCase = await createCaseAsUser({ + const postedCase = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, }); await deleteComment({ @@ -311,19 +305,18 @@ export default ({ getService }: FtrProviderContext): void => { } it('should NOT delete a comment in a space with where the user does not have permissions', async () => { - const postedCase = await createCaseAsUser({ + const postedCase = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space2', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - user: superUser, - space: 'space2', + auth: { user: superUser, space: 'space2' }, }); await deleteComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 2f870b0cf698c..470c2481410ff 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -10,10 +10,14 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCommentAlertReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + getPostCaseRequest, + postCaseReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; import { createCaseAction, - createCaseAsUser, createComment, createSubCase, deleteAllCaseItems, @@ -23,6 +27,7 @@ import { deleteComments, ensureSavedObjectIsAuthorized, getSpaceUrlPrefix, + createCase, } from '../../../../common/lib/utils'; import { @@ -179,19 +184,19 @@ export default ({ getService }: FtrProviderContext): void => { const [secCase, obsCase] = await Promise.all([ // Create case owned by the security solution user - createCaseAsUser({ + createCase( supertestWithoutAuth, - user: secOnly, - space: space1, - owner: 'securitySolutionFixture', - }), - // Create case owned by the observability user - createCaseAsUser({ + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: space1 } + ), + createCase( supertestWithoutAuth, - user: obsOnly, - space: space1, - owner: 'observabilityFixture', - }), + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: space1 } + ), + // Create case owned by the observability user ]); await Promise.all([ @@ -199,15 +204,13 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - user: secOnly, - space: space1, + auth: { user: secOnly, space: space1 }, }), createComment({ supertest: supertestWithoutAuth, caseId: obsCase.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - user: obsOnly, - space: space1, + auth: { user: obsOnly, space: space1 }, }), ]); @@ -282,17 +285,16 @@ export default ({ getService }: FtrProviderContext): void => { scenario.space } - should NOT read a comment`, async () => { // super user creates a case and comment in the appropriate space - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: scenario.space, - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: scenario.space } + ); await createComment({ supertest: supertestWithoutAuth, - user: superUser, - space: scenario.space, + auth: { user: superUser, space: scenario.space }, params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, caseId: caseInfo.id, }); @@ -306,17 +308,16 @@ export default ({ getService }: FtrProviderContext): void => { } it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { - const obsCase = await createCaseAsUser({ + const obsCase = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'observabilityFixture', - }); + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ); await createComment({ supertest: supertestWithoutAuth, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); @@ -335,17 +336,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not allow retrieving unauthorized comments using the filter field', async () => { - const obsCase = await createCaseAsUser({ + const obsCase = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'observabilityFixture', - }); + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ); await createComment({ supertest: supertestWithoutAuth, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); @@ -364,10 +364,11 @@ export default ({ getService }: FtrProviderContext): void => { // 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 createCaseAsUser({ - supertestWithoutAuth: supertest, - owner: 'observabilityFixture', - }); + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); await createComment({ supertest, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 2a482cb7e3e41..9cd0ad2059333 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -18,7 +18,6 @@ import { createCase, createComment, getAllComments, - createCaseAsUser, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; import { @@ -138,27 +137,25 @@ export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); it('should get all comments when the user has the correct permissions', async () => { - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, }); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -173,19 +170,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not get comments when the user does not have correct permission', async () => { - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, }); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { @@ -199,19 +195,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should NOT get a comment in a space with no permissions', async () => { - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space2', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space2', + auth: { user: superUser, space: 'space2' }, }); await getAllComments({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 82d4713e9cdae..7b55d468312a1 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -17,7 +17,6 @@ import { createCase, createComment, getComment, - createCaseAsUser, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; import { @@ -91,19 +90,18 @@ export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); it('should get a comment when the user has the correct permissions', async () => { - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -117,19 +115,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not get comment when the user does not have correct permissions', async () => { - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space1', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space1', + auth: { user: superUser, space: 'space1' }, }); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { @@ -144,19 +141,18 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should NOT get a case in a space with no permissions', async () => { - const caseInfo = await createCaseAsUser({ + const caseInfo = await createCase( supertestWithoutAuth, - user: superUser, - space: 'space2', - owner: 'securitySolutionFixture', - }); + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - user: superUser, - space: 'space2', + auth: { user: superUser, space: 'space2' }, }); await getComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index 1f153d344883d..70c75b31a4ab8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -7,7 +7,7 @@ import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { @@ -21,6 +21,7 @@ import { postCaseReq, postCommentUserReq, postCommentAlertReq, + getPostCaseRequest, } from '../../../../common/lib/mock'; import { createCaseAction, @@ -34,6 +35,16 @@ import { createComment, updateComment, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -154,12 +165,16 @@ export default ({ getService }: FtrProviderContext): void => { }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const updatedCase = await updateComment(supertest, postedCase.id, { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - type: CommentType.user, - owner: 'securitySolution', + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolution', + }, }); const userComment = updatedCase.comments![0] as AttributesTypeUser; @@ -175,17 +190,21 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentAlertReq, }); - const updatedCase = await updateComment(supertest, postedCase.id, { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - alertId: 'new-id', - index: postCommentAlertReq.index, - rule: { - id: 'id', - name: 'name', + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolution', }, - owner: 'securitySolution', }); const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; @@ -207,49 +226,49 @@ export default ({ getService }: FtrProviderContext): void => { params: postCommentUserReq, }); - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.user, comment: postCommentUserReq.comment, owner: 'changedOwner', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('unhappy path - 404s when comment is not there', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', owner: 'securitySolution', }, - 404 - ); + expectedHttpCode: 404, + }); }); it('unhappy path - 404s when case is not there', async () => { - await updateComment( + await updateComment({ supertest, - 'fake-id', - { + caseId: 'fake-id', + req: { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', owner: 'securitySolution', }, - 404 - ); + expectedHttpCode: 404, + }); }); it('unhappy path - 400s when trying to change comment type', async () => { @@ -260,10 +279,10 @@ export default ({ getService }: FtrProviderContext): void => { params: postCommentUserReq, }); - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.alert, @@ -275,8 +294,8 @@ export default ({ getService }: FtrProviderContext): void => { }, owner: 'securitySolution', }, - 400 - ); + expectedHttpCode: 400, + }); }); it('unhappy path - 400s when missing attributes for type user', async () => { @@ -287,16 +306,16 @@ export default ({ getService }: FtrProviderContext): void => { params: postCommentUserReq, }); - await updateComment( + await updateComment({ supertest, - postedCase.id, + caseId: postedCase.id, // @ts-expect-error - { + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, }, - 400 - ); + expectedHttpCode: 400, + }); }); it('unhappy path - 400s when adding excess attributes for type user', async () => { @@ -308,10 +327,10 @@ export default ({ getService }: FtrProviderContext): void => { }); for (const attribute of ['alertId', 'index']) { - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, comment: 'a comment', @@ -319,8 +338,8 @@ export default ({ getService }: FtrProviderContext): void => { [attribute]: attribute, owner: 'securitySolution', }, - 400 - ); + expectedHttpCode: 400, + }); } }); @@ -344,17 +363,17 @@ export default ({ getService }: FtrProviderContext): void => { for (const attribute of ['alertId', 'index']) { const requestAttributes = omit(attribute, allRequestAttributes); - await updateComment( + await updateComment({ supertest, - postedCase.id, + caseId: postedCase.id, // @ts-expect-error - { + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, ...requestAttributes, }, - 400 - ); + expectedHttpCode: 400, + }); } }); @@ -367,10 +386,10 @@ export default ({ getService }: FtrProviderContext): void => { }); for (const attribute of ['comment']) { - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.alert, @@ -383,8 +402,8 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolution', [attribute]: attribute, }, - 400 - ); + expectedHttpCode: 400, + }); } }); @@ -397,18 +416,18 @@ export default ({ getService }: FtrProviderContext): void => { }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: 'version-mismatch', type: CommentType.user, comment: newComment, owner: 'securitySolution', }, - 409 - ); + expectedHttpCode: 409, + }); }); describe('alert format', () => { @@ -427,10 +446,10 @@ export default ({ getService }: FtrProviderContext): void => { params: postCommentAlertReq, }); - await updateComment( + await updateComment({ supertest, - patchedCase.id, - { + caseId: patchedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: type as AlertComment, @@ -439,8 +458,8 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolution', rule: postCommentAlertReq.rule, }, - 400 - ); + expectedHttpCode: 400, + }); }); } @@ -462,46 +481,157 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await updateComment(supertest, postedCase.id, { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: type as AlertComment, - alertId, - index, - owner: 'securitySolution', - rule: postCommentAlertReq.rule, + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolution', + rule: postCommentAlertReq.rule, + }, }); }); } }); describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + afterEach(async () => { await deleteAllCaseItems(es); }); it('should update a comment that the user has permissions for', async () => { - it('should patch a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space1' }, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + expect(userComment.owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a comment that has a different owner thant he user has access to', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const updatedCase = await updateComment(supertest, postedCase.id, { + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not update a comment in a space the user does not have permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, comment: newComment, - type: CommentType.user, - owner: 'securitySolution', - }); - - 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); + }, + auth: { user: secOnly, space: 'space2' }, + // getting the case will fail in the saved object layer with a 403 + expectedHttpCode: 403, }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index a9e4f46a42ee2..0e501648c512b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -7,7 +7,7 @@ import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; @@ -24,6 +24,7 @@ import { postCommentAlertReq, postCollectionReq, postCommentGenAlertReq, + getPostCaseRequest, } from '../../../../common/lib/mock'; import { createCaseAction, @@ -50,6 +51,16 @@ import { createRule, getQuerySignalIds, } from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -84,6 +95,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); // updates the case correctly after adding a comment @@ -112,6 +124,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); // updates the case correctly after adding a comment @@ -133,7 +146,7 @@ export default ({ getService }: FtrProviderContext): void => { action_field: ['comment'], action: 'create', action_by: defaultUser, - new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}"}`, + new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, old_value: null, case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, @@ -143,7 +156,19 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('unhappy path', () => { - // TODO: create test that checks that you can't create a comment with a different owner than the case + it('400s when attempting to create a comment with a different owner than the case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'securitySolutionFixture' }) + ); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + expectedHttpCode: 400, + }); + }); it('400s when type is missing', async () => { const postedCase = await createCase(supertest, postCaseReq); @@ -182,7 +207,7 @@ export default ({ getService }: FtrProviderContext): void => { type: CommentType.user, [attribute]: attribute, comment: 'a comment', - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 400, }); @@ -200,12 +225,18 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }; for (const attribute of ['alertId', 'index']) { const requestAttributes = omit(attribute, allRequestAttributes); - // @ts-expect-error - await createComment(supertest, postedCase.id, requestAttributes, 400); + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: requestAttributes, + expectedHttpCode: 400, + }); } }); @@ -225,7 +256,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 400, }); @@ -348,7 +379,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', type: CommentType.alert, }, }); @@ -401,7 +432,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', type: CommentType.alert, }, }); @@ -489,5 +520,84 @@ export default ({ getService }: FtrProviderContext): void => { expect(subCaseComments.comments[1].type).to.be(CommentType.user); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should create a comment when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not create a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not create a comment in a space the user does not have permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; From 7734644817322ebdcc086c7277654fa50330d24d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 26 Apr 2021 17:14:33 -0400 Subject: [PATCH 08/25] Working tests --- .../feature_privilege_builder/cases.test.ts | 62 +++++++------------ .../common/lib/authentication/index.ts | 18 +++++- .../tests/common/cases/get_case.ts | 2 + .../tests/common/cases/patch_cases.ts | 16 ++--- .../tests/common/comments/delete_comment.ts | 34 +++++++++- .../tests/common/comments/get_all_comments.ts | 17 +++-- .../tests/common/comments/patch_comment.ts | 23 +++---- .../tests/common/configure/migrations.ts | 7 ++- .../tests/common/configure/patch_configure.ts | 30 +++++---- .../user_actions/get_all_user_actions.ts | 18 ++++-- 10 files changed, 138 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index e974ea77fba7b..2898cce51a1ce 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -116,6 +116,9 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getAllComments", "cases:1.0.0-zeta1:security/findComments", + "cases:1.0.0-zeta1:security/getTags", + "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", @@ -123,12 +126,6 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/deleteAllComments", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", - "cases:1.0.0-zeta1:security/getTags", - "cases:1.0.0-zeta1:security/getReporters", - "cases:1.0.0-zeta1:security/findConfigurations", - "cases:1.0.0-zeta1:security/createCase", - "cases:1.0.0-zeta1:security/deleteCase", - "cases:1.0.0-zeta1:security/updateCase", "cases:1.0.0-zeta1:security/createConfiguration", "cases:1.0.0-zeta1:security/updateConfiguration", ] @@ -170,6 +167,9 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getAllComments", "cases:1.0.0-zeta1:security/findComments", + "cases:1.0.0-zeta1:security/getTags", + "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", @@ -177,21 +177,13 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/deleteAllComments", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", + "cases:1.0.0-zeta1:security/createConfiguration", + "cases:1.0.0-zeta1:security/updateConfiguration", "cases:1.0.0-zeta1:obs/getCase", "cases:1.0.0-zeta1:obs/findCases", "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getAllComments", "cases:1.0.0-zeta1:obs/findComments", - "cases:1.0.0-zeta1:security/getTags", - "cases:1.0.0-zeta1:security/getReporters", - "cases:1.0.0-zeta1:security/findConfigurations", - "cases:1.0.0-zeta1:security/createCase", - "cases:1.0.0-zeta1:security/deleteCase", - "cases:1.0.0-zeta1:security/updateCase", - "cases:1.0.0-zeta1:security/createConfiguration", - "cases:1.0.0-zeta1:security/updateConfiguration", - "cases:1.0.0-zeta1:obs/getCase", - "cases:1.0.0-zeta1:obs/findCases", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", "cases:1.0.0-zeta1:obs/findConfigurations", @@ -234,6 +226,9 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getAllComments", "cases:1.0.0-zeta1:security/findComments", + "cases:1.0.0-zeta1:security/getTags", + "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", @@ -241,11 +236,16 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/deleteAllComments", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", + "cases:1.0.0-zeta1:security/createConfiguration", + "cases:1.0.0-zeta1:security/updateConfiguration", "cases:1.0.0-zeta1:other-security/getCase", "cases:1.0.0-zeta1:other-security/findCases", "cases:1.0.0-zeta1:other-security/getComment", "cases:1.0.0-zeta1:other-security/getAllComments", "cases:1.0.0-zeta1:other-security/findComments", + "cases:1.0.0-zeta1:other-security/getTags", + "cases:1.0.0-zeta1:other-security/getReporters", + "cases:1.0.0-zeta1:other-security/findConfigurations", "cases:1.0.0-zeta1:other-security/createCase", "cases:1.0.0-zeta1:other-security/deleteCase", "cases:1.0.0-zeta1:other-security/updateCase", @@ -253,41 +253,21 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:other-security/deleteAllComments", "cases:1.0.0-zeta1:other-security/deleteComment", "cases:1.0.0-zeta1:other-security/updateComment", + "cases:1.0.0-zeta1:other-security/createConfiguration", + "cases:1.0.0-zeta1:other-security/updateConfiguration", "cases:1.0.0-zeta1:obs/getCase", "cases:1.0.0-zeta1:obs/findCases", "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getAllComments", "cases:1.0.0-zeta1:obs/findComments", - "cases:1.0.0-zeta1:other-obs/getCase", - "cases:1.0.0-zeta1:other-obs/findCases", - "cases:1.0.0-zeta1:other-obs/getComment", - "cases:1.0.0-zeta1:other-obs/getAllComments", - "cases:1.0.0-zeta1:other-obs/findComments", - "cases:1.0.0-zeta1:security/getTags", - "cases:1.0.0-zeta1:security/getReporters", - "cases:1.0.0-zeta1:security/findConfigurations", - "cases:1.0.0-zeta1:security/createCase", - "cases:1.0.0-zeta1:security/deleteCase", - "cases:1.0.0-zeta1:security/updateCase", - "cases:1.0.0-zeta1:security/createConfiguration", - "cases:1.0.0-zeta1:security/updateConfiguration", - "cases:1.0.0-zeta1:other-security/getCase", - "cases:1.0.0-zeta1:other-security/findCases", - "cases:1.0.0-zeta1:other-security/getTags", - "cases:1.0.0-zeta1:other-security/getReporters", - "cases:1.0.0-zeta1:other-security/findConfigurations", - "cases:1.0.0-zeta1:other-security/createCase", - "cases:1.0.0-zeta1:other-security/deleteCase", - "cases:1.0.0-zeta1:other-security/updateCase", - "cases:1.0.0-zeta1:other-security/createConfiguration", - "cases:1.0.0-zeta1:other-security/updateConfiguration", - "cases:1.0.0-zeta1:obs/getCase", - "cases:1.0.0-zeta1:obs/findCases", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", "cases:1.0.0-zeta1:obs/findConfigurations", "cases:1.0.0-zeta1:other-obs/getCase", "cases:1.0.0-zeta1:other-obs/findCases", + "cases:1.0.0-zeta1:other-obs/getComment", + "cases:1.0.0-zeta1:other-obs/getAllComments", + "cases:1.0.0-zeta1:other-obs/findComments", "cases:1.0.0-zeta1:other-obs/getTags", "cases:1.0.0-zeta1:other-obs/getReporters", "cases:1.0.0-zeta1:other-obs/findConfigurations", 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 bcc23896f85f8..a72141745e577 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 @@ -54,18 +54,30 @@ const createUsersAndRoles = async (getService: CommonFtrProviderContext['getServ export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { const spacesService = getService('spaces'); for (const space of spaces) { - await spacesService.delete(space.id); + try { + await spacesService.delete(space.id); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } } }; const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { const security = getService('security'); for (const user of users) { - await security.user.delete(user.username); + try { + await security.user.delete(user.username); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } } for (const role of roles) { - await security.role.delete(role.name); + try { + await security.role.delete(role.name); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users + } } }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts index 254222a5ac998..222632b41c297 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/get_case.ts @@ -76,6 +76,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); }); @@ -158,6 +159,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 76d1c7a44620b..b50c18192a05b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -487,7 +487,7 @@ export default ({ getService }: FtrProviderContext): void => { index: defaultSignalsIndex, rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -506,7 +506,7 @@ export default ({ getService }: FtrProviderContext): void => { index: defaultSignalsIndex, rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -630,7 +630,7 @@ export default ({ getService }: FtrProviderContext): void => { index: defaultSignalsIndex, rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -642,7 +642,7 @@ export default ({ getService }: FtrProviderContext): void => { index: signalsIndex2, rule: { id: 'test-rule-id', name: 'test-index-id' }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -745,7 +745,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'name', }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -800,7 +800,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -850,7 +850,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'name', }, type: CommentType.alert, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -911,7 +911,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 0f95ff6694359..353974632feb8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -269,7 +269,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { @@ -304,6 +304,38 @@ export default ({ getService }: FtrProviderContext): void => { }); } + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + // the find in the delete all will return no results + expectedHttpCode: 404, + }); + }); + it('should NOT delete a comment in a space with where the user does not have permissions', async () => { const postedCase = await createCase( supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 9cd0ad2059333..2be30ed7bc02c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -184,13 +184,22 @@ export default ({ getService }: FtrProviderContext): void => { auth: { user: superUser, space: 'space1' }, }); - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { - await getAllComments({ + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnly, returnCode: 200 }, + { user: obsOnlyRead, returnCode: 200 }, + ]) { + const comments = await getAllComments({ supertest: supertestWithoutAuth, caseId: caseInfo.id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, + auth: { user: scenario.user, space: 'space1' }, + expectedHttpCode: scenario.returnCode, }); + + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); + } } }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index 70c75b31a4ab8..fcaebddeb8bde 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -72,6 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }) .expect(400); @@ -173,7 +174,7 @@ export default ({ getService }: FtrProviderContext): void => { version: patchedCase.comments![0].version, comment: newComment, type: CommentType.user, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -203,7 +204,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, }); @@ -250,7 +251,7 @@ export default ({ getService }: FtrProviderContext): void => { version: 'version', type: CommentType.user, comment: 'comment', - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 404, }); @@ -265,7 +266,7 @@ export default ({ getService }: FtrProviderContext): void => { version: 'version', type: CommentType.user, comment: 'comment', - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 404, }); @@ -292,7 +293,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 400, }); @@ -336,7 +337,7 @@ export default ({ getService }: FtrProviderContext): void => { comment: 'a comment', type: CommentType.user, [attribute]: attribute, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 400, }); @@ -399,7 +400,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, - owner: 'securitySolution', + owner: 'securitySolutionFixture', [attribute]: attribute, }, expectedHttpCode: 400, @@ -424,7 +425,7 @@ export default ({ getService }: FtrProviderContext): void => { version: 'version-mismatch', type: CommentType.user, comment: newComment, - owner: 'securitySolution', + owner: 'securitySolutionFixture', }, expectedHttpCode: 409, }); @@ -455,7 +456,7 @@ export default ({ getService }: FtrProviderContext): void => { type: type as AlertComment, alertId, index, - owner: 'securitySolution', + owner: 'securitySolutionFixture', rule: postCommentAlertReq.rule, }, expectedHttpCode: 400, @@ -476,7 +477,7 @@ export default ({ getService }: FtrProviderContext): void => { ...postCommentAlertReq, alertId, index, - owner: 'securitySolution', + owner: 'securitySolutionFixture', type: type as AlertComment, }, }); @@ -490,7 +491,7 @@ export default ({ getService }: FtrProviderContext): void => { type: type as AlertComment, alertId, index, - owner: 'securitySolution', + owner: 'securitySolutionFixture', rule: postCommentAlertReq.rule, }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts index fd9baf39b49f9..cc2f6c414503d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts @@ -30,9 +30,10 @@ export default function createGetTests({ getService }: FtrProviderContext) { .send() .expect(200); - expect(body).key('connector'); - expect(body).not.key('connector_id'); - expect(body.connector).to.eql({ + expect(body.length).to.be(1); + expect(body[0]).key('connector'); + expect(body[0]).not.key('connector_id'); + expect(body[0].connector).to.eql({ id: 'connector-1', name: 'Connector 1', type: '.none', diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index c76e5f408e475..43c69dad83d8b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts @@ -78,13 +78,16 @@ export default ({ getService }: FtrProviderContext): void => { // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration(supertest); + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }), + ...reqWithoutOwner, version: configuration.version, }); @@ -135,13 +138,16 @@ export default ({ getService }: FtrProviderContext): void => { }) ); + // the update request doesn't accept the owner field + const { owner, ...rest } = getConfigurationRequest({ + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...getConfigurationRequest({ - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }), + ...rest, version: configuration.version, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 56a6d1b15004b..19911890929d2 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -293,12 +293,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await supertest.patch(`${CASES_URL}/${postedCase.id}/comments`).set('kbn-xsrf', 'true').send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - comment: newComment, - type: CommentType.user, - }); + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }) + .expect(200); const { body } = await supertest .get(`${CASES_URL}/${postedCase.id}/user_actions`) @@ -313,6 +318,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(JSON.parse(body[2].new_value)).to.eql({ comment: newComment, type: CommentType.user, + owner: 'securitySolutionFixture', }); }); }); From 86180fa5a4096169592a449ef0c37ff77af9820d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Apr 2021 08:55:43 -0400 Subject: [PATCH 09/25] Fixing some tests --- .../components/timeline_actions/add_to_case_action.test.tsx | 3 +++ .../tests/common/sub_cases/find_sub_cases.ts | 1 + .../tests/common/sub_cases/patch_sub_cases.ts | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 40a202f5257a7..58a0af0ba9cf2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -189,6 +189,7 @@ describe('AddToCaseAction', () => { name: 'rule-name', }, type: 'alert', + owner: 'securitySolution', }); }); @@ -226,6 +227,7 @@ describe('AddToCaseAction', () => { name: 'rule-name', }, type: 'alert', + owner: 'securitySolution', }); }); @@ -257,6 +259,7 @@ describe('AddToCaseAction', () => { name: null, }, type: 'alert', + owner: 'securitySolution', }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts index 14c0460c7583b..d54523bec0c4d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts @@ -298,6 +298,7 @@ export default ({ getService }: FtrProviderContext): void => { { _id: `${i}`, _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }; responses.push( await createSubCase({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts index 43526bca644db..442644463fa38 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts @@ -100,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -156,6 +157,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -225,6 +227,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -243,6 +246,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); @@ -354,6 +358,7 @@ export default function ({ getService }: FtrProviderContext) { }, ]), type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', }, }); From 3d4726dff19b2ff2a2a7b4dca035f6cca1f0e510 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Apr 2021 10:17:54 -0400 Subject: [PATCH 10/25] Fixing type issues from pulling in master --- .../cases/common/api/cases/user_actions.ts | 1 + .../server/authorization/audit_logger.ts | 16 +++--- .../cases/server/authorization/index.ts | 57 +++++++++++++------ .../cases/server/authorization/types.ts | 23 +++++--- .../cases/server/client/cases/create.ts | 5 +- .../cases/server/client/cases/delete.ts | 5 +- .../cases/server/client/configure/client.ts | 7 +-- x-pack/plugins/cases/server/client/utils.ts | 16 +++--- .../server/services/user_actions/helpers.ts | 1 + .../security_solution/cypress/objects/case.ts | 2 + .../cypress/tasks/api_calls/cases.ts | 1 + 11 files changed, 81 insertions(+), 53 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 55dfac391f3be..1b53adb002436 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -22,6 +22,7 @@ const UserActionFieldTypeRt = rt.union([ rt.literal('status'), rt.literal('settings'), rt.literal('sub_case'), + rt.literal('owner'), ]); const UserActionFieldRt = rt.array(UserActionFieldTypeRt); const UserActionRt = rt.union([ diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 2a739ea6e8106..216cf7d9c20e0 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { OperationDetails } from '.'; -import { AuditLogger, EventCategory, EventOutcome } from '../../../security/server'; +import { DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '.'; +import { AuditLogger } from '../../../security/server'; enum AuthorizationResult { Unauthorized = 'Unauthorized', @@ -51,9 +51,9 @@ export class AuthorizationAuditLogger { message: `${username ?? 'unknown user'} ${message}`, event: { action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: EventOutcome.SUCCESS, + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: ECS_OUTCOMES.success, }, ...(username != null && { user: { @@ -81,9 +81,9 @@ export class AuthorizationAuditLogger { message: `${username ?? 'unknown user'} ${message}`, event: { action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: EventOutcome.FAILURE, + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: ECS_OUTCOMES.failure, }, // add the user information if we have it ...(username != null && { diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 01e03ceb9b5aa..994f0cf6adb6b 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EventType } from '../../../security/server'; +import { EcsEventCategory, EcsEventOutcome, EcsEventType } from 'kibana/server'; import { CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, @@ -41,13 +41,34 @@ const deleteVerbs: Verbs = { past: 'deleted', }; +const eventTypes: Record = { + creation: 'creation', + deletion: 'deletion', + change: 'change', + access: 'access', +}; + +/** + * Database constant for ECS category for use for audit logging. + */ +export const DATABASE_CATEGORY: EcsEventCategory[] = ['database']; + +/** + * ECS Outcomes for audit logging. + */ +export const ECS_OUTCOMES: Record = { + failure: 'failure', + success: 'success', + unknown: 'unknown', +}; + /** * Definition of all APIs within the cases backend. */ export const Operations: Record = { // case operations [WriteOperations.CreateCase]: { - type: EventType.CREATION, + type: eventTypes.creation, name: WriteOperations.CreateCase, action: 'create-case', verbs: createVerbs, @@ -55,7 +76,7 @@ export const Operations: Record Promise; -// TODO: we need to have an operation per entity route so I think we need to create a bunch like -// getCase, getComment, getSubCase etc for each, need to think of a clever way of creating them for all the routes easily? - -// if you add a value here you'll likely also need to make changes here: -// x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +/** + * Read operations for the cases APIs. + * + * NOTE: If you add a value here you'll likely also need to make changes here: + * x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + */ export enum ReadOperations { GetCase = 'getCase', FindCases = 'findCases', @@ -37,7 +37,12 @@ export enum ReadOperations { FindConfigurations = 'findConfigurations', } -// TODO: comments +/** + * Write operations for the cases APIs. + * + * NOTE: If you add a value here you'll likely also need to make changes here: + * x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + */ export enum WriteOperations { CreateCase = 'createCase', DeleteCase = 'deleteCase', @@ -54,7 +59,7 @@ export enum WriteOperations { * Defines the structure for a case API route. */ export interface OperationDetails { - type: EventType; + type: EcsEventType; name: ReadOperations | WriteOperations; action: string; verbs: Verbs; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 15fbd34628182..3b792ea2ff2aa 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -25,8 +25,7 @@ import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createAuditMsg, ensureAuthorized, getConnectorFromConfiguration } from '../utils'; import { createCaseError } from '../../common/error'; -import { Operations } from '../../authorization'; -import { EventOutcome } from '../../../../security/server'; +import { ECS_OUTCOMES, Operations } from '../../authorization'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { flattenCaseSavedObject, @@ -86,7 +85,7 @@ export const create = async ( auditLogger?.log( createAuditMsg({ operation: Operations.createCase, - outcome: EventOutcome.UNKNOWN, + outcome: ECS_OUTCOMES.unknown, savedObjectID, }) ); diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 4657df2e71b30..7fc2b3927c22c 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -12,9 +12,8 @@ import { CasesClientArgs } from '..'; import { createCaseError } from '../../common/error'; import { AttachmentService, CaseService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { Operations } from '../../authorization'; +import { ECS_OUTCOMES, Operations } from '../../authorization'; import { createAuditMsg, ensureAuthorized } from '../utils'; -import { EventOutcome } from '../../../../security/server'; async function deleteSubCases({ attachmentService, @@ -93,7 +92,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P auditLogger?.log( createAuditMsg({ operation: Operations.deleteCase, - outcome: EventOutcome.UNKNOWN, + outcome: ECS_OUTCOMES.unknown, savedObjectID, }) ); diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 1037a2ff9d893..d810c2682618e 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -32,7 +32,6 @@ import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, } from '../../common'; -import { EventOutcome } from '../../../../security/server'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { getFields } from './get_fields'; @@ -41,7 +40,7 @@ import { getMappings } from './get_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; -import { Operations } from '../../authorization'; +import { ECS_OUTCOMES, Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter, createAuditMsg, @@ -280,7 +279,7 @@ async function update( auditLogger?.log( createAuditMsg({ operation: Operations.updateConfiguration, - outcome: EventOutcome.UNKNOWN, + outcome: ECS_OUTCOMES.unknown, savedObjectID: configuration.id, }) ); @@ -430,7 +429,7 @@ async function create( auditLogger?.log( createAuditMsg({ operation: Operations.createConfiguration, - outcome: EventOutcome.UNKNOWN, + outcome: ECS_OUTCOMES.unknown, savedObjectID, }) ); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 6e69f2c6fc406..38166262fcc5e 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -12,7 +12,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { SavedObjectsFindResponse } from 'kibana/server'; +import { EcsEventOutcome, SavedObjectsFindResponse } from 'kibana/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { esKuery } from '../../../../../src/plugins/data/server'; @@ -29,7 +29,7 @@ import { AlertCommentRequestRt, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; -import { AuditEvent, EventCategory, EventOutcome } from '../../../security/server'; +import { AuditEvent } from '../../../security/server'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { getIDsAndIndicesAsArrays, @@ -37,7 +37,7 @@ import { isCommentRequestTypeUser, SavedObjectFindOptionsKueryNode, } from '../common'; -import { Authorization, OperationDetails } from '../authorization'; +import { Authorization, DATABASE_CATEGORY, ECS_OUTCOMES, OperationDetails } from '../authorization'; import { AuditLogger } from '../../../security/server'; export const decodeCommentRequest = (comment: CommentRequest) => { @@ -573,7 +573,7 @@ export function createAuditMsg({ }: { operation: OperationDetails; savedObjectID?: string; - outcome?: EventOutcome; + outcome?: EcsEventOutcome; error?: Error; }): AuditEvent { const doc = @@ -582,7 +582,7 @@ export function createAuditMsg({ : `a ${operation.docType}`; const message = error ? `Failed attempt to ${operation.verbs.present} ${doc}` - : outcome === EventOutcome.UNKNOWN + : outcome === ECS_OUTCOMES.unknown ? `User is ${operation.verbs.progressive} ${doc}` : `User has ${operation.verbs.past} ${doc}`; @@ -590,9 +590,9 @@ export function createAuditMsg({ message, event: { action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: outcome ?? (error ? ECS_OUTCOMES.failure : ECS_OUTCOMES.success), }, ...(savedObjectID != null && { kibana: { diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index e987bd1685405..2ab3bdb5e1cee 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -157,6 +157,7 @@ const userActionFieldsAllowed: UserActionField = [ 'status', 'settings', 'sub_case', + 'owner', ]; interface CaseSubIDs { diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index a0135431c6543..278eab29f0a62 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -13,6 +13,7 @@ export interface TestCase { description: string; timeline: CompleteTimeline; reporter: string; + owner: string; } export interface Connector { @@ -45,6 +46,7 @@ export const case1: TestCase = { description: 'This is the case description', timeline, reporter: 'elastic', + owner: 'securitySolution', }; export const serviceNowConnector: Connector = { diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts index f73b8e47066d2..798cd184d6012 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/cases.ts @@ -24,6 +24,7 @@ export const createCase = (newCase: TestCase) => settings: { syncAlerts: true, }, + owner: newCase.owner, }, headers: { 'kbn-xsrf': 'cypress-creds' }, }); From 518db99c49d365aa24b8cc1b2597235f58fea025 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Apr 2021 12:41:54 -0400 Subject: [PATCH 11/25] Fixing connector tests that only work in trial license --- .../tests/basic/configure/create_connector.ts | 20 +++ .../tests/common/configure/get_configure.ts | 67 -------- .../tests/common/configure/get_connectors.ts | 70 +------- .../tests/common/configure/patch_configure.ts | 128 -------------- .../tests/common/configure/post_configure.ts | 61 ------- .../tests/trial/configure/get_configure.ts | 95 ++++++++++ .../tests/trial/configure/get_connectors.ts | 16 +- .../tests/trial/configure/index.ts | 18 ++ .../tests/trial/configure/patch_configure.ts | 162 ++++++++++++++++++ .../tests/trial/configure/post_configure.ts | 95 ++++++++++ .../security_and_spaces/tests/trial/index.ts | 1 + 11 files changed, 407 insertions(+), 326 deletions(-) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts new file mode 100644 index 0000000000000..a403e6d55be86 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.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 { createConnector, getServiceNowConnector } from '../../../../common/lib/utils'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function serviceNow({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('create service now action', () => { + it('should return 403 when creating a service now action', async () => { + await createConnector(supertest, getServiceNowConnector(), 403); + }); + }); +} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts index b26e8a3f3b381..279936ebbef46 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_configure.ts @@ -8,12 +8,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, @@ -21,8 +15,6 @@ import { getConfiguration, createConfiguration, getConfigurationRequest, - createConnector, - getServiceNowConnector, ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; import { @@ -42,21 +34,10 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('get_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); - afterEach(async () => { await deleteConfiguration(es); - await actionsRemover.removeAll(); }); it('should return an empty find body correctly if no configuration is loaded', async () => { @@ -91,54 +72,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql(getConfigurationOutput()); }); - it('should return a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - const configuration = await getConfiguration({ supertest }); - const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); - expect(data).to.eql( - getConfigurationOutput(false, { - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: null, - }, - }) - ); - }); - describe('rbac', () => { it('should return the correct configuration', async () => { await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts index cfa23a968182f..5156b9537583f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts @@ -8,86 +8,18 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - getCaseConnectors, - createConnector, - getServiceNowConnector, - getJiraConnector, - getResilientConnector, - getServiceNowSIRConnector, - getWebhookConnector, -} from '../../../../common/lib/utils'; +import { getCaseConnectors } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const actionsRemover = new ActionsRemover(supertest); describe('get_connectors', () => { - afterEach(async () => { - await actionsRemover.removeAll(); - }); - it('should return an empty find body correctly if no connectors are loaded', async () => { const connectors = await getCaseConnectors(supertest); expect(connectors).to.eql([]); }); - it('should return case owned connectors', async () => { - const sn = await createConnector(supertest, getServiceNowConnector()); - actionsRemover.add('default', sn.id, 'action', 'actions'); - - const jira = await createConnector(supertest, getJiraConnector()); - actionsRemover.add('default', jira.id, 'action', 'actions'); - - const resilient = await createConnector(supertest, getResilientConnector()); - actionsRemover.add('default', resilient.id, 'action', 'actions'); - - const sir = await createConnector(supertest, getServiceNowSIRConnector()); - actionsRemover.add('default', sir.id, 'action', 'actions'); - - // Should not be returned when getting the connectors - const webhook = await createConnector(supertest, getWebhookConnector()); - actionsRemover.add('default', webhook.id, 'action', 'actions'); - - const connectors = await getCaseConnectors(supertest); - expect(connectors).to.eql([ - { - id: jira.id, - actionTypeId: '.jira', - name: 'Jira Connector', - config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey' }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: resilient.id, - actionTypeId: '.resilient', - name: 'Resilient Connector', - config: { apiUrl: 'http://some.non.existent.com', orgId: 'pkey' }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: sn.id, - actionTypeId: '.servicenow', - name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, - isPreconfigured: false, - referencedByCount: 0, - }, - { - id: sir.id, - actionTypeId: '.servicenow-sir', - name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, - isPreconfigured: false, - referencedByCount: 0, - }, - ]); - }); - it.skip('filters out connectors that are not enabled in license', async () => { // TODO: Should find a way to downgrade license to gold and upgrade back to trial }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index 43c69dad83d8b..ced727f8e4e75 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts @@ -8,10 +8,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -20,10 +16,7 @@ import { deleteConfiguration, createConfiguration, updateConfiguration, - getServiceNowConnector, - createConnector, } from '../../../../common/lib/utils'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { secOnly, obsOnlyRead, @@ -39,17 +32,9 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); afterEach(async () => { await deleteConfiguration(es); @@ -67,119 +52,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); }); - it('should patch a configuration connector and create mappings', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - // Configuration is created with no connector so the mappings are empty - const configuration = await createConfiguration(supertest); - - // the update request doesn't accept the owner field - const { owner, ...reqWithoutOwner } = getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }); - - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...reqWithoutOwner, - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ - ...getConfigurationOutput(true), - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }, - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - }); - }); - - it('should mappings when updating the connector', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - // Configuration is created with connector so the mappings are created - const configuration = await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - // the update request doesn't accept the owner field - const { owner, ...rest } = getConfigurationRequest({ - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }); - - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...rest, - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ - ...getConfigurationOutput(true), - connector: { - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }, - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - }); - }); - it('should not patch a configuration with unsupported connector type', async () => { const configuration = await createConfiguration(supertest); await updateConfiguration( diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index a47c10efe5037..f1dae9f319109 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -9,10 +9,6 @@ import expect from '@kbn/expect'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -20,8 +16,6 @@ import { getConfigurationOutput, deleteConfiguration, createConfiguration, - createConnector, - getServiceNowConnector, getConfiguration, ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; @@ -41,17 +35,9 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('post_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); afterEach(async () => { await deleteConfiguration(es); @@ -73,53 +59,6 @@ export default ({ getService }: FtrProviderContext): void => { expect(configuration.length).to.be(1); }); - it('should create a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - const postRes = await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - const data = removeServerGeneratedPropertiesFromSavedObject(postRes); - expect(data).to.eql( - getConfigurationOutput(false, { - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: null, - }, - }) - ); - }); - it('should return an error when failing to get mapping', async () => { const postRes = await createConfiguration( supertest, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts new file mode 100644 index 0000000000000..6d556423893d5 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + createConnector, + createConfiguration, + getConfiguration, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_configure', () => { + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return a configuration with mapping', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 75d1378260b19..6faea0e1789bb 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -14,6 +14,8 @@ import { getServiceNowConnector, getJiraConnector, getResilientConnector, + createConnector, + getServiceNowSIRConnector, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -38,7 +40,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send({ name: 'An email action', - actionTypeId: '.email', + connector_type_id: '.email', config: { service: '__json', from: 'bob@example.com', @@ -62,6 +64,9 @@ export default ({ getService }: FtrProviderContext): void => { .send(getResilientConnector()) .expect(200); + const sir = await createConnector(supertest, getServiceNowSIRConnector()); + + actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); actionsRemover.add('default', emailConnector.id, 'action', 'actions'); actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); @@ -72,6 +77,7 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .send() .expect(200); + expect(connectors).to.eql([ { id: jiraConnector.id, @@ -105,6 +111,14 @@ export default ({ getService }: FtrProviderContext): void => { isPreconfigured: false, referencedByCount: 0, }, + { + id: sir.id, + actionTypeId: '.servicenow-sir', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + referencedByCount: 0, + }, ]); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts new file mode 100644 index 0000000000000..0c8c3931d1577 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('configuration tests', function () { + loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_connectors')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts new file mode 100644 index 0000000000000..9e82ce1f0c233 --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, + getServiceNowConnector, + createConnector, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should patch a configuration connector and create mappings', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with no connector so the mappings are empty + const configuration = await createConfiguration(supertest); + + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...reqWithoutOwner, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + + it('should mappings when updating the connector', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with connector so the mappings are created + const configuration = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + // the update request doesn't accept the owner field + const { owner, ...rest } = getConfigurationRequest({ + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...rest, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts new file mode 100644 index 0000000000000..503e0384859ec --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + createConnector, + getServiceNowConnector, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should create a configuration with mapping', async () => { + const connector = await createConnector(supertest, { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(postRes); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 6f2c3a6bb2701..5ba09dd56bd67 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -27,5 +27,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { // Trial loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./configure/index')); }); }; From a32b1ba068952c4cd2ae47010c7136e060b0a692 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Apr 2021 15:23:52 -0400 Subject: [PATCH 12/25] Attempting to fix cypress --- x-pack/plugins/cases/common/api/helpers.ts | 5 +++ .../cases/containers/configure/api.test.ts | 6 ++-- .../public/cases/containers/configure/api.ts | 29 ++++++++++++----- .../configure/use_configure.test.tsx | 2 ++ .../containers/configure/use_configure.tsx | 31 +++++++++++++++++-- .../public/cases/containers/utils.ts | 10 ++++++ 6 files changed, 70 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts index 43e292b91db4b..6b5f126c74fdb 100644 --- a/x-pack/plugins/cases/common/api/helpers.ts +++ b/x-pack/plugins/cases/common/api/helpers.ts @@ -14,6 +14,7 @@ import { SUB_CASES_URL, CASE_PUSH_URL, SUB_CASE_USER_ACTIONS_URL, + CASE_CONFIGURE_DETAILS_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -47,3 +48,7 @@ export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): stri export const getCasePushUrl = (caseId: string, connectorId: string): string => { return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId); }; + +export const getCaseConfigurationDetailsUrl = (configureID: string): string => { + return CASE_CONFIGURE_DETAILS_URL.replace('{configuration_id}', configureID); +}; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts index 999cb8d29d745..a1ed7311ac74b 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.test.ts @@ -53,7 +53,7 @@ describe('Case Configuration API', () => { describe('fetch configuration', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(caseConfigurationResposeMock); + fetchMock.mockResolvedValue([caseConfigurationResposeMock]); }); test('check url, method, signal', async () => { @@ -106,13 +106,14 @@ describe('Case Configuration API', () => { test('check url, body, method, signal', async () => { await patchCaseConfigure( + '123', { connector: { id: '456', name: 'My Connector 2', type: ConnectorTypes.none, fields: null }, version: 'WzHJ12', }, abortCtrl.signal ); - expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/123', { body: '{"connector":{"id":"456","name":"My Connector 2","type":".none","fields":null},"version":"WzHJ12"}', method: 'PATCH', @@ -122,6 +123,7 @@ describe('Case Configuration API', () => { test('happy path', async () => { const resp = await patchCaseConfigure( + '123', { connector: { id: '456', name: 'My Connector 2', type: ConnectorTypes.none, fields: null }, version: 'WzHJ12', diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts index 943724ef08398..142958ae2919b 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts @@ -12,6 +12,7 @@ import { CasesConfigurePatch, CasesConfigureResponse, CasesConfigureRequest, + CasesConfigurationsResponse, } from '../../../../../cases/common/api'; import { KibanaServices } from '../../../common/lib/kibana'; @@ -22,8 +23,13 @@ import { } from '../../../../../cases/common/constants'; import { ApiProps } from '../types'; -import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; +import { + convertToCamelCase, + decodeCaseConfigurationsResponse, + decodeCaseConfigureResponse, +} from '../utils'; import { CaseConfigure } from './types'; +import { getCaseConfigurationDetailsUrl } from '../../../../../cases/common/api/helpers'; export const fetchConnectors = async ({ signal }: ApiProps): Promise => { const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { @@ -34,8 +40,9 @@ export const fetchConnectors = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch( + const response = await KibanaServices.get().http.fetch( CASE_CONFIGURE_URL, { method: 'GET', @@ -43,11 +50,16 @@ export const getCaseConfigure = async ({ signal }: ApiProps): Promise( - decodeCaseConfigureResponse(response) - ) - : null; + if (!isEmpty(response)) { + const decodedConfigs = decodeCaseConfigurationsResponse(response); + if (Array.isArray(decodedConfigs) && decodedConfigs.length > 0) { + return convertToCamelCase(decodedConfigs[0]); + } else { + return null; + } + } else { + return null; + } }; export const getConnectorMappings = async ({ signal }: ApiProps): Promise => { @@ -77,11 +89,12 @@ export const postCaseConfigure = async ( }; export const patchCaseConfigure = async ( + id: string, caseConfiguration: CasesConfigurePatch, signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - CASE_CONFIGURE_URL, + getCaseConfigurationDetailsUrl(id), { method: 'PATCH', body: JSON.stringify(caseConfiguration), diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx index 44a503cd089ef..267e0f337c128 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx @@ -84,6 +84,7 @@ describe('useConfigure', () => { setCurrentConfiguration: result.current.setCurrentConfiguration, setMappings: result.current.setMappings, version: caseConfigurationCamelCaseResponseMock.version, + id: caseConfigurationCamelCaseResponseMock.id, }); }); }); @@ -286,6 +287,7 @@ describe('useConfigure', () => { Promise.resolve({ ...caseConfigurationCamelCaseResponseMock, version: '', + id: '', }) ); const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index ca817747e9191..21b1b6dc6392b 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -28,6 +28,7 @@ export interface State extends ConnectorConfiguration { mappings: CaseConnectorMapping[]; persistLoading: boolean; version: string; + id: string; } export type Action = | { @@ -54,6 +55,10 @@ export type Action = type: 'setVersion'; payload: string; } + | { + type: 'setID'; + payload: string; + } | { type: 'setClosureType'; closureType: ClosureType; @@ -85,6 +90,11 @@ export const configureCasesReducer = (state: State, action: Action) => { ...state, version: action.payload, }; + case 'setID': + return { + ...state, + id: action.payload, + }; case 'setCurrentConfiguration': { return { ...state, @@ -145,6 +155,7 @@ export const initialState: State = { mappings: [], persistLoading: false, version: '', + id: '', }; export const useCaseConfigure = (): ReturnUseCaseConfigure => { @@ -206,6 +217,14 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }); }, []); + // TODO: refactor + const setID = useCallback((id: string) => { + dispatch({ + payload: id, + type: 'setID', + }); + }, []); + const [, dispatchToaster] = useStateToaster(); const isCancelledRefetchRef = useRef(false); const abortCtrlRefetchRef = useRef(new AbortController()); @@ -229,6 +248,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setClosureType(res.closureType); } setVersion(res.version); + setID(res.id); setMappings(res.mappings); if (!state.firstLoad) { @@ -278,14 +298,17 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { const connectorObj = { connector, closure_type: closureType, - // TODO: use constant after https://github.com/elastic/kibana/pull/97646 is being merged - owner: 'securitySolution', }; const res = state.version.length === 0 - ? await postCaseConfigure(connectorObj, abortCtrlPersistRef.current.signal) + ? await postCaseConfigure( + // TODO: use constant after https://github.com/elastic/kibana/pull/97646 is being merged + { ...connectorObj, owner: 'securitySolution' }, + abortCtrlPersistRef.current.signal + ) : await patchCaseConfigure( + state.id, { ...connectorObj, version: state.version, @@ -299,6 +322,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setClosureType(res.closureType); } setVersion(res.version); + setID(res.id); setMappings(res.mappings); if (setCurrentConfiguration != null) { setCurrentConfiguration({ @@ -340,6 +364,7 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { setMappings, setPersistLoading, setVersion, + setID, state, ] ); diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 7c33e4481b2aa..7c291bc77c80f 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -22,6 +22,8 @@ import { CasesStatusResponseRt, CasesStatusResponse, throwErrors, + CasesConfigurationsResponse, + CaseConfigurationsResponseRt, CasesConfigureResponse, CaseConfigureResponseRt, CaseUserActionsResponse, @@ -93,6 +95,14 @@ export const decodeCasesResponse = (respCase?: CasesResponse) => export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); +// TODO: might need to refactor this +export const decodeCaseConfigurationsResponse = (respCase?: CasesConfigurationsResponse) => { + return pipe( + CaseConfigurationsResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); +}; + export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => pipe( CaseConfigureResponseRt.decode(respCase), From 28f8b6244030ca9fea8fd458c59aadc832ab280e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Apr 2021 15:33:18 -0400 Subject: [PATCH 13/25] Mock return of array for configure --- .../integration/cases/connectors.spec.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 996df2a8fe60a..8712d8f2d57e2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -53,16 +53,18 @@ describe('Cases connectors', () => { cy.intercept('GET', '/api/cases/configure', (req) => { req.reply((res) => { const resBody = - res.body.version != null - ? { - ...res.body, - error: null, - mappings: [ - { source: 'title', target: 'short_description', action_type: 'overwrite' }, - { source: 'description', target: 'description', action_type: 'overwrite' }, - { source: 'comments', target: 'comments', action_type: 'append' }, - ], - } + res.body.length > 0 && res.body[0].version != null + ? [ + { + ...res.body[0], + error: null, + mappings: [ + { source: 'title', target: 'short_description', action_type: 'overwrite' }, + { source: 'description', target: 'description', action_type: 'overwrite' }, + { source: 'comments', target: 'comments', action_type: 'append' }, + ], + }, + ] : res.body; res.send(200, resBody); }); From 7e2778c5ced1e8101a88ecce8530f8404c62078b Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Apr 2021 17:34:49 -0400 Subject: [PATCH 14/25] Fixing cypress test --- .../cypress/integration/cases/connectors.spec.ts | 2 ++ .../public/cases/components/configure_cases/__mock__/index.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 8712d8f2d57e2..9e55067ce4ed4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -40,6 +40,8 @@ describe('Cases connectors', () => { { source: 'comments', target: 'comments', action_type: 'append' }, ], version: 'WzEwNCwxXQ==', + id: '123', + owner: 'securitySolution', }; beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index ccc697a2ae84e..63541b43461a9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -46,6 +46,7 @@ export const useCaseConfigureResponse: ReturnUseCaseConfigure = { setCurrentConfiguration: jest.fn(), setMappings: jest.fn(), version: '', + id: '', }; export const useConnectorsResponse: UseConnectorsResponse = { From 221d17c69fc11c4c64034bc32fb666cf422c6240 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 28 Apr 2021 10:07:49 -0400 Subject: [PATCH 15/25] Cleaning up --- .../cases/server/authorization/index.ts | 36 +++--- .../cases/server/client/cases/create.ts | 13 +-- .../cases/server/client/cases/delete.ts | 15 +-- .../cases/server/client/configure/client.ts | 21 +--- x-pack/plugins/cases/server/client/utils.ts | 107 ++++++++++-------- .../common/lib/authentication/roles.ts | 1 - .../case_api_integration/common/lib/utils.ts | 8 +- 7 files changed, 84 insertions(+), 117 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 994f0cf6adb6b..dcf3a70f38d4f 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -41,7 +41,7 @@ const deleteVerbs: Verbs = { past: 'deleted', }; -const eventTypes: Record = { +const EVENT_TYPES: Record = { creation: 'creation', deletion: 'deletion', change: 'change', @@ -68,7 +68,7 @@ export const ECS_OUTCOMES: Record = { export const Operations: Record = { // case operations [WriteOperations.CreateCase]: { - type: eventTypes.creation, + type: EVENT_TYPES.creation, name: WriteOperations.CreateCase, action: 'create-case', verbs: createVerbs, @@ -76,7 +76,7 @@ export const Operations: Record caseService.deleteCase({ diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index d810c2682618e..1e44e615626b7 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -40,10 +40,9 @@ import { getMappings } from './get_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; -import { ECS_OUTCOMES, Operations } from '../../authorization'; +import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter, - createAuditMsg, ensureAuthorized, getAuthorizationFilter, } from '../utils'; @@ -275,15 +274,6 @@ async function update( savedObjectIDs: [configuration.id], }); - // log that we're attempting to update a configuration - auditLogger?.log( - createAuditMsg({ - operation: Operations.updateConfiguration, - outcome: ECS_OUTCOMES.unknown, - savedObjectID: configuration.id, - }) - ); - if (version !== configuration.version) { throw Boom.conflict( 'This configuration has been updated. Please refresh before saving additional updates.' @@ -425,15 +415,6 @@ async function create( savedObjectIDs: [savedObjectID], }); - // log that we're attempting to create a configuration - auditLogger?.log( - createAuditMsg({ - operation: Operations.createConfiguration, - outcome: ECS_OUTCOMES.unknown, - savedObjectID, - }) - ); - const creationDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 38166262fcc5e..1b199909cda60 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -481,6 +481,52 @@ export const sortToSnake = (sortField: string | undefined): SortFieldCase => { } }; +/** + * Creates an AuditEvent describing the state of a request. + */ +function createAuditMsg({ + operation, + outcome, + error, + savedObjectID, +}: { + operation: OperationDetails; + savedObjectID?: string; + outcome?: EcsEventOutcome; + error?: Error; +}): AuditEvent { + const doc = + savedObjectID != null + ? `${operation.savedObjectType} [id=${savedObjectID}]` + : `a ${operation.docType}`; + const message = error + ? `Failed attempt to ${operation.verbs.present} ${doc}` + : outcome === ECS_OUTCOMES.unknown + ? `User is ${operation.verbs.progressive} ${doc}` + : `User has ${operation.verbs.past} ${doc}`; + + return { + message, + event: { + action: operation.action, + category: DATABASE_CATEGORY, + type: [operation.type], + outcome: outcome ?? (error ? ECS_OUTCOMES.failure : ECS_OUTCOMES.success), + }, + ...(savedObjectID != null && { + kibana: { + saved_object: { type: operation.savedObjectType, id: savedObjectID }, + }, + }), + ...(error != null && { + error: { + code: error.name, + message: error.message, + }, + }), + }; +} + /** * Wraps the Authorization class' ensureAuthorized call in a try/catch to handle the audit logging * on a failure. @@ -498,12 +544,19 @@ export async function ensureAuthorized({ authorization: PublicMethodsOf; auditLogger?: AuditLogger; }) { - try { - return await authorization.ensureAuthorized(owners, operation); - } catch (error) { + const logSavedObjects = ({ outcome, error }: { outcome?: EcsEventOutcome; error?: Error }) => { for (const savedObjectID of savedObjectIDs) { - auditLogger?.log(createAuditMsg({ operation, error, savedObjectID })); + auditLogger?.log(createAuditMsg({ operation, outcome, error, savedObjectID })); } + }; + + try { + await authorization.ensureAuthorized(owners, operation); + + // log that we're attempting an operation + logSavedObjects({ outcome: ECS_OUTCOMES.unknown }); + } catch (error) { + logSavedObjects({ error }); throw error; } } @@ -561,49 +614,3 @@ export async function getAuthorizationFilter({ throw error; } } - -/** - * Creates an AuditEvent describing the state of a request. - */ -export function createAuditMsg({ - operation, - outcome, - error, - savedObjectID, -}: { - operation: OperationDetails; - savedObjectID?: string; - outcome?: EcsEventOutcome; - error?: Error; -}): AuditEvent { - const doc = - savedObjectID != null - ? `${operation.savedObjectType} [id=${savedObjectID}]` - : `a ${operation.docType}`; - const message = error - ? `Failed attempt to ${operation.verbs.present} ${doc}` - : outcome === ECS_OUTCOMES.unknown - ? `User is ${operation.verbs.progressive} ${doc}` - : `User has ${operation.verbs.past} ${doc}`; - - return { - message, - event: { - action: operation.action, - category: DATABASE_CATEGORY, - type: [operation.type], - outcome: outcome ?? (error ? ECS_OUTCOMES.failure : ECS_OUTCOMES.success), - }, - ...(savedObjectID != null && { - kibana: { - saved_object: { type: operation.savedObjectType, id: savedObjectID }, - }, - }), - ...(error != null && { - error: { - code: error.name, - message: error.message, - }, - }), - }; -} 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 57fc304e35bf5..c08b68bb2721f 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 @@ -36,7 +36,6 @@ export const globalRead: Role = { { feature: { securitySolutionFixture: ['read'], - // TODO: is this supposed to be all or read here? observabilityFixture: ['read'], }, spaces: ['*'], 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 7a3bd966357c8..43090df495ce9 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -456,9 +456,9 @@ export const deleteAllCaseItems = async (es: KibanaClient) => { deleteCasesByESQuery(es), deleteSubCases(es), deleteCasesUserActions(es), + deleteComments(es), deleteConfiguration(es), deleteMappings(es), - deleteComments(es), ]); }; @@ -611,14 +611,14 @@ export const createComment = async ({ auth?: { user: User; space: string | null }; expectedHttpCode?: number; }): Promise => { - const { body: comment } = await supertest + const { body: theCase } = await supertest .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) - .set('kbn-xsrf', 'true') .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') .send(params) .expect(expectedHttpCode); - return comment; + return theCase; }; export const getAllUserAction = async ( From de78d74ca4df37973b991a524025dae6ed2a3d0d Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Apr 2021 11:07:09 -0400 Subject: [PATCH 16/25] Working case update tests --- .../cases/server/authorization/index.ts | 8 + .../cases/server/authorization/types.ts | 1 + .../plugins/cases/server/client/cases/push.ts | 206 +++--- .../cases/server/client/cases/update.ts | 92 ++- .../feature_privilege_builder/cases.ts | 1 + .../case_api_integration/common/lib/utils.ts | 19 +- .../tests/common/cases/find_cases.ts | 57 +- .../tests/common/cases/patch_cases.ts | 612 +++++++++++++----- .../tests/common/cases/status/get_status.ts | 38 +- .../tests/trial/cases/push_case.ts | 19 +- 10 files changed, 699 insertions(+), 354 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index dcf3a70f38d4f..161bb4e9e8cbf 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -91,6 +91,14 @@ export const Operations: Record; - let updatedComments: SavedObjectsBulkUpdateResponse; + const externalService = { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + connector_id: connector.id, + connector_name: connector.name, + external_id: externalServiceResponse.id, + external_title: externalServiceResponse.title, + external_url: externalServiceResponse.url, + }; - const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); + const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); - try { - [updatedCase, updatedComments] = await Promise.all([ + const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ soClient: savedObjectsClient, caseId, @@ -268,34 +230,34 @@ export const push = async ( ], }), ]); - } catch (e) { - const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; - throw createCaseError({ error: e, message, logger }); - } - /* End of update case with push information */ - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }) - ); + /* End of update case with push information */ + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }) + ); + } catch (error) { + throw createCaseError({ message: 'Failed to push case', error, logger }); + } }; diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 402e6726a71cd..732e99494be87 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -36,7 +36,7 @@ import { CommentAttributes, } from '../../../common/api'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; -import { getCaseToUpdate } from '../utils'; +import { ensureAuthorized, getCaseToUpdate } from '../utils'; import { CaseService } from '../../services'; import { @@ -55,6 +55,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { UpdateAlertRequest } from '../alerts/client'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '..'; +import { Operations } from '../../authorization'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -113,6 +114,18 @@ function throwIfUpdateType(requests: ESCasePatchRequest[]) { } } +/** + * Throws an error if any of the requests attempt to update the owner of a case. + */ +function throwIfUpdateOwner(requests: ESCasePatchRequest[]) { + const requestsUpdatingOwner = requests.filter((req) => req.owner !== undefined); + + if (requestsUpdatingOwner.length > 0) { + const ids = requestsUpdatingOwner.map((req) => req.id); + throw Boom.badRequest(`Updating the owner of a case is not allowed ids: [${ids.join(', ')}]`); + } +} + /** * Throws an error if any of the requests attempt to update an individual style cases' type field to a collection * when alerts are attached to the case. @@ -337,12 +350,58 @@ async function updateAlerts({ await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } +function partitionPatchRequest( + casesMap: Map>, + patchReqCases: CasePatchRequest[] +): { + nonExistingCases: CasePatchRequest[]; + conflictedCases: CasePatchRequest[]; + casesToAuthorize: Array>; +} { + const reqCasesMap = patchReqCases.reduce((acc, req) => { + acc.set(req.id, req); + return acc; + }, new Map()); + + const nonExistingCases: CasePatchRequest[] = []; + const conflictedCases: CasePatchRequest[] = []; + const casesToAuthorize: Array> = []; + + for (const reqCase of reqCasesMap.values()) { + const foundCase = casesMap.get(reqCase.id); + + if (!foundCase || foundCase.error) { + nonExistingCases.push(reqCase); + } else if (foundCase.version !== reqCase.version) { + conflictedCases.push(reqCase); + // let's try to authorize the conflicted case even though we'll fail after afterwards just in case + casesToAuthorize.push(foundCase); + } else { + casesToAuthorize.push(foundCase); + } + } + + return { + nonExistingCases, + conflictedCases, + casesToAuthorize, + }; +} + export const update = async ( cases: CasesPatchRequest, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { - const { savedObjectsClient, caseService, userActionService, user, logger } = clientArgs; + const { + savedObjectsClient, + caseService, + userActionService, + user, + logger, + authorization, + auditLogger, + } = clientArgs; const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) @@ -354,15 +413,22 @@ export const update = async ( caseIds: query.cases.map((q) => q.id), }); - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); + const casesMap = myCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; + const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest( + casesMap, + query.cases + ); + + await ensureAuthorized({ + authorization, + auditLogger, + owners: casesToAuthorize.map((caseInfo) => caseInfo.attributes.owner), + operation: Operations.updateCase, + savedObjectIDs: casesToAuthorize.map((caseInfo) => caseInfo.id), }); if (nonExistingCases.length > 0) { @@ -403,15 +469,11 @@ export const update = async ( throw Boom.notAcceptable('All update fields are identical to current version.'); } - const casesMap = myCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); - if (!ENABLE_CASE_CONNECTOR) { throwIfUpdateType(updateFilterCases); } + throwIfUpdateOwner(updateFilterCases); throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); await throwIfInvalidUpdateOfTypeWithAlerts({ diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 6abe8b1d9d6f4..4b8b870b0202a 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -26,6 +26,7 @@ const writeOperations: string[] = [ 'createCase', 'deleteCase', 'updateCase', + 'pushCase', 'createComment', 'deleteAllComments', 'deleteComment', 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 43090df495ce9..abcced1acaf8c 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -634,13 +634,20 @@ export const getAllUserAction = async ( return userActions; }; -export const updateCase = async ( - supertest: st.SuperTest, - params: CasesPatchRequest, - expectedHttpCode: number = 200 -): Promise => { +export const updateCase = async ({ + supertest, + params, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + params: CasesPatchRequest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: cases } = await supertest - .patch(CASES_URL) + .patch(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(params) .expect(expectedHttpCode); 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 6bcd78f98e5eb..b7838dd9299bc 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 @@ -99,14 +99,17 @@ export default ({ getService }: FtrProviderContext): void => { it('filters by status', async () => { await createCase(supertest, postCaseReq); const toCloseCase = await createCase(supertest, postCaseReq); - const patchedCase = await updateCase(supertest, { - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); @@ -164,24 +167,30 @@ export default ({ getService }: FtrProviderContext): void => { const inProgressCase = await createCase(supertest, postCaseReq); const postedCase = await createCase(supertest, postCaseReq); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - await updateCase(supertest, { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const cases = await findCases({ supertest }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index b50c18192a05b..13d99f5bfba33 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; import { @@ -18,6 +18,7 @@ import { } from '../../../../../../plugins/cases/common/api'; import { defaultUser, + getPostCaseRequest, postCaseReq, postCaseResp, postCollectionReq, @@ -34,6 +35,7 @@ import { getAllUserAction, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, + findCases, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -46,6 +48,16 @@ import { createRule, getQuerySignalIds, } from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -61,14 +73,17 @@ export default ({ getService }: FtrProviderContext): void => { describe('happy path', () => { it('should patch a case', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, }); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -81,14 +96,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should closes the case correctly', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); const userActions = await getAllUserAction(supertest, postedCase.id); @@ -116,14 +134,17 @@ export default ({ getService }: FtrProviderContext): void => { it('should change the status of case to in-progress correctly', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses['in-progress'], - }, - ], + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const userActions = await getAllUserAction(supertest, postedCase.id); @@ -150,19 +171,22 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch a case with new connector', async () => { const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: null, parent: null }, + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, }, - }, - ], + ], + }, }); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -186,23 +210,43 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - await updateCase(supertest, { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, }); }); }); describe('unhappy path', () => { + it('400s when attempting to change the owner of a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + owner: 'observabilityFixture', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + it('404s when case is not there', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: 'not-real', @@ -211,14 +255,14 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 404 - ); + expectedHttpCode: 404, + }); }); it('400s when id is missing', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ // @ts-expect-error { @@ -227,15 +271,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('406s when fields are identical', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -244,14 +288,14 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 406 - ); + expectedHttpCode: 406, + }); }); it('400s when version is missing', async () => { - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ // @ts-expect-error { @@ -260,16 +304,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should 400 and not allow converting a collection back to an individual case', async () => { const postedCase = await createCase(supertest, postCollectionReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -278,15 +322,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('406s when excess data sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -296,15 +340,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 406 - ); + expectedHttpCode: 406, + }); }); it('400s when bad data sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -314,15 +358,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when unsupported status sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -332,15 +376,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when bad connector type sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -350,15 +394,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('400s when bad connector sent', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -374,15 +418,15 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); it('409s when version does not match', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -392,8 +436,8 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 409 - ); + expectedHttpCode: 409, + }); }); it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { @@ -403,9 +447,9 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentAlertReq, }); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: patchedCase.id, @@ -414,16 +458,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -432,16 +476,16 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip("should 400 when attempting to update a collection case's status", async () => { const postedCase = await createCase(supertest, postCollectionReq); - await updateCase( + await updateCase({ supertest, - { + params: { cases: [ { id: postedCase.id, @@ -450,8 +494,8 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - 400 - ); + expectedHttpCode: 400, + }); }); }); @@ -562,12 +606,15 @@ export default ({ getService }: FtrProviderContext): void => { // it updates alert status when syncAlerts is turned on // turn on the sync settings - await updateCase(supertest, { - cases: updatedIndWithStatus.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - })), + await updateCase({ + supertest, + params: { + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -682,14 +729,17 @@ export default ({ getService }: FtrProviderContext): void => { ).to.be(CaseStatuses.open); // turn on the sync settings - await updateCase(supertest, { - cases: [ - { - id: updatedIndWithStatus[0].id, - version: updatedIndWithStatus[0].version, - settings: { syncAlerts: true }, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }, }); await es.indices.refresh({ index: defaultSignalsIndex }); @@ -750,14 +800,17 @@ export default ({ getService }: FtrProviderContext): void => { }); await es.indices.refresh({ index: alert._index }); - await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); // force a refresh on the index that the signal is stored in so that we can search for it and get the correct @@ -804,14 +857,17 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const { body: updatedAlert } = await supertest @@ -855,25 +911,31 @@ export default ({ getService }: FtrProviderContext): void => { }); // Update the status of the case with sync alerts off - const caseStatusUpdated = await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], + const caseStatusUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); // Turn sync alerts on - await updateCase(supertest, { - cases: [ - { - id: caseStatusUpdated[0].id, - version: caseStatusUpdated[0].version, - settings: { syncAlerts: true }, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseStatusUpdated[0].id, + version: caseStatusUpdated[0].version, + settings: { syncAlerts: true }, + }, + ], + }, }); // refresh the index because syncAlerts was set to true so the alert's status should have been updated @@ -916,25 +978,31 @@ export default ({ getService }: FtrProviderContext): void => { }); // Turn sync alerts off - const caseSettingsUpdated = await updateCase(supertest, { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - settings: { syncAlerts: false }, - }, - ], + const caseSettingsUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + settings: { syncAlerts: false }, + }, + ], + }, }); // Update the status of the case with sync alerts off - await updateCase(supertest, { - cases: [ - { - id: caseSettingsUpdated[0].id, - version: caseSettingsUpdated[0].version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseSettingsUpdated[0].id, + version: caseSettingsUpdated[0].version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const { body: updatedAlert } = await supertest @@ -947,5 +1015,223 @@ export default ({ getService }: FtrProviderContext): void => { }); }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { + user: secOnly, + space: 'space1', + }); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); + + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: { user: superUser, space: 'space1' } }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index b71c7105be8f2..b961e5b38c413 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -32,24 +32,30 @@ export default ({ getService }: FtrProviderContext): void => { const inProgressCase = await createCase(supertest, postCaseReq); const postedCase = await createCase(supertest, postCaseReq); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); - await updateCase(supertest, { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, }); const statuses = await getAllCasesStatuses(supertest); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 88f7c15f4a5fe..97d08b1044cc2 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -198,14 +198,17 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path = 409s when case is closed', async () => { const { postedCase, connector } = await createCaseWithConnector(); - await updateCase(supertest, { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, }); await pushCase(supertest, postedCase.id, connector.id, 409); From d048e070db5af3b2a35b0a8c583b5111afafd6a5 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Apr 2021 11:51:09 -0400 Subject: [PATCH 17/25] Addressing PR comments --- x-pack/plugins/cases/server/authorization/utils.ts | 8 +++----- x-pack/plugins/cases/server/client/utils.ts | 2 +- .../cases/server/common/models/commentable_case.ts | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 7483ad3d01298..11d143eb05b2a 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -19,8 +19,8 @@ export const getOwnersFilter = (savedObjectType: string, owners: string[]): Kuer }; export const combineFilterWithAuthorizationFilter = ( - filter: KueryNode | undefined, - authorizationFilter: KueryNode | undefined + filter?: KueryNode, + authorizationFilter?: KueryNode ) => { if (!filter && !authorizationFilter) { return; @@ -49,9 +49,7 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean return true; }; -export const includeFieldsRequiredForAuthentication = ( - fields: string[] | undefined -): string[] | undefined => { +export const includeFieldsRequiredForAuthentication = (fields?: string[]): string[] | undefined => { if (fields === undefined) { return; } diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 1b199909cda60..eb00cce8654ef 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -180,7 +180,7 @@ export function combineFilters(nodes: Array): KueryNode | /** * Creates a KueryNode from a string expression. Returns undefined if the expression is undefined. */ -export function stringToKueryNode(expression: string | undefined): KueryNode | undefined { +export function stringToKueryNode(expression?: string): KueryNode | undefined { if (!expression) { return; } diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 44ba3acf4131f..81b5aca58f797 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -120,7 +120,6 @@ export class CommentableCase { } private get owner(): string { - // TODO: check for subCase?.attributes.owner here return this.collection.attributes.owner; } From f7ae701e251479088b8499fa8ddb29d53471f0a5 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Apr 2021 13:58:31 -0400 Subject: [PATCH 18/25] Reducing operations --- .../cases/server/authorization/index.ts | 24 +++++++++++----- .../cases/server/authorization/types.ts | 21 +++++++++++++- .../feature_privilege_builder/cases.test.ts | 28 ------------------- .../feature_privilege_builder/cases.ts | 4 --- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index dcf3a70f38d4f..be8ca55ccd262 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -48,6 +48,16 @@ const EVENT_TYPES: Record = { access: 'access', }; +/** + * These values need to match the respective values in this file: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + * These are shared between find, get, get all, and delete/delete all + * There currently isn't a use case for a user to delete one comment but not all or differentiating between get, get all, + * and find operations from a privilege stand point. + */ +const DELETE_COMMENT_OPERATION = 'deleteComment'; +const ACCESS_COMMENT_OPERATION = 'getComment'; +const ACCESS_CASE_OPERATION = 'getCase'; + /** * Database constant for ECS category for use for audit logging. */ @@ -117,7 +127,7 @@ export const Operations: Record- e.g get-comment, find-cases + */ action: string; + /** + * The verbs that are associated with this type of operation, these should line up with the event type e.g. creating, created, create etc + */ verbs: Verbs; + /** + * The readable name of the entity being operated on e.g. case, comment, configurations (make it plural if it reads better that way etc) + */ docType: string; + /** + * The actual saved object type of the entity e.g. cases, cases-comments + */ savedObjectType: string; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index 2898cce51a1ce..ef396f75b8575 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -71,10 +71,7 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:observability/getCase", - "cases:1.0.0-zeta1:observability/findCases", "cases:1.0.0-zeta1:observability/getComment", - "cases:1.0.0-zeta1:observability/getAllComments", - "cases:1.0.0-zeta1:observability/findComments", "cases:1.0.0-zeta1:observability/getTags", "cases:1.0.0-zeta1:observability/getReporters", "cases:1.0.0-zeta1:observability/findConfigurations", @@ -112,10 +109,7 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:security/getCase", - "cases:1.0.0-zeta1:security/findCases", "cases:1.0.0-zeta1:security/getComment", - "cases:1.0.0-zeta1:security/getAllComments", - "cases:1.0.0-zeta1:security/findComments", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", "cases:1.0.0-zeta1:security/findConfigurations", @@ -123,7 +117,6 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", "cases:1.0.0-zeta1:security/createComment", - "cases:1.0.0-zeta1:security/deleteAllComments", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", "cases:1.0.0-zeta1:security/createConfiguration", @@ -163,10 +156,7 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:security/getCase", - "cases:1.0.0-zeta1:security/findCases", "cases:1.0.0-zeta1:security/getComment", - "cases:1.0.0-zeta1:security/getAllComments", - "cases:1.0.0-zeta1:security/findComments", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", "cases:1.0.0-zeta1:security/findConfigurations", @@ -174,16 +164,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", "cases:1.0.0-zeta1:security/createComment", - "cases:1.0.0-zeta1:security/deleteAllComments", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", "cases:1.0.0-zeta1:security/createConfiguration", "cases:1.0.0-zeta1:security/updateConfiguration", "cases:1.0.0-zeta1:obs/getCase", - "cases:1.0.0-zeta1:obs/findCases", "cases:1.0.0-zeta1:obs/getComment", - "cases:1.0.0-zeta1:obs/getAllComments", - "cases:1.0.0-zeta1:obs/findComments", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", "cases:1.0.0-zeta1:obs/findConfigurations", @@ -222,10 +208,7 @@ describe(`cases`, () => { expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ "cases:1.0.0-zeta1:security/getCase", - "cases:1.0.0-zeta1:security/findCases", "cases:1.0.0-zeta1:security/getComment", - "cases:1.0.0-zeta1:security/getAllComments", - "cases:1.0.0-zeta1:security/findComments", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", "cases:1.0.0-zeta1:security/findConfigurations", @@ -233,16 +216,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", "cases:1.0.0-zeta1:security/createComment", - "cases:1.0.0-zeta1:security/deleteAllComments", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", "cases:1.0.0-zeta1:security/createConfiguration", "cases:1.0.0-zeta1:security/updateConfiguration", "cases:1.0.0-zeta1:other-security/getCase", - "cases:1.0.0-zeta1:other-security/findCases", "cases:1.0.0-zeta1:other-security/getComment", - "cases:1.0.0-zeta1:other-security/getAllComments", - "cases:1.0.0-zeta1:other-security/findComments", "cases:1.0.0-zeta1:other-security/getTags", "cases:1.0.0-zeta1:other-security/getReporters", "cases:1.0.0-zeta1:other-security/findConfigurations", @@ -250,24 +229,17 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:other-security/deleteCase", "cases:1.0.0-zeta1:other-security/updateCase", "cases:1.0.0-zeta1:other-security/createComment", - "cases:1.0.0-zeta1:other-security/deleteAllComments", "cases:1.0.0-zeta1:other-security/deleteComment", "cases:1.0.0-zeta1:other-security/updateComment", "cases:1.0.0-zeta1:other-security/createConfiguration", "cases:1.0.0-zeta1:other-security/updateConfiguration", "cases:1.0.0-zeta1:obs/getCase", - "cases:1.0.0-zeta1:obs/findCases", "cases:1.0.0-zeta1:obs/getComment", - "cases:1.0.0-zeta1:obs/getAllComments", - "cases:1.0.0-zeta1:obs/findComments", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", "cases:1.0.0-zeta1:obs/findConfigurations", "cases:1.0.0-zeta1:other-obs/getCase", - "cases:1.0.0-zeta1:other-obs/findCases", "cases:1.0.0-zeta1:other-obs/getComment", - "cases:1.0.0-zeta1:other-obs/getAllComments", - "cases:1.0.0-zeta1:other-obs/findComments", "cases:1.0.0-zeta1:other-obs/getTags", "cases:1.0.0-zeta1:other-obs/getReporters", "cases:1.0.0-zeta1:other-obs/findConfigurations", diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index 6abe8b1d9d6f4..2643d7c6d6aaf 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -14,10 +14,7 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; // x-pack/plugins/cases/server/authorization/index.ts const readOperations: string[] = [ 'getCase', - 'findCases', 'getComment', - 'getAllComments', - 'findComments', 'getTags', 'getReporters', 'findConfigurations', @@ -27,7 +24,6 @@ const writeOperations: string[] = [ 'deleteCase', 'updateCase', 'createComment', - 'deleteAllComments', 'deleteComment', 'updateComment', 'createConfiguration', From 5e463580e76183e19e395f34fe4f55542ae42b89 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Apr 2021 15:06:29 -0400 Subject: [PATCH 19/25] Working rbac push case tests --- .../common/lib/authentication/roles.ts | 10 + .../case_api_integration/common/lib/utils.ts | 41 ++- .../tests/basic/cases/push_case.ts | 9 +- .../tests/basic/configure/create_connector.ts | 2 +- .../tests/trial/cases/push_case.ts | 241 ++++++++++++++---- .../tests/trial/configure/get_configure.ts | 9 +- .../tests/trial/configure/get_connectors.ts | 2 +- .../tests/trial/configure/patch_configure.ts | 18 +- .../tests/trial/configure/post_configure.ts | 9 +- 9 files changed, 262 insertions(+), 79 deletions(-) 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 c08b68bb2721f..5ddecd9206106 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 @@ -37,6 +37,8 @@ export const globalRead: Role = { feature: { securitySolutionFixture: ['read'], observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['*'], }, @@ -59,6 +61,8 @@ export const securitySolutionOnlyAll: Role = { { feature: { securitySolutionFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], }, spaces: ['space1'], }, @@ -81,6 +85,8 @@ export const securitySolutionOnlyRead: Role = { { feature: { securitySolutionFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['space1'], }, @@ -103,6 +109,8 @@ export const observabilityOnlyAll: Role = { { feature: { observabilityFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], }, spaces: ['space1'], }, @@ -125,6 +133,8 @@ export const observabilityOnlyRead: Role = { { feature: { observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], }, spaces: ['space1'], }, 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 abcced1acaf8c..e1e3a61435b88 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -803,13 +803,20 @@ export type CreateConnectorResponse = Omit & { connector_type_id: string; }; -export const createConnector = async ( - supertest: st.SuperTest, - req: Record, - expectedHttpCode: number = 200 -): Promise => { +export const createConnector = async ({ + supertest, + req, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + req: Record; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: connector } = await supertest - .post('/api/actions/connector') + .post(`${getSpaceUrlPrefix(auth.space)}/api/actions/connector`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send(req) .expect(expectedHttpCode); @@ -946,14 +953,22 @@ export const getReporters = async ({ return res; }; -export const pushCase = async ( - supertest: st.SuperTest, - caseId: string, - connectorId: string, - expectedHttpCode: number = 200 -): Promise => { +export const pushCase = async ({ + supertest, + caseId, + connectorId, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + connectorId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: res } = await supertest - .post(`${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/connector/${connectorId}/_push`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .send({}) .expect(expectedHttpCode); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts index f964ef3ee8592..5285b57f3be72 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/cases/push_case.ts @@ -36,7 +36,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get 403 when trying to create a connector', async () => { - await createConnector(supertest, getServiceNowConnector(), 403); + await createConnector({ supertest, req: getServiceNowConnector(), expectedHttpCode: 403 }); }); it('should get 404 when trying to push to a case without a valid connector id', async () => { @@ -65,7 +65,12 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await pushCase(supertest, postedCase.id, 'not-exist', 404); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'not-exist', + expectedHttpCode: 404, + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts index a403e6d55be86..fe8e311b5e4f6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/configure/create_connector.ts @@ -14,7 +14,7 @@ export default function serviceNow({ getService }: FtrProviderContext) { describe('create service now action', () => { it('should return 403 when creating a service now action', async () => { - await createConnector(supertest, getServiceNowConnector(), 403); + await createConnector({ supertest, req: getServiceNowConnector(), expectedHttpCode: 403 }); }); }); } diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 97d08b1044cc2..9ede5c4f5ad9d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -8,15 +8,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import * as st from 'supertest'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { postCaseReq, defaultUser, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, + postCaseReq, + defaultUser, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { getConfigurationRequest, getServiceNowConnector, createConnector, @@ -28,6 +31,7 @@ import { updateCase, getAllUserAction, removeServerGeneratedPropertiesFromUserAction, + deleteAllCaseItems, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, @@ -35,11 +39,22 @@ import { } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { CaseConnector, + CasePostRequest, CaseResponse, CaseStatuses, CaseUserActionResponse, ConnectorTypes, } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { User } from '../../../../common/lib/authentication/types'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -58,56 +73,79 @@ export default ({ getService }: FtrProviderContext): void => { }); afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); await actionsRemover.removeAll(); }); - const createCaseWithConnector = async ( - configureReq = {} - ): Promise<{ + const createCaseWithConnector = async ({ + testAgent = supertest, + configureReq = {}, + auth = { user: superUser, space: null }, + createCaseReq = getPostCaseRequest(), + }: { + testAgent?: st.SuperTest; + configureReq?: Record; + auth?: { user: User; space: string | null }; + createCaseReq?: CasePostRequest; + } = {}): Promise<{ postedCase: CaseResponse; connector: CreateConnectorResponse; }> => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest: testAgent, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); - await createConfiguration(supertest, { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }), - ...configureReq, - }); + actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); + await createConfiguration( + testAgent, + { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }, + 200, + auth + ); - const postedCase = await createCase(supertest, { - ...postCaseReq, - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - } as CaseConnector, - }); + const postedCase = await createCase( + testAgent, + { + ...createCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200, + auth + ); return { postedCase, connector }; }; it('should push a case', async () => { const { postedCase, connector } = await createCaseWithConnector(); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); const { pushed_at, external_url, ...rest } = theCase.external_service!; @@ -130,23 +168,37 @@ export default ({ getService }: FtrProviderContext): void => { it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector(); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); expect(theCase.comments![0].pushed_by).to.eql(defaultUser); }); it('should pushes a case and closes when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ - closure_type: 'close-by-pushing', + configureReq: { + closure_type: 'close-by-pushing', + }, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); expect(theCase.status).to.eql('closed'); }); it('should create the correct user action', async () => { const { postedCase, connector } = await createCaseWithConnector(); - const pushedCase = await pushCase(supertest, postedCase.id, connector.id); + const pushedCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); const userActions = await getAllUserAction(supertest, pushedCase.id); const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); @@ -177,15 +229,26 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ - closure_type: 'close-by-pushing', + configureReq: { + closure_type: 'close-by-pushing', + }, }); - const theCase = await pushCase(supertest, postedCase.id, connector.id); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); expect(theCase.status).to.eql(CaseStatuses.open); }); it('unhappy path - 404s when case does not exist', async () => { - await pushCase(supertest, 'fake-id', 'fake-connector', 404); + await pushCase({ + supertest, + caseId: 'fake-id', + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); }); it('unhappy path - 404s when connector does not exist', async () => { @@ -193,7 +256,12 @@ export default ({ getService }: FtrProviderContext): void => { ...postCaseReq, connector: getConfigurationRequest().connector, }); - await pushCase(supertest, postedCase.id, 'fake-connector', 404); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); }); it('unhappy path = 409s when case is closed', async () => { @@ -211,7 +279,80 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - await pushCase(supertest, postedCase.id, connector.id, 409); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + expectedHttpCode: 409, + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space1' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space1' }, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space1' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not push a case in a space that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space2' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts index 6d556423893d5..ff8f1cff884af 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts @@ -45,9 +45,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index 6faea0e1789bb..bc27dd17a21b6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -64,7 +64,7 @@ export default ({ getService }: FtrProviderContext): void => { .send(getResilientConnector()) .expect(200); - const sir = await createConnector(supertest, getServiceNowSIRConnector()); + const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); actionsRemover.add('default', sir.id, 'action', 'actions'); actionsRemover.add('default', snConnector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts index 9e82ce1f0c233..789b68b19beb6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts @@ -47,9 +47,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should patch a configuration connector and create mappings', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); @@ -100,9 +103,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should mappings when updating the connector', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts index 503e0384859ec..96ffcf4bc3f5c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts @@ -46,9 +46,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create a configuration with mapping', async () => { - const connector = await createConnector(supertest, { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); From efc6e8294152852a6a9ee57e35bb483e855f33bb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Apr 2021 16:28:55 -0400 Subject: [PATCH 20/25] Starting stats apis --- x-pack/plugins/cases/common/api/cases/case.ts | 10 +++++----- .../plugins/cases/common/api/cases/comment.ts | 7 ++++--- .../cases/common/api/cases/configure.ts | 11 ++++++---- .../plugins/cases/common/api/cases/index.ts | 5 +++++ .../cases/common/api/cases/user_actions.ts | 3 ++- .../cases/common/api/connectors/mappings.ts | 3 ++- .../cases/server/authorization/index.ts | 9 +++++++++ .../cases/server/authorization/types.ts | 1 + .../cases/server/authorization/utils.ts | 5 +++-- .../cases/server/client/cases/create.ts | 3 ++- .../cases/server/client/cases/delete.ts | 3 ++- .../plugins/cases/server/client/cases/find.ts | 2 +- .../cases/server/client/stats/client.ts | 20 +++++++++++++++++-- x-pack/plugins/cases/server/client/utils.ts | 12 ++++++++--- .../cases/server/services/cases/index.ts | 20 ++++++++++++++----- .../server/services/user_actions/helpers.ts | 3 ++- .../tests/common/cases/status/get_status.ts | 4 ++++ 17 files changed, 91 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 389caffee1a5c..3dfd3d80d63c4 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -13,6 +13,7 @@ import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt, CaseStatusRt } from './status'; import { CaseConnectorRt, ESCaseConnector } from '../connectors'; import { SubCaseResponseRt } from './sub_case'; +import { OWNER_FIELD } from '.'; export enum CaseType { collection = 'collection', @@ -38,8 +39,7 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, - // TODO: should a user be able to update the owner? - owner: rt.string, + [OWNER_FIELD]: rt.string, }); const CaseExternalServiceBasicRt = rt.type({ @@ -80,7 +80,7 @@ const CasePostRequestNoTypeRt = rt.type({ title: rt.string, connector: CaseConnectorRt, settings: SettingsRt, - owner: rt.string, + [OWNER_FIELD]: rt.string, }); /** @@ -115,7 +115,7 @@ export const CasesFindRequestRt = rt.partial({ searchFields: rt.union([rt.array(rt.string), rt.string]), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - owner: rt.union([rt.array(rt.string), rt.string]), + [OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]), }); export const CaseResponseRt = rt.intersection([ @@ -177,7 +177,7 @@ export const ExternalServiceResponseRt = rt.intersection([ ]); export const AllTagsFindRequestRt = rt.partial({ - owner: rt.union([rt.array(rt.string), rt.string]), + [OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]), }); export const AllReportersFindRequestRt = AllTagsFindRequestRt; diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 089bba8615725..b6ebb2bccfe13 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { OWNER_FIELD } from '.'; import { SavedObjectFindOptionsRt } from '../saved_object'; import { UserRT } from '../user'; @@ -28,7 +29,7 @@ export const CommentAttributesBasicRt = rt.type({ ]), created_at: rt.string, created_by: UserRT, - owner: rt.string, + [OWNER_FIELD]: rt.string, pushed_at: rt.union([rt.string, rt.null]), pushed_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), @@ -44,7 +45,7 @@ export enum CommentType { export const ContextTypeUserRt = rt.type({ comment: rt.string, type: rt.literal(CommentType.user), - owner: rt.string, + [OWNER_FIELD]: rt.string, }); /** @@ -60,7 +61,7 @@ export const AlertCommentRequestRt = rt.type({ id: rt.union([rt.string, rt.null]), name: rt.union([rt.string, rt.null]), }), - owner: rt.string, + [OWNER_FIELD]: rt.string, }); const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 02e2cb6596230..e86f012eeaddb 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -10,6 +10,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; import { OmitProp } from '../runtime_types'; +import { OWNER_FIELD } from '.'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); @@ -17,10 +18,12 @@ const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-b const CasesConfigureBasicRt = rt.type({ connector: CaseConnectorRt, closure_type: ClosureTypeRT, - owner: rt.string, + [OWNER_FIELD]: rt.string, }); -const CasesConfigureBasicWithoutOwnerRt = rt.type(OmitProp(CasesConfigureBasicRt.props, 'owner')); +const CasesConfigureBasicWithoutOwnerRt = rt.type( + OmitProp(CasesConfigureBasicRt.props, OWNER_FIELD) +); export const CasesConfigureRequestRt = CasesConfigureBasicRt; export const CasesConfigurePatchRt = rt.intersection([ @@ -45,12 +48,12 @@ export const CaseConfigureResponseRt = rt.intersection([ id: rt.string, version: rt.string, error: rt.union([rt.string, rt.null]), - owner: rt.string, + [OWNER_FIELD]: rt.string, }), ]); export const GetConfigureFindRequestRt = rt.partial({ - owner: rt.union([rt.array(rt.string), rt.string]), + [OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]), }); export const CaseConfigureRequestParamsRt = rt.type({ diff --git a/x-pack/plugins/cases/common/api/cases/index.ts b/x-pack/plugins/cases/common/api/cases/index.ts index 6e7fb818cb2b5..1aad24cb96323 100644 --- a/x-pack/plugins/cases/common/api/cases/index.ts +++ b/x-pack/plugins/cases/common/api/cases/index.ts @@ -11,3 +11,8 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; + +/** + * The field used for authorization in various entities within cases. + */ +export const OWNER_FIELD = 'owner'; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 1b53adb002436..44d03788e7b39 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { OWNER_FIELD } from '.'; import { UserRT } from '../user'; @@ -22,7 +23,7 @@ const UserActionFieldTypeRt = rt.union([ rt.literal('status'), rt.literal('settings'), rt.literal('sub_case'), - rt.literal('owner'), + rt.literal(OWNER_FIELD), ]); const UserActionFieldRt = rt.array(UserActionFieldTypeRt); const UserActionRt = rt.union([ diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index e0fdd2d7e62dc..76c0125d430b6 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { OWNER_FIELD } from '..'; const ActionTypeRT = rt.union([ rt.literal('append'), @@ -31,7 +32,7 @@ export const ConnectorMappingsAttributesRT = rt.type({ export const ConnectorMappingsRt = rt.type({ mappings: rt.array(ConnectorMappingsAttributesRT), - owner: rt.string, + [OWNER_FIELD]: rt.string, }); export type ConnectorMappingsAttributes = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 54611e251d121..a71f71409521d 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -222,4 +222,13 @@ export const Operations: Record Promise; export enum ReadOperations { GetCase = 'getCase', FindCases = 'findCases', + GetCaseStatuses = 'getCaseStatuses', GetComment = 'getComment', GetAllComments = 'getAllComments', FindComments = 'findComments', diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 11d143eb05b2a..3cb47fd3d4627 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -7,11 +7,12 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; +import { OWNER_FIELD } from '../../common/api'; export const getOwnersFilter = (savedObjectType: string, owners: string[]): KueryNode => { return nodeBuilder.or( owners.reduce((query, owner) => { - ensureFieldIsSafeForQuery('owner', owner); + ensureFieldIsSafeForQuery(OWNER_FIELD, owner); query.push(nodeBuilder.is(`${savedObjectType}.attributes.owner`, owner)); return query; }, []) @@ -53,5 +54,5 @@ export const includeFieldsRequiredForAuthentication = (fields?: string[]): strin if (fields === undefined) { return; } - return uniq([...fields, 'owner']); + return uniq([...fields, OWNER_FIELD]); }; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 3f66db7281c38..b4828e6b01006 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -20,6 +20,7 @@ import { CasesClientPostRequestRt, CasePostRequest, CaseType, + OWNER_FIELD, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; @@ -108,7 +109,7 @@ export const create = async ( actionAt: createdDate, actionBy: { username, full_name, email }, caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', 'owner'], + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings', OWNER_FIELD], newValue: JSON.stringify(query), }), ], diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 100135e2992eb..9077783521c0a 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -14,6 +14,7 @@ import { AttachmentService, CaseService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; import { ensureAuthorized } from '../utils'; +import { OWNER_FIELD } from '../../../common/api'; async function deleteSubCases({ attachmentService, @@ -146,7 +147,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 'title', 'connector', 'settings', - 'owner', + OWNER_FIELD, 'comment', ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), ], diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 53ae6a2e76b81..0899cd3d0150f 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -80,7 +80,6 @@ export const find = async ( ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]); - // TODO: Make sure we do not leak information when authorization is on const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter }); @@ -88,6 +87,7 @@ export const find = async ( soClient: savedObjectsClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, + ensureSavedObjectsAreAuthorized, }); }), ]); diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index 40ced0bfbf4bb..8c18c35e8f4fd 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -7,8 +7,9 @@ import { CasesClientArgs } from '..'; import { CasesStatusResponse, CasesStatusResponseRt, caseStatuses } from '../../../common/api'; +import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -import { constructQueryOptions } from '../utils'; +import { constructQueryOptions, getAuthorizationFilter } from '../utils'; /** * Statistics API contract. @@ -30,19 +31,34 @@ async function getStatusTotalsByType({ savedObjectsClient: soClient, caseService, logger, + authorization, + auditLogger, }: CasesClientArgs): Promise { try { + const { + filter: authorizationFilter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + operation: Operations.getCaseStatuses, + auditLogger, + }); + const [openCases, inProgressCases, closedCases] = await Promise.all([ ...caseStatuses.map((status) => { - const statusQuery = constructQueryOptions({ status }); + const statusQuery = constructQueryOptions({ status, authorizationFilter }); return caseService.findCaseStatusStats({ soClient, caseOptions: statusQuery.case, subCaseOptions: statusQuery.subCase, + ensureSavedObjectsAreAuthorized, }); }), ]); + logSuccessfulAuthorization(); + return CasesStatusResponseRt.encode({ count_open_cases: openCases, count_in_progress_cases: inProgressCases, diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index eb00cce8654ef..931372cc1d6c9 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -27,6 +27,7 @@ import { excess, ContextTypeUserRt, AlertCommentRequestRt, + OWNER_FIELD, } from '../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; import { AuditEvent } from '../../../security/server'; @@ -157,7 +158,7 @@ export const combineAuthorizedAndOwnerFilter = ( ): KueryNode | undefined => { const ownerFilter = buildFilter({ filters: owner, - field: 'owner', + field: OWNER_FIELD, operator: 'or', type: savedObjectType, }); @@ -241,7 +242,7 @@ export const constructQueryOptions = ({ operator: 'or', }); const sortField = sortToSnake(sortByField); - const ownerFilter = buildFilter({ filters: owner ?? [], field: 'owner', operator: 'or' }); + const ownerFilter = buildFilter({ filters: owner ?? [], field: OWNER_FIELD, operator: 'or' }); switch (caseType) { case CaseType.individual: { @@ -570,9 +571,14 @@ interface OwnerEntity { id: string; } +/** + * Function callback for making sure the found saved objects are of the authorized owner + */ +export type EnsureSOAuthCallback = (entities: OwnerEntity[]) => void; + interface AuthFilterHelpers { filter?: KueryNode; - ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; logSuccessfulAuthorization: () => void; } diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 870ba94b1ba13..3463eb82d2680 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -32,6 +32,7 @@ import { caseTypeField, CasesFindRequest, CaseStatuses, + OWNER_FIELD, } from '../../../common/api'; import { defaultSortField, @@ -48,6 +49,8 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../../common/constants'; import { ClientArgs } from '..'; +import { EnsureSOAuthCallback } from '../../client/utils'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; interface PushedArgs { pushed_at: string; @@ -298,9 +301,11 @@ export class CaseService { soClient, caseOptions, subCaseOptions, + ensureSavedObjectsAreAuthorized, }: { soClient: SavedObjectsClientContract; caseOptions: SavedObjectFindOptionsKueryNode; + ensureSavedObjectsAreAuthorized: EnsureSOAuthCallback; subCaseOptions?: SavedObjectFindOptionsKueryNode; }): Promise { const casesStats = await this.findCases({ @@ -337,12 +342,17 @@ export class CaseService { soClient, options: { ...caseOptions, - fields: [caseTypeField], + fields: includeFieldsRequiredForAuthentication([caseTypeField]), page: 1, perPage: casesStats.total, }, }); + // make sure that the retrieved cases were correctly filtered by owner + ensureSavedObjectsAreAuthorized( + cases.saved_objects.map((caseInfo) => ({ id: caseInfo.id, owner: caseInfo.attributes.owner })) + ); + const caseIds = cases.saved_objects .filter((caseInfo) => caseInfo.attributes.type === CaseType.collection) .map((caseInfo) => caseInfo.id); @@ -922,7 +932,7 @@ export class CaseService { this.log.debug(`Attempting to GET all reporters`); const firstReporters = await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['created_by', 'owner'], + fields: ['created_by', OWNER_FIELD], page: 1, perPage: 1, filter: cloneDeep(filter), @@ -930,7 +940,7 @@ export class CaseService { return await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['created_by', 'owner'], + fields: ['created_by', OWNER_FIELD], page: 1, perPage: firstReporters.total, filter: cloneDeep(filter), @@ -949,7 +959,7 @@ export class CaseService { this.log.debug(`Attempting to GET all cases`); const firstTags = await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['tags', 'owner'], + fields: ['tags', OWNER_FIELD], page: 1, perPage: 1, filter: cloneDeep(filter), @@ -957,7 +967,7 @@ export class CaseService { return await soClient.find({ type: CASE_SAVED_OBJECT, - fields: ['tags', 'owner'], + fields: ['tags', OWNER_FIELD], page: 1, perPage: firstTags.total, filter: cloneDeep(filter), diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index 2ab3bdb5e1cee..c05358ef8d0dd 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -17,6 +17,7 @@ import { User, UserActionFieldType, SubCaseAttributes, + OWNER_FIELD, } from '../../../common/api'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; @@ -157,7 +158,7 @@ const userActionFieldsAllowed: UserActionField = [ 'status', 'settings', 'sub_case', - 'owner', + OWNER_FIELD, ]; interface CaseSubIDs { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index b961e5b38c413..17d552a907127 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -66,5 +66,9 @@ export default ({ getService }: FtrProviderContext): void => { count_in_progress_cases: 1, }); }); + + describe('rbac', () => { + // TODO: + }); }); }; From 8543b5b5146d80b014215727f95071f2b47015ca Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 30 Apr 2021 12:31:04 -0400 Subject: [PATCH 21/25] Working status tests --- x-pack/plugins/cases/common/api/cases/case.ts | 2 +- .../plugins/cases/common/api/cases/comment.ts | 2 +- .../cases/common/api/cases/configure.ts | 2 +- .../cases/common/api/cases/constants.ts | 11 ++ .../plugins/cases/common/api/cases/index.ts | 6 +- .../cases/common/api/cases/user_actions.ts | 2 +- x-pack/plugins/cases/server/client/utils.ts | 6 - .../case_api_integration/common/lib/utils.ts | 16 +- .../tests/common/cases/status/get_status.ts | 141 +++++++++++++++--- 9 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/cases/common/api/cases/constants.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 3dfd3d80d63c4..cad9750ff8fde 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -13,7 +13,7 @@ import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt, CaseStatusRt } from './status'; import { CaseConnectorRt, ESCaseConnector } from '../connectors'; import { SubCaseResponseRt } from './sub_case'; -import { OWNER_FIELD } from '.'; +import { OWNER_FIELD } from './constants'; export enum CaseType { collection = 'collection', diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index b6ebb2bccfe13..461881d3d1b8a 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { OWNER_FIELD } from '.'; +import { OWNER_FIELD } from './constants'; import { SavedObjectFindOptionsRt } from '../saved_object'; import { UserRT } from '../user'; diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index e86f012eeaddb..5e56b2f84533c 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -10,7 +10,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; import { OmitProp } from '../runtime_types'; -import { OWNER_FIELD } from '.'; +import { OWNER_FIELD } from './constants'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); diff --git a/x-pack/plugins/cases/common/api/cases/constants.ts b/x-pack/plugins/cases/common/api/cases/constants.ts new file mode 100644 index 0000000000000..b8dd13c5d490e --- /dev/null +++ b/x-pack/plugins/cases/common/api/cases/constants.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * The field used for authorization in various entities within cases. + */ +export const OWNER_FIELD = 'owner'; diff --git a/x-pack/plugins/cases/common/api/cases/index.ts b/x-pack/plugins/cases/common/api/cases/index.ts index 1aad24cb96323..0f78ca9b35377 100644 --- a/x-pack/plugins/cases/common/api/cases/index.ts +++ b/x-pack/plugins/cases/common/api/cases/index.ts @@ -11,8 +11,4 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; - -/** - * The field used for authorization in various entities within cases. - */ -export const OWNER_FIELD = 'owner'; +export * from './constants'; diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 44d03788e7b39..b974327e02f04 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { OWNER_FIELD } from '.'; +import { OWNER_FIELD } from './constants'; import { UserRT } from '../user'; diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 891f1d9805849..931372cc1d6c9 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -571,12 +571,6 @@ interface OwnerEntity { id: string; } -interface AuthFilterHelpers { - filter?: KueryNode; - ensureSavedObjectsAreAuthorized: (entities: OwnerEntity[]) => void; - logSuccessfulAuthorization: () => void; -} - /** * Function callback for making sure the found saved objects are of the authorized owner */ 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 e1e3a61435b88..25ae7bf1e87cd 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -853,12 +853,18 @@ export const updateConfiguration = async ( return configuration; }; -export const getAllCasesStatuses = async ( - supertest: st.SuperTest, - expectedHttpCode: number = 200 -): Promise => { +export const getAllCasesStatuses = async ({ + supertest, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: statuses } = await supertest - .get(CASE_STATUS_URL) + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_STATUS_URL}`) + .auth(auth.user.username, auth.user.password) .set('kbn-xsrf', 'true') .expect(expectedHttpCode); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index 17d552a907127..c8780939981c1 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -6,16 +6,25 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; -import { postCaseReq } from '../../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq } from '../../../../../common/lib/mock'; import { - deleteCasesByESQuery, createCase, updateCase, getAllCasesStatuses, + deleteAllCaseItems, } from '../../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -24,26 +33,15 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_status', () => { afterEach(async () => { - await deleteCasesByESQuery(es); + await deleteAllCaseItems(es); }); it('should return case statuses', async () => { - await createCase(supertest, postCaseReq); - const inProgressCase = await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, postCaseReq); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + ]); await updateCase({ supertest, @@ -54,11 +52,16 @@ export default ({ getService }: FtrProviderContext): void => { version: inProgressCase.version, status: CaseStatuses['in-progress'], }, + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, ], }, }); - const statuses = await getAllCasesStatuses(supertest); + const statuses = await getAllCasesStatuses({ supertest }); expect(statuses).to.eql({ count_open_cases: 1, @@ -68,7 +71,101 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('rbac', () => { - // TODO: + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct status stats', async () => { + /** + * Owner: Sec + * open: 0, in-prog: 1, closed: 1 + * Owner: Obs + * open: 1, in-prog: 1 + */ + const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ), + ]); + + 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: { user: superUser, space: 'space1' }, + }); + + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + ]) { + const statuses = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: 'space1' }, + }); + + expect(statuses).to.eql({ + count_open_cases: scenario.stats.open, + count_closed_cases: scenario.stats.closed, + count_in_progress_cases: scenario.stats.inProgress, + }); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should return a 403 when retrieving the statuses when the user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } }); }); }; From 8841e659b946f5ed91fc1efde5728e0d2c194f66 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 30 Apr 2021 16:18:17 -0400 Subject: [PATCH 22/25] User action tests and fixing migration errors --- .../cases/common/api/cases/sub_case.ts | 2 + .../cases/common/api/cases/user_actions.ts | 1 + .../cases/common/api/connectors/mappings.ts | 2 +- .../cases/server/authorization/index.ts | 10 +++ .../cases/server/authorization/types.ts | 1 + .../cases/server/client/attachments/add.ts | 3 + .../cases/server/client/attachments/delete.ts | 2 + .../cases/server/client/attachments/update.ts | 1 + .../cases/server/client/cases/create.ts | 1 + .../cases/server/client/cases/delete.ts | 5 +- .../plugins/cases/server/client/cases/push.ts | 2 + .../cases/server/client/sub_cases/client.ts | 7 +- .../cases/server/client/user_actions/get.ts | 12 ++- .../cases/server/services/cases/index.ts | 6 +- .../server/services/user_actions/helpers.ts | 21 ++++- .../feature_privilege_builder/cases.test.ts | 12 +++ .../feature_privilege_builder/cases.ts | 1 + .../case_api_integration/common/lib/utils.ts | 21 ++++- .../security_and_spaces/tests/basic/index.ts | 8 +- .../tests/common/cases/delete_cases.ts | 1 + .../tests/common/cases/patch_cases.ts | 2 + .../tests/common/cases/post_case.ts | 1 + .../tests/common/comments/post_comment.ts | 1 + .../security_and_spaces/tests/common/index.ts | 6 +- .../tests/common/migrations.ts | 18 ++++ .../user_actions/get_all_user_actions.ts | 89 ++++++++++++++++--- .../tests/trial/cases/push_case.ts | 1 + .../security_and_spaces/tests/trial/index.ts | 10 ++- 28 files changed, 218 insertions(+), 29 deletions(-) create mode 100644 x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index ba6cd6a8affa4..b61850851ec89 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -10,6 +10,7 @@ import * as rt from 'io-ts'; import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; +import { OWNER_FIELD } from './constants'; import { CasesStatusResponseRt } from './status'; import { CaseStatusRt } from './status'; @@ -26,6 +27,7 @@ export const SubCaseAttributesRt = rt.intersection([ created_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), updated_by: rt.union([UserRT, rt.null]), + [OWNER_FIELD]: rt.string, }), ]); diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index b974327e02f04..1473617b2f5b2 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -42,6 +42,7 @@ const CaseUserActionBasicRT = rt.type({ action_by: UserRT, new_value: rt.union([rt.string, rt.null]), old_value: rt.union([rt.string, rt.null]), + [OWNER_FIELD]: rt.string, }); const CaseUserActionResponseRT = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index 76c0125d430b6..b23629b82767f 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { OWNER_FIELD } from '..'; +import { OWNER_FIELD } from '../cases/constants'; const ActionTypeRT = rt.union([ rt.literal('append'), diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index a71f71409521d..3a6ec502ff72b 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -10,6 +10,7 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, } from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; @@ -231,4 +232,13 @@ export const Operations: Record + actions: cases.saved_objects.map((caseInfo) => buildCaseUserActionItem({ action: 'delete', actionAt: deleteDate, actionBy: user, - caseId: id, + caseId: caseInfo.id, fields: [ 'description', 'status', @@ -151,6 +151,7 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P 'comment', ...(ENABLE_CASE_CONNECTOR ? ['sub_case'] : []), ], + owner: caseInfo.attributes.owner, }) ), }); diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 0955b7e5d1757..abeba5f882fb1 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -216,6 +216,7 @@ export const push = async ( fields: ['status'], newValue: CaseStatuses.closed, oldValue: myCase.attributes.status, + owner: myCase.attributes.owner, }), ] : []), @@ -226,6 +227,7 @@ export const push = async ( caseId, fields: ['pushed'], newValue: JSON.stringify(externalService), + owner: myCase.attributes.owner, }), ], }), diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index ac390710def87..102cbee14a206 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -105,16 +105,17 @@ async function deleteSubCase(ids: string[], clientArgs: CasesClientArgs): Promis await userActionService.bulkCreate({ soClient, - actions: ids.map((id) => + actions: subCases.saved_objects.map((subCase) => buildCaseUserActionItem({ action: 'delete', actionAt: deleteDate, actionBy: user, // if for some reason the sub case didn't have a reference to its parent, we'll still log a user action // but we won't have the case ID - caseId: subCaseIDToParentID.get(id) ?? '', - subCaseId: id, + caseId: subCaseIDToParentID.get(subCase.id) ?? '', + subCaseId: subCase.id, fields: ['sub_case', 'comment', 'status'], + owner: subCase.attributes.owner, }) ), }); diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index dac997c3fa90a..0b03fb75614a8 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -14,6 +14,8 @@ import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../com import { createCaseError } from '../../common/error'; import { checkEnabledCaseConnectorOrThrow } from '../../common'; import { CasesClientArgs } from '..'; +import { ensureAuthorized } from '../utils'; +import { Operations } from '../../authorization'; interface GetParams { caseId: string; @@ -24,7 +26,7 @@ export const get = async ( { caseId, subCaseId }: GetParams, clientArgs: CasesClientArgs ): Promise => { - const { savedObjectsClient, userActionService, logger } = clientArgs; + const { savedObjectsClient, userActionService, logger, authorization, auditLogger } = clientArgs; try { checkEnabledCaseConnectorOrThrow(subCaseId); @@ -35,6 +37,14 @@ export const get = async ( subCaseId, }); + await ensureAuthorized({ + authorization, + auditLogger, + owners: userActions.saved_objects.map((userAction) => userAction.attributes.owner), + savedObjectIDs: userActions.saved_objects.map((userAction) => userAction.id), + operation: Operations.getUserActions, + }); + return CaseUserActionsResponseRt.encode( userActions.saved_objects.reduce((acc, ua) => { if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 3463eb82d2680..246872b0af9d4 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -186,9 +186,11 @@ interface GetReportersArgs { const transformNewSubCase = ({ createdAt, createdBy, + owner, }: { createdAt: string; createdBy: User; + owner: string; }): SubCaseAttributes => { return { closed_at: null, @@ -198,6 +200,7 @@ const transformNewSubCase = ({ status: CaseStatuses.open, updated_at: null, updated_by: null, + owner, }; }; @@ -585,7 +588,8 @@ export class CaseService { this.log.debug(`Attempting to POST a new sub case`); return soClient.create( SUB_CASE_SAVED_OBJECT, - transformNewSubCase({ createdAt, createdBy }), + // ENABLE_CASE_CONNECTOR: populate the owner field correctly + transformNewSubCase({ createdAt, createdBy, owner: '' }), { references: [ { diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index c05358ef8d0dd..664a9041491a1 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -35,6 +35,7 @@ export const transformNewUserAction = ({ email, // eslint-disable-next-line @typescript-eslint/naming-convention full_name, + owner, newValue = null, oldValue = null, username, @@ -42,6 +43,7 @@ export const transformNewUserAction = ({ actionField: UserActionField; action: UserAction; actionAt: string; + owner: string; email?: string | null; full_name?: string | null; newValue?: string | null; @@ -54,6 +56,7 @@ export const transformNewUserAction = ({ action_by: { email, full_name, username }, new_value: newValue, old_value: oldValue, + owner, }); interface BuildCaseUserAction { @@ -61,6 +64,7 @@ interface BuildCaseUserAction { actionAt: string; actionBy: User; caseId: string; + owner: string; fields: UserActionField | unknown[]; newValue?: string | unknown; oldValue?: string | unknown; @@ -81,11 +85,13 @@ export const buildCommentUserActionItem = ({ newValue, oldValue, subCaseId, + owner, }: BuildCommentUserActionItem): UserActionItem => ({ attributes: transformNewUserAction({ actionField: fields as UserActionField, action, actionAt, + owner, ...actionBy, newValue: newValue as string, oldValue: oldValue as string, @@ -122,11 +128,13 @@ export const buildCaseUserActionItem = ({ newValue, oldValue, subCaseId, + owner, }: BuildCaseUserAction): UserActionItem => ({ attributes: transformNewUserAction({ actionField: fields as UserActionField, action, actionAt, + owner, ...actionBy, newValue: newValue as string, oldValue: oldValue as string, @@ -181,7 +189,14 @@ interface Getters { getCaseAndSubID: GetCaseAndSubID; } -const buildGenericCaseUserActions = ({ +interface OwnerEntity { + owner: string; +} + +/** + * The entity associated with the user action must contain an owner field + */ +const buildGenericCaseUserActions = ({ actionDate, actionBy, originalCases, @@ -222,6 +237,7 @@ const buildGenericCaseUserActions = ({ fields: [field], newValue: updatedValue, oldValue: origValue, + owner: originalItem.attributes.owner, }), ]; } else if (Array.isArray(origValue) && Array.isArray(updatedValue)) { @@ -237,6 +253,7 @@ const buildGenericCaseUserActions = ({ subCaseId, fields: [field], newValue: compareValues.addedItems.join(', '), + owner: originalItem.attributes.owner, }), ]; } @@ -252,6 +269,7 @@ const buildGenericCaseUserActions = ({ subCaseId, fields: [field], newValue: compareValues.deletedItems.join(', '), + owner: originalItem.attributes.owner, }), ]; } @@ -271,6 +289,7 @@ const buildGenericCaseUserActions = ({ fields: [field], newValue: JSON.stringify(updatedValue), oldValue: JSON.stringify(origValue), + owner: originalItem.attributes.owner, }), ]; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index ef396f75b8575..b7550a0717a28 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts @@ -74,6 +74,7 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:observability/getComment", "cases:1.0.0-zeta1:observability/getTags", "cases:1.0.0-zeta1:observability/getReporters", + "cases:1.0.0-zeta1:observability/getUserActions", "cases:1.0.0-zeta1:observability/findConfigurations", ] `); @@ -112,10 +113,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -159,10 +162,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -172,6 +177,7 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/getUserActions", "cases:1.0.0-zeta1:obs/findConfigurations", ] `); @@ -211,10 +217,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:security/getComment", "cases:1.0.0-zeta1:security/getTags", "cases:1.0.0-zeta1:security/getReporters", + "cases:1.0.0-zeta1:security/getUserActions", "cases:1.0.0-zeta1:security/findConfigurations", "cases:1.0.0-zeta1:security/createCase", "cases:1.0.0-zeta1:security/deleteCase", "cases:1.0.0-zeta1:security/updateCase", + "cases:1.0.0-zeta1:security/pushCase", "cases:1.0.0-zeta1:security/createComment", "cases:1.0.0-zeta1:security/deleteComment", "cases:1.0.0-zeta1:security/updateComment", @@ -224,10 +232,12 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:other-security/getComment", "cases:1.0.0-zeta1:other-security/getTags", "cases:1.0.0-zeta1:other-security/getReporters", + "cases:1.0.0-zeta1:other-security/getUserActions", "cases:1.0.0-zeta1:other-security/findConfigurations", "cases:1.0.0-zeta1:other-security/createCase", "cases:1.0.0-zeta1:other-security/deleteCase", "cases:1.0.0-zeta1:other-security/updateCase", + "cases:1.0.0-zeta1:other-security/pushCase", "cases:1.0.0-zeta1:other-security/createComment", "cases:1.0.0-zeta1:other-security/deleteComment", "cases:1.0.0-zeta1:other-security/updateComment", @@ -237,11 +247,13 @@ describe(`cases`, () => { "cases:1.0.0-zeta1:obs/getComment", "cases:1.0.0-zeta1:obs/getTags", "cases:1.0.0-zeta1:obs/getReporters", + "cases:1.0.0-zeta1:obs/getUserActions", "cases:1.0.0-zeta1:obs/findConfigurations", "cases:1.0.0-zeta1:other-obs/getCase", "cases:1.0.0-zeta1:other-obs/getComment", "cases:1.0.0-zeta1:other-obs/getTags", "cases:1.0.0-zeta1:other-obs/getReporters", + "cases:1.0.0-zeta1:other-obs/getUserActions", "cases:1.0.0-zeta1:other-obs/findConfigurations", ] `); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts index b8e5f8ed208a4..4b04f98704c8f 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts @@ -17,6 +17,7 @@ const readOperations: string[] = [ 'getComment', 'getTags', 'getReporters', + 'getUserActions', 'findConfigurations', ]; const writeOperations: string[] = [ 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 25ae7bf1e87cd..731ddca08a34e 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -43,9 +43,10 @@ import { CasesConfigurePatch, CasesStatusResponse, CasesConfigurationsResponse, + CaseUserActionsResponse, } from '../../../../plugins/cases/common/api'; import { postCollectionReq, postCommentGenAlertReq } from './mock'; -import { getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; +import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types'; @@ -655,6 +656,24 @@ export const updateCase = async ({ return cases; }; +export const getCaseUserActions = async ({ + supertest, + caseID, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseID: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { + const { body: userActions } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionUrl(caseID)}`) + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + return userActions; +}; + export const deleteComment = async ({ supertest, caseId, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 502c64ccce04a..90fbb10637434 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -21,10 +21,14 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { after(async () => { await deleteSpacesAndUsers(getService); }); - // Common - loadTestFile(require.resolve('../common')); // Basic loadTestFile(require.resolve('./cases/push_case')); + + // Common + loadTestFile(require.resolve('../common')); + + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; 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 484dca314c9cc..5476182a9c2c7 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 @@ -108,6 +108,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 13d99f5bfba33..26f109168faa1 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -129,6 +129,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); @@ -166,6 +167,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: '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 f2b9027cfb1f1..91fb03604b3c4 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 @@ -128,6 +128,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); expect(parsedNewValue).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 0e501648c512b..83ca0395be9c5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -151,6 +151,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: `${patchedCase.comments![0].id}`, sub_case_id: '', + owner: 'securitySolutionFixture', }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts index c6c68efd7a752..ff2d1b5f37aae 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/index.ts @@ -35,9 +35,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./sub_cases/get_sub_case')); loadTestFile(require.resolve('./sub_cases/find_sub_cases')); - // Migrations - loadTestFile(require.resolve('./cases/migrations')); - loadTestFile(require.resolve('./configure/migrations')); - loadTestFile(require.resolve('./user_actions/migrations')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces + // which causes errors in any tests after them that relies on those }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts new file mode 100644 index 0000000000000..17d93e76bbdda --- /dev/null +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/migrations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common migrations', function () { + // Migrations + loadTestFile(require.resolve('./cases/migrations')); + loadTestFile(require.resolve('./configure/migrations')); + loadTestFile(require.resolve('./user_actions/migrations')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 19911890929d2..e12df95aa5b15 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -9,14 +9,32 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { userActionPostResp, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, + CaseResponse, + CaseStatuses, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + userActionPostResp, + postCaseReq, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, } from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -25,10 +43,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_all_user_actions', () => { afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); }); it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { @@ -321,5 +336,59 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolutionFixture', }); }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: { user: superUser, space: 'space1' }, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should 403 when requesting the user actions of a case with user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 9ede5c4f5ad9d..728be6929c883 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -213,6 +213,7 @@ export default ({ getService }: FtrProviderContext): void => { case_id: `${postedCase.id}`, comment_id: null, sub_case_id: '', + owner: 'securitySolutionFixture', }); expect(parsedNewValue).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index 5ba09dd56bd67..26bc6a072450d 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -22,11 +22,15 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpacesAndUsers(getService); }); - // Common - loadTestFile(require.resolve('../common')); - // Trial loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/index')); + + // Common + loadTestFile(require.resolve('../common')); + + // NOTE: These need to be at the end because they could delete the .kibana index and inadvertently remove the users and spaces + loadTestFile(require.resolve('../common/migrations')); }); }; From ba7df510be7e82ef54e3916d7e527724639d7f27 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 3 May 2021 09:36:59 -0400 Subject: [PATCH 23/25] Fixing type errors --- x-pack/plugins/cases/server/client/cases/mock.ts | 6 ++++++ x-pack/plugins/cases/server/client/cases/utils.test.ts | 1 + .../server/routes/api/__fixtures__/mock_saved_objects.ts | 2 ++ .../security_solution/public/cases/containers/mock.ts | 2 ++ 4 files changed, 11 insertions(+) diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 1d46f5715c4ba..01740c9a41a93 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -135,6 +135,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['pushed'], @@ -151,6 +152,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0a801750-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['comment'], @@ -166,6 +168,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-1', + owner: 'securitySolution', }, { action_field: ['comment'], @@ -181,6 +184,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-alert-2', + owner: 'securitySolution', }, { action_field: ['pushed'], @@ -197,6 +201,7 @@ export const userActions: CaseUserActionsResponse = [ action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, { action_field: ['comment'], @@ -212,5 +217,6 @@ export const userActions: CaseUserActionsResponse = [ action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: 'comment-user-1', + owner: 'securitySolution', }, ]; diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 5f41a95d3c501..391fe5803f81f 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -701,6 +701,7 @@ describe('utils', () => { action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', comment_id: null, + owner: 'securitySolution', }, ]); 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 e5b826cf0ddef..f221942716d08 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 @@ -513,6 +513,7 @@ export const mockUserActions: Array> = [ new_value: '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', old_value: null, + owner: 'securitySolution', }, version: 'WzYsMV0=', references: [], @@ -532,6 +533,7 @@ export const mockUserActions: Array> = [ new_value: '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', old_value: null, + owner: 'securitySolution', }, version: 'WzYsMV0=', references: [], 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 6880a105b1ce6..8e29e3760c8d8 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -185,6 +185,7 @@ const basicAction = { newValue: 'what a cool value', caseId: basicCaseId, commentId: null, + owner: 'securitySolution', }; export const cases: Case[] = [ @@ -317,6 +318,7 @@ const basicActionSnake = { new_value: 'what a cool value', case_id: basicCaseId, comment_id: null, + owner: 'securitySolution', }; export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ ...basicActionSnake, From fbf57849c39725333456e4f9edc82f9190600e0e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 3 May 2021 11:24:17 -0400 Subject: [PATCH 24/25] including error in message --- x-pack/plugins/cases/server/client/cases/push.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index abeba5f882fb1..0427da1824dfa 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -260,6 +260,6 @@ export const push = async ( }) ); } catch (error) { - throw createCaseError({ message: 'Failed to push case', error, logger }); + throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } }; From 9fae3d9f4c45057f6367bc0fe3bd80457bb184a3 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 3 May 2021 16:17:09 -0400 Subject: [PATCH 25/25] Addressing pr feedback --- x-pack/plugins/cases/common/api/cases/case.ts | 9 ++++----- x-pack/plugins/cases/common/api/cases/comment.ts | 7 +++---- x-pack/plugins/cases/common/api/cases/configure.ts | 6 +++--- x-pack/plugins/cases/common/api/cases/sub_case.ts | 3 +-- .../plugins/cases/common/api/cases/user_actions.ts | 2 +- .../plugins/cases/common/api/connectors/mappings.ts | 3 +-- x-pack/plugins/cases/server/authorization/utils.ts | 2 +- x-pack/plugins/cases/server/client/cases/push.ts | 2 +- x-pack/plugins/cases/server/client/cases/update.ts | 7 +------ .../common/lib/authentication/index.ts | 4 +++- .../tests/common/cases/delete_cases.ts | 6 ++++-- .../tests/common/cases/patch_cases.ts | 3 ++- .../tests/common/cases/status/get_status.ts | 7 ++++--- .../tests/common/comments/delete_comment.ts | 9 +++++---- .../tests/common/comments/find_comments.ts | 9 +++++---- .../tests/common/comments/get_all_comments.ts | 11 ++++++----- .../tests/common/comments/get_comment.ts | 9 +++++---- .../tests/common/comments/patch_comment.ts | 13 +++++++------ .../tests/common/comments/post_comment.ts | 5 +++-- .../common/user_actions/get_all_user_actions.ts | 3 ++- .../tests/trial/cases/push_case.ts | 7 ++++--- 21 files changed, 66 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index cad9750ff8fde..9b184d437f281 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -13,7 +13,6 @@ import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt, CaseStatusRt } from './status'; import { CaseConnectorRt, ESCaseConnector } from '../connectors'; import { SubCaseResponseRt } from './sub_case'; -import { OWNER_FIELD } from './constants'; export enum CaseType { collection = 'collection', @@ -39,7 +38,7 @@ const CaseBasicRt = rt.type({ [caseTypeField]: CaseTypeRt, connector: CaseConnectorRt, settings: SettingsRt, - [OWNER_FIELD]: rt.string, + owner: rt.string, }); const CaseExternalServiceBasicRt = rt.type({ @@ -80,7 +79,7 @@ const CasePostRequestNoTypeRt = rt.type({ title: rt.string, connector: CaseConnectorRt, settings: SettingsRt, - [OWNER_FIELD]: rt.string, + owner: rt.string, }); /** @@ -115,7 +114,7 @@ export const CasesFindRequestRt = rt.partial({ searchFields: rt.union([rt.array(rt.string), rt.string]), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - [OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]), + owner: rt.union([rt.array(rt.string), rt.string]), }); export const CaseResponseRt = rt.intersection([ @@ -177,7 +176,7 @@ export const ExternalServiceResponseRt = rt.intersection([ ]); export const AllTagsFindRequestRt = rt.partial({ - [OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]), + owner: rt.union([rt.array(rt.string), rt.string]), }); export const AllReportersFindRequestRt = AllTagsFindRequestRt; diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 461881d3d1b8a..089bba8615725 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { OWNER_FIELD } from './constants'; import { SavedObjectFindOptionsRt } from '../saved_object'; import { UserRT } from '../user'; @@ -29,7 +28,7 @@ export const CommentAttributesBasicRt = rt.type({ ]), created_at: rt.string, created_by: UserRT, - [OWNER_FIELD]: rt.string, + owner: rt.string, pushed_at: rt.union([rt.string, rt.null]), pushed_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), @@ -45,7 +44,7 @@ export enum CommentType { export const ContextTypeUserRt = rt.type({ comment: rt.string, type: rt.literal(CommentType.user), - [OWNER_FIELD]: rt.string, + owner: rt.string, }); /** @@ -61,7 +60,7 @@ export const AlertCommentRequestRt = rt.type({ id: rt.union([rt.string, rt.null]), name: rt.union([rt.string, rt.null]), }), - [OWNER_FIELD]: rt.string, + owner: rt.string, }); const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 5e56b2f84533c..eeeb9ed4ebd04 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -18,7 +18,7 @@ const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-b const CasesConfigureBasicRt = rt.type({ connector: CaseConnectorRt, closure_type: ClosureTypeRT, - [OWNER_FIELD]: rt.string, + owner: rt.string, }); const CasesConfigureBasicWithoutOwnerRt = rt.type( @@ -48,12 +48,12 @@ export const CaseConfigureResponseRt = rt.intersection([ id: rt.string, version: rt.string, error: rt.union([rt.string, rt.null]), - [OWNER_FIELD]: rt.string, + owner: rt.string, }), ]); export const GetConfigureFindRequestRt = rt.partial({ - [OWNER_FIELD]: rt.union([rt.array(rt.string), rt.string]), + owner: rt.union([rt.array(rt.string), rt.string]), }); export const CaseConfigureRequestParamsRt = rt.type({ diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts index b61850851ec89..826654cab2d7f 100644 --- a/x-pack/plugins/cases/common/api/cases/sub_case.ts +++ b/x-pack/plugins/cases/common/api/cases/sub_case.ts @@ -10,7 +10,6 @@ import * as rt from 'io-ts'; import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; -import { OWNER_FIELD } from './constants'; import { CasesStatusResponseRt } from './status'; import { CaseStatusRt } from './status'; @@ -27,7 +26,7 @@ export const SubCaseAttributesRt = rt.intersection([ created_by: rt.union([UserRT, rt.null]), updated_at: rt.union([rt.string, rt.null]), updated_by: rt.union([UserRT, rt.null]), - [OWNER_FIELD]: rt.string, + owner: rt.string, }), ]); diff --git a/x-pack/plugins/cases/common/api/cases/user_actions.ts b/x-pack/plugins/cases/common/api/cases/user_actions.ts index 1473617b2f5b2..03912c550d77a 100644 --- a/x-pack/plugins/cases/common/api/cases/user_actions.ts +++ b/x-pack/plugins/cases/common/api/cases/user_actions.ts @@ -42,7 +42,7 @@ const CaseUserActionBasicRT = rt.type({ action_by: UserRT, new_value: rt.union([rt.string, rt.null]), old_value: rt.union([rt.string, rt.null]), - [OWNER_FIELD]: rt.string, + owner: rt.string, }); const CaseUserActionResponseRT = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index b23629b82767f..e0fdd2d7e62dc 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { OWNER_FIELD } from '../cases/constants'; const ActionTypeRT = rt.union([ rt.literal('append'), @@ -32,7 +31,7 @@ export const ConnectorMappingsAttributesRT = rt.type({ export const ConnectorMappingsRt = rt.type({ mappings: rt.array(ConnectorMappingsAttributesRT), - [OWNER_FIELD]: rt.string, + owner: rt.string, }); export type ConnectorMappingsAttributes = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index 3cb47fd3d4627..eb2dcc1a0f2e4 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -13,7 +13,7 @@ export const getOwnersFilter = (savedObjectType: string, owners: string[]): Kuer return nodeBuilder.or( owners.reduce((query, owner) => { ensureFieldIsSafeForQuery(OWNER_FIELD, owner); - query.push(nodeBuilder.is(`${savedObjectType}.attributes.owner`, owner)); + query.push(nodeBuilder.is(`${savedObjectType}.attributes.${OWNER_FIELD}`, owner)); return query; }, []) ); diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 0427da1824dfa..3991a9730c440 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -90,7 +90,7 @@ export const push = async ( // We need to change the logic when we support subcases if (theCase?.status === CaseStatuses.closed) { throw Boom.conflict( - `This case ${theCase.title} is closed. You can not pushed if the case is closed.` + `The ${theCase.title} case is closed. Pushing a closed case is not allowed.` ); } diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 732e99494be87..de3c499db5098 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -358,16 +358,11 @@ function partitionPatchRequest( conflictedCases: CasePatchRequest[]; casesToAuthorize: Array>; } { - const reqCasesMap = patchReqCases.reduce((acc, req) => { - acc.set(req.id, req); - return acc; - }, new Map()); - const nonExistingCases: CasePatchRequest[] = []; const conflictedCases: CasePatchRequest[] = []; const casesToAuthorize: Array> = []; - for (const reqCase of reqCasesMap.values()) { + for (const reqCase of patchReqCases) { const foundCase = casesMap.get(reqCase.id); if (!foundCase || foundCase.error) { 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..dfd151344b40c 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; import { Role, User, UserInfo } from './types'; -import { users } from './users'; +import { superUser, users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; @@ -90,3 +90,5 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[ await deleteSpaces(getService); await deleteUsersAndRoles(getService); }; + +export const superUserSpace1Auth = { user: superUser, space: 'space1' }; diff --git a/x-pack/test/case_api_integration/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 5476182a9c2c7..17aac2dd7e285 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 @@ -39,6 +39,8 @@ import { superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -238,14 +240,14 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseSec.id, expectedHttpCode: 200, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await getCase({ supertest: supertestWithoutAuth, caseId: caseObs.id, expectedHttpCode: 200, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 26f109168faa1..674c2c68381b8 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -58,6 +58,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -1169,7 +1170,7 @@ export default ({ getService }: FtrProviderContext): void => { expectedHttpCode: 403, }); - const resp = await findCases({ supertest, auth: { user: superUser, space: 'space1' } }); + const resp = await findCases({ supertest, auth: superUserSpace1Auth }); expect(resp.cases.length).to.eql(3); // the update should have failed and none of the title should have been changed expect(resp.cases[0].title).to.eql(postCaseReq.title); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index c8780939981c1..f58dfa1522d4a 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -25,6 +25,7 @@ import { secOnlyRead, superUser, } from '../../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -93,13 +94,13 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ), ]); @@ -124,7 +125,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const scenario of [ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 353974632feb8..73b85ef97d119 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -33,6 +33,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -277,14 +278,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await deleteComment({ @@ -309,14 +310,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await deleteComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 470c2481410ff..0f73b1ee7a624 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -40,6 +40,7 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -312,12 +313,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); @@ -340,12 +341,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 2be30ed7bc02c..361e72bdc79bf 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -31,6 +31,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -141,21 +142,21 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -174,14 +175,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const scenario of [ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 7b55d468312a1..98b6cc5a7a30c 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -30,6 +30,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -94,14 +95,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -119,14 +120,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index fcaebddeb8bde..c1f37d5eb2f05 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -45,6 +45,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -511,14 +512,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -546,14 +547,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -579,14 +580,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 83ca0395be9c5..1fcb49ec10ad4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -61,6 +61,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -534,7 +535,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ @@ -570,7 +571,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - { user: superUser, space: 'space1' } + superUserSpace1Auth ); await createComment({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index e12df95aa5b15..5cd4082bd3293 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -35,6 +35,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -358,7 +359,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 728be6929c883..3c096cb7557c3 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -55,6 +55,7 @@ import { superUser, } from '../../../../common/lib/authentication/users'; import { User } from '../../../../common/lib/authentication/types'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -294,7 +295,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should push a case that the user has permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ testAgent: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await pushCase({ @@ -308,7 +309,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ testAgent: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), }); @@ -327,7 +328,7 @@ export default ({ getService }: FtrProviderContext): void => { } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { const { postedCase, connector } = await createCaseWithConnector({ testAgent: supertestWithoutAuth, - auth: { user: superUser, space: 'space1' }, + auth: superUserSpace1Auth, }); await pushCase({