From 103388e2b9cf359a3119e17f691839936596a9ad Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Fri, 30 Apr 2021 09:29:20 -0400 Subject: [PATCH] [Cases] Attachments RBAC (#97756) * Starting rbac for comments * Adding authorization to rest of comment apis * Starting the comment rbac tests * Fixing some of the rbac tests * Adding some integration tests * Starting patch tests * Working tests for comments * Working tests * Fixing some tests * Fixing type issues from pulling in master * Fixing connector tests that only work in trial license * Attempting to fix cypress * Mock return of array for configure * Fixing cypress test * Cleaning up * Addressing PR comments * Reducing operations --- .../plugins/cases/common/api/cases/comment.ts | 10 + .../cases/common/api/cases/user_actions.ts | 1 + x-pack/plugins/cases/common/api/helpers.ts | 5 + .../server/authorization/audit_logger.ts | 16 +- .../cases/server/authorization/index.ts | 130 +++++- .../cases/server/authorization/types.ts | 51 ++- .../cases/server/authorization/utils.ts | 22 +- .../cases/server/client/attachments/add.ts | 95 +++-- .../cases/server/client/attachments/client.ts | 12 +- .../cases/server/client/attachments/delete.ts | 28 +- .../cases/server/client/attachments/get.ts | 102 ++++- .../cases/server/client/attachments/update.ts | 17 +- .../cases/server/client/cases/create.ts | 12 +- .../cases/server/client/cases/delete.ts | 14 +- .../plugins/cases/server/client/cases/find.ts | 4 +- .../plugins/cases/server/client/cases/mock.ts | 3 + .../cases/server/client/cases/utils.ts | 4 + .../cases/server/client/configure/client.ts | 20 - x-pack/plugins/cases/server/client/utils.ts | 200 +++++---- .../server/common/models/commentable_case.ts | 11 + .../plugins/cases/server/common/utils.test.ts | 20 +- x-pack/plugins/cases/server/common/utils.ts | 1 + .../server/connectors/case/index.test.ts | 4 + .../cases/server/connectors/case/index.ts | 1 + .../cases/server/connectors/case/schema.ts | 3 + x-pack/plugins/cases/server/plugin.ts | 2 +- .../api/__fixtures__/mock_saved_objects.ts | 6 + .../routes/api/comments/find_comments.ts | 11 +- .../cases/server/scripts/sub_cases/index.ts | 1 + .../server/services/attachments/index.ts | 4 +- .../cases/server/services/cases/index.ts | 8 +- .../server/services/user_actions/helpers.ts | 1 + .../feature_privilege_builder/cases.test.ts | 28 +- .../feature_privilege_builder/cases.ts | 5 +- .../integration/cases/connectors.spec.ts | 24 +- .../security_solution/cypress/objects/case.ts | 2 + .../cypress/tasks/api_calls/cases.ts | 1 + .../components/add_comment/index.test.tsx | 1 + .../cases/components/add_comment/index.tsx | 3 +- .../components/case_view/helpers.test.tsx | 2 + .../configure_cases/__mock__/index.tsx | 1 + .../add_to_case_action.test.tsx | 3 + .../timeline_actions/add_to_case_action.tsx | 2 + .../public/cases/containers/api.test.tsx | 1 + .../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/mock.ts | 3 + .../containers/use_post_comment.test.tsx | 1 + .../public/cases/containers/utils.ts | 10 + .../common/lib/authentication/index.ts | 18 +- .../common/lib/authentication/roles.ts | 2 +- .../case_api_integration/common/lib/mock.ts | 3 + .../case_api_integration/common/lib/utils.ts | 136 ++++-- .../tests/basic/configure/create_connector.ts | 20 + .../tests/common/cases/delete_cases.ts | 19 +- .../tests/common/cases/find_cases.ts | 26 +- .../tests/common/cases/get_case.ts | 16 +- .../tests/common/cases/patch_cases.ts | 148 ++++--- .../tests/common/comments/delete_comment.ts | 239 ++++++++++- .../tests/common/comments/find_comments.ts | 243 ++++++++++- .../tests/common/comments/get_all_comments.ts | 123 +++++- .../tests/common/comments/get_comment.ts | 119 +++++- .../tests/common/comments/patch_comment.ts | 393 ++++++++++++++---- .../tests/common/comments/post_comment.ts | 262 +++++++++--- .../tests/common/configure/get_configure.ts | 67 --- .../tests/common/configure/get_connectors.ts | 70 +--- .../tests/common/configure/migrations.ts | 7 +- .../tests/common/configure/patch_configure.ts | 122 ------ .../tests/common/configure/post_configure.ts | 61 --- .../tests/common/connectors/case.ts | 1 - .../tests/common/sub_cases/find_sub_cases.ts | 1 + .../tests/common/sub_cases/patch_sub_cases.ts | 5 + .../user_actions/get_all_user_actions.ts | 18 +- .../tests/trial/cases/push_case.ts | 2 +- .../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 + 82 files changed, 2594 insertions(+), 888 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/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 4eb2ad1eadd6c..089bba8615725 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'; @@ -27,6 +28,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 +44,7 @@ export enum CommentType { export const ContextTypeUserRt = rt.type({ comment: rt.string, type: rt.literal(CommentType.user), + owner: rt.string, }); /** @@ -57,6 +60,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]); @@ -112,6 +116,12 @@ export const CommentsResponseRt = rt.type({ export const AllCommentsResponseRt = rt.array(CommentResponseRt); +export const FindQueryParamsRt = rt.partial({ + ...SavedObjectFindOptionsRt.props, + subCaseId: 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/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/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/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 9f30e8cf7a8da..be8ca55ccd262 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { EventType } from '../../../security/server'; -import { CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../common/constants'; +import { EcsEventCategory, EcsEventOutcome, EcsEventType } from 'kibana/server'; +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'; @@ -37,13 +41,44 @@ const deleteVerbs: Verbs = { past: 'deleted', }; +const EVENT_TYPES: Record = { + creation: 'creation', + deletion: 'deletion', + change: 'change', + 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. + */ +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: EVENT_TYPES.creation, name: WriteOperations.CreateCase, action: 'create-case', verbs: createVerbs, @@ -51,7 +86,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', + GetComment = 'getComment', + GetAllComments = 'getAllComments', + FindComments = 'findComments', GetTags = 'getTags', GetReporters = 'getReporters', 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', UpdateCase = 'updateCase', + CreateComment = 'createComment', + DeleteAllComments = 'deleteAllComments', + DeleteComment = 'deleteComment', + UpdateComment = 'updateComment', CreateConfiguration = 'createConfiguration', UpdateConfiguration = 'updateConfiguration', } @@ -47,11 +59,30 @@ export enum WriteOperations { * Defines the structure for a case API route. */ export interface OperationDetails { - type: EventType; - name: ReadOperations | WriteOperations; + /** + * The ECS event type that this operation should be audit logged as (creation, deletion, access, etc) + */ + type: EcsEventType; + /** + * The name of the operation to authorize against for the privilege check. + * These values need to match one of the operation strings defined here: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + */ + name: string; + /** + * The ECS `event.action` field, should be in the form of - 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/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index a7e210d07d214..11d143eb05b2a 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, + authorizationFilter?: KueryNode ) => { - 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 => { @@ -41,5 +49,9 @@ export const ensureFieldIsSafeForQuery = (field: string, value: string): boolean return true; }; -export const includeFieldsRequiredForAuthentication = (fields: string[]): string[] => - uniq([...fields, 'owner']); +export const includeFieldsRequiredForAuthentication = (fields?: string[]): 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 cb0d7ef5a1e14..4cc9ca7f868ec 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,10 @@ import { CaseStatuses, CaseType, SubCaseAttributes, - CommentRequest, CaseResponse, User, - CommentRequestAlertType, AlertCommentRequestRt, + CommentRequest, } from '../../../common/api'; import { buildCaseUserActionItem, @@ -45,7 +49,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 +111,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 +140,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 +189,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,16 +296,20 @@ async function getCombinedCase({ } } -interface AddCommentArgs { +/** + * The arguments needed for creating a new attachment to a case. + */ +export interface AddArgs { caseId: string; comment: CommentRequest; } export const addComment = async ( - { caseId, comment }: AddCommentArgs, + addArgs: AddArgs, clientArgs: CasesClientArgs, casesClientInternal: CasesClientInternal ): Promise => { + const { comment, caseId } = addArgs; const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -305,6 +322,8 @@ export const addComment = async ( attachmentService, user, logger, + authorization, + auditLogger, } = clientArgs; if (isCommentRequestTypeGenAlert(comment)) { @@ -314,20 +333,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({ @@ -350,6 +370,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 7ffbb8684f959..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(params: AttachmentsAdd): Promise; + add(params: AddArgs): Promise; deleteAll(deleteAllArgs: DeleteAllArgs): Promise; delete(deleteArgs: DeleteArgs): Promise; find(findArgs: FindArgs): Promise; @@ -40,7 +34,7 @@ export const createAttachmentsSubClient = ( casesClientInternal: CasesClientInternal ): AttachmentsSubClient => { const attachmentSubClient: AttachmentsSubClient = { - add: (params: AttachmentsAdd) => addComment(params, clientArgs, casesClientInternal), + 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), diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 37069b94df7cb..f600aef64d1b6 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,18 @@ 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, + 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 +117,8 @@ export async function deleteComment( attachmentService, userActionService, logger, + authorization, + auditLogger, } = clientArgs; try { @@ -117,6 +135,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; @@ -146,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 70aeb5a3df2aa..f6f5bcfb4f046 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 { esKuery } from '../../../../../../src/plugins/data/server'; import { AllCommentsResponse, AllCommentsResponseRt, @@ -19,7 +17,7 @@ import { CommentResponseRt, CommentsResponse, CommentsResponseRt, - SavedObjectFindOptionsRt, + FindQueryParams, } from '../../../common/api'; import { checkEnabledCaseConnectorOrThrow, @@ -31,13 +29,14 @@ import { import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; - -const FindQueryParamsRt = rt.partial({ - ...SavedObjectFindOptionsRt.props, - subCaseId: rt.string, -}); - -type FindQueryParams = rt.TypeOf; +import { + combineFilters, + ensureAuthorized, + getAuthorizationFilter, + stringToKueryNode, +} from '../utils'; +import { Operations } from '../../authorization'; +import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; export interface FindArgs { caseID: string; @@ -60,14 +59,39 @@ 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([stringToKueryNode(filter), authorizationFilter]); + const args = queryParams ? { caseService, @@ -80,8 +104,9 @@ export async function find( page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', - filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, + filter: combinedFilter, ...queryWithoutFilter, + fields, }, associationType, } @@ -93,11 +118,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({ @@ -115,7 +151,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 +165,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({ @@ -141,7 +191,13 @@ export async function getAll( { caseID, includeSubCaseComments, subCaseID }: GetAllArgs, clientArgs: CasesClientArgs ): Promise { - const { savedObjectsClient: soClient, caseService, logger } = clientArgs; + const { + savedObjectsClient: soClient, + caseService, + logger, + authorization, + auditLogger, + } = clientArgs; try { let comments: SavedObjectsFindResponse; @@ -155,11 +211,22 @@ export async function getAll( ); } + const { + filter, + ensureSavedObjectsAreAuthorized, + logSuccessfulAuthorization, + } = await getAuthorizationFilter({ + authorization, + auditLogger, + operation: Operations.getAllComments, + }); + if (subCaseID) { comments = await caseService.getAllSubCaseComments({ soClient, id: subCaseID, options: { + filter, sortField: defaultSortField, }, }); @@ -169,11 +236,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..c2c6d6800e51f 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 { @@ -120,10 +123,22 @@ export async function update( throw Boom.notFound(`This comment ${queryCommentId} does not exist anymore.`); } + await ensureAuthorized({ + authorization, + auditLogger, + operation: Operations.updateComment, + savedObjectIDs: [myComment.id], + owners: [myComment.attributes.owner], + }); + if (myComment.attributes.type !== queryRestAttributes.type) { 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/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 15fbd34628182..3f66db7281c38 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -22,11 +22,10 @@ import { CaseType, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { createAuditMsg, ensureAuthorized, getConnectorFromConfiguration } from '../utils'; +import { ensureAuthorized, getConnectorFromConfiguration } from '../utils'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; -import { EventOutcome } from '../../../../security/server'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { flattenCaseSavedObject, @@ -82,15 +81,6 @@ export const create = async ( savedObjectIDs: [savedObjectID], }); - // log that we're attempting to create a case - auditLogger?.log( - createAuditMsg({ - operation: Operations.createCase, - outcome: EventOutcome.UNKNOWN, - savedObjectID, - }) - ); - // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 4657df2e71b30..100135e2992eb 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -13,8 +13,7 @@ import { createCaseError } from '../../common/error'; import { AttachmentService, CaseService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; -import { createAuditMsg, ensureAuthorized } from '../utils'; -import { EventOutcome } from '../../../../security/server'; +import { ensureAuthorized } from '../utils'; async function deleteSubCases({ attachmentService, @@ -88,17 +87,6 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P savedObjectIDs: [...soIds.values()], }); - // log that we're attempting to delete a case - for (const savedObjectID of soIds) { - auditLogger?.log( - createAuditMsg({ - operation: Operations.deleteCase, - outcome: EventOutcome.UNKNOWN, - savedObjectID, - }) - ); - } - await Promise.all( ids.map((id) => caseService.deleteCase({ diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 988812da0d852..53ae6a2e76b81 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -73,9 +73,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/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 1037a2ff9d893..1e44e615626b7 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'; @@ -44,7 +43,6 @@ import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter, - createAuditMsg, ensureAuthorized, getAuthorizationFilter, } from '../utils'; @@ -276,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: EventOutcome.UNKNOWN, - savedObjectID: configuration.id, - }) - ); - if (version !== configuration.version) { throw Boom.conflict( 'This configuration has been updated. Please refresh before saving additional updates.' @@ -426,15 +415,6 @@ async function create( savedObjectIDs: [savedObjectID], }); - // log that we're attempting to create a configuration - auditLogger?.log( - createAuditMsg({ - operation: Operations.createConfiguration, - outcome: EventOutcome.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 b61de9f2beb6a..eb00cce8654ef 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -12,9 +12,10 @@ 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'; import { CaseConnector, ESCasesConfigureAttributes, @@ -28,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, @@ -36,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) => { @@ -118,21 +119,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]( @@ -140,24 +147,47 @@ export const buildFilter = ({ ); }; +/** + * Combines the authorized filters with the requested owners. + */ export const combineAuthorizedAndOwnerFilter = ( owner?: string[] | string, authorizationFilter?: KueryNode, savedObjectType?: string ): KueryNode | undefined => { - const filters = Array.isArray(owner) ? owner : owner != null ? [owner] : []; const ownerFilter = buildFilter({ - filters, + filters: owner, field: 'owner', operator: 'or', type: savedObjectType, }); - return authorizationFilter != null && ownerFilter != null - ? combineFilterWithAuthorizationFilter(ownerFilter, authorizationFilter) - : authorizationFilter ?? ownerFilter ?? undefined; + 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); +} + +/** + * Creates a KueryNode from a string expression. Returns undefined if the expression is undefined. + */ +export function stringToKueryNode(expression?: string): 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 @@ -238,10 +268,7 @@ export const constructQueryOptions = ({ return { case: { - filter: - authorizationFilter != null && caseFilters != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) - : caseFilters, + filter: combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter), sortField, }, }; @@ -263,17 +290,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,17 +335,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, }, }; @@ -466,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. @@ -483,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; } } @@ -502,6 +570,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. @@ -514,7 +588,7 @@ export async function getAuthorizationFilter({ operation: OperationDetails; authorization: PublicMethodsOf; auditLogger?: AuditLogger; -}) { +}): Promise { try { const { filter, @@ -540,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?: EventOutcome; - 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 === EventOutcome.UNKNOWN - ? `User is ${operation.verbs.progressive} ${doc}` - : `User has ${operation.verbs.past} ${doc}`; - - return { - message, - event: { - action: operation.action, - category: EventCategory.DATABASE, - type: operation.type, - outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.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/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index d2276c0027ece..81b5aca58f797 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,10 @@ export class CommentableCase { return this.subCase?.id; } + private get owner(): string { + return this.collection.attributes.owner; + } + private buildRefsToCase(): SavedObjectReference[] { const subCaseSOType = SUB_CASE_SAVED_OBJECT; const caseSOType = CASE_SAVED_OBJECT; @@ -244,10 +248,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 +266,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 +280,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..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", @@ -586,6 +587,7 @@ describe('common utils', () => { full_name: 'Elastic', username: 'elastic', associationType: AssociationType.case, + owner: 'securitySolution', }; const res = transformNewComment(comment); @@ -599,6 +601,7 @@ describe('common utils', () => { "full_name": "Elastic", "username": "elastic", }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -613,6 +616,7 @@ describe('common utils', () => { comment: 'A comment', type: CommentType.user as const, createdDate: '2020-04-09T09:43:51.778Z', + owner: 'securitySolution', associationType: AssociationType.case, }; @@ -628,6 +632,7 @@ describe('common utils', () => { "full_name": undefined, "username": undefined, }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -645,6 +650,7 @@ describe('common utils', () => { email: null, full_name: null, username: null, + owner: 'securitySolution', associationType: AssociationType.case, }; @@ -660,6 +666,7 @@ describe('common utils', () => { "full_name": null, "username": null, }, + "owner": "securitySolution", "pushed_at": null, "pushed_by": null, "type": "user", @@ -675,7 +682,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 +706,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, + owner: 'securitySolution', }, ], }, @@ -719,6 +730,7 @@ describe('common utils', () => { id: 'rule-id-1', name: 'rule-name-1', }, + owner: 'securitySolution', }, ], }, @@ -739,6 +751,7 @@ describe('common utils', () => { { alertId: ['a', 'b'], index: '', + owner: 'securitySolution', type: CommentType.alert, rule: { id: 'rule-id-1', @@ -747,6 +760,7 @@ describe('common utils', () => { }, { comment: '', + owner: 'securitySolution', type: CommentType.user, }, ], @@ -766,6 +780,7 @@ describe('common utils', () => { ids: ['1'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -780,6 +795,7 @@ describe('common utils', () => { ids: ['2'], comments: [ { + owner: 'securitySolution', comment: '', type: CommentType.user, }, @@ -803,6 +819,7 @@ describe('common utils', () => { ids: ['1', '2'], comments: [ { + owner: 'securitySolution', alertId: ['a', 'b'], index: '', type: CommentType.alert, @@ -834,6 +851,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/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 876b8909b9317..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', }, }, }; @@ -1134,6 +1136,7 @@ describe('case connector', () => { username: 'awesome', }, id: 'mock-comment', + owner: 'securitySolution', pushed_at: null, pushed_by: null, updated_at: null, @@ -1157,6 +1160,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/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/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index 933a59cf06016..e5b826cf0ddef 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/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts index c992e7d0c114c..a758805deb6ef 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/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/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/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/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 2362d893739a0..870ba94b1ba13 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -795,7 +795,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, @@ -822,7 +822,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; } } @@ -866,7 +866,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: { @@ -899,7 +899,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/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/server/authorization/privileges/feature_privilege_builder/cases.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.test.ts index 4ca2bd01d9a2d..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,7 +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/getTags", "cases:1.0.0-zeta1:observability/getReporters", "cases:1.0.0-zeta1:observability/findConfigurations", @@ -109,13 +109,16 @@ 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/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/createComment", + "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", ] @@ -153,17 +156,20 @@ 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/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/createComment", + "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/getTags", "cases:1.0.0-zeta1:obs/getReporters", "cases:1.0.0-zeta1:obs/findConfigurations", @@ -202,32 +208,38 @@ 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/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/createComment", + "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/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/createComment", + "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/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/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 1ff72e9ad3fe1..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,7 +14,7 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; // x-pack/plugins/cases/server/authorization/index.ts const readOperations: string[] = [ 'getCase', - 'findCases', + 'getComment', 'getTags', 'getReporters', 'findConfigurations', @@ -23,6 +23,9 @@ const writeOperations: string[] = [ 'createCase', 'deleteCase', 'updateCase', + 'createComment', + 'deleteComment', + 'updateComment', 'createConfiguration', 'updateConfiguration', ]; 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..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(); @@ -53,16 +55,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); }); 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' }, }); 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/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 = { 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/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/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/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(() => { 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), 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/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index cf21b01c3967e..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,7 @@ export const globalRead: Role = { { feature: { securitySolutionFixture: ['read'], - observabilityFixture: ['all'], + 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 c3a6cb8714115..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,6 +74,7 @@ export const userActionPostResp: CasesClientPostRequest = { export const postCommentUserReq: CommentRequestUserType = { comment: 'This is a cool comment', type: CommentType.user, + owner: 'securitySolutionFixture', }; 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: 'securitySolutionFixture', }; 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: '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 0a0151d37d3f8..43090df495ce9 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -470,6 +470,7 @@ export const deleteCasesUserActions = async (es: KibanaClient): Promise => wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -481,6 +482,7 @@ export const deleteCasesByESQuery = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -496,6 +498,7 @@ export const deleteSubCases = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -507,6 +510,7 @@ export const deleteComments = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -518,6 +522,7 @@ export const deleteConfiguration = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; @@ -529,10 +534,11 @@ export const deleteMappings = async (es: KibanaClient): Promise => { wait_for_completion: true, refresh: true, body: {}, + conflicts: 'proceed', }); }; -export const getSpaceUrlPrefix = (spaceId?: string | null) => { +export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; @@ -592,13 +598,19 @@ export const deleteCases = async ({ return body; }; -export const createComment = async ( - supertest: st.SuperTest, - caseId: string, - params: CommentRequest, - expectedHttpCode: number = 200, - auth: { user: User; space: string | null } = { user: superUser, space: null } -): Promise => { +export const createComment = async ({ + supertest, + caseId, + params, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, +}: { + supertest: st.SuperTest; + caseId: string; + params: CommentRequest; + auth?: { user: User; space: string | null }; + expectedHttpCode?: number; +}): Promise => { const { body: theCase } = await supertest .post(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .auth(auth.user.username, auth.user.password) @@ -636,58 +648,108 @@ 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, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + commentId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): 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, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise<{} | Error> => { + const { body: comment } = await supertest + .delete(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode) + .send(); + + return comment; +}; + +export const getAllComments = async ({ + supertest, + caseId, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + caseId: string; + auth?: { user: User; space: string | null }; + expectedHttpCode?: number; +}): Promise => { + const { body: comments } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments`) + .auth(auth.user.username, auth.user.password) .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') + .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); 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/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/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 9ebc16f5e07aa..484dca314c9cc 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 @@ -61,14 +61,27 @@ 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); + 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('should create a user action when creating a case', async () => { 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 c537d2477cb59..6bcd78f98e5eb 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 @@ -137,8 +137,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({ @@ -566,7 +570,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases', async () => { await Promise.all([ // Create case owned by the security solution user - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -576,7 +580,7 @@ export default ({ getService }: FtrProviderContext): void => { } ), // Create case owned by the observability user - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, @@ -651,7 +655,7 @@ 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 createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -661,7 +665,7 @@ export default ({ getService }: FtrProviderContext): void => { } ), // super user creates a case with owner observabilityFixture - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, @@ -692,7 +696,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() @@ -725,7 +729,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { await Promise.all([ - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -734,7 +738,7 @@ export default ({ getService }: FtrProviderContext): void => { space: 'space1', } ), - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, @@ -762,7 +766,7 @@ 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 createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, @@ -771,7 +775,7 @@ export default ({ getService }: FtrProviderContext): void => { space: 'space1', } ), - await createCase( + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, 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 187c84be7c196..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 @@ -60,7 +60,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, caseId: postedCase.id, includeComments: true }); const comment = removeServerGeneratedPropertiesFromSavedObject( @@ -76,6 +76,7 @@ export default ({ getService }: FtrProviderContext): void => { pushed_at: null, pushed_by: null, updated_by: null, + owner: 'securitySolutionFixture', }); }); @@ -127,9 +128,15 @@ export default ({ getService }: FtrProviderContext): void => { } ); - await createComment(supertestWithoutAuth, postedCase.id, postCommentUserReq, 200, { - user: secOnly, - space: 'space1', + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, }); const theCase = await getCase({ @@ -152,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 1d7baabaf93b0..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 @@ -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: 'securitySolutionFixture', + }, }); 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: 'securitySolutionFixture', + }, }); 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: 'securitySolutionFixture', + }, }); - 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: 'securitySolutionFixture', + }, }); 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: 'securitySolutionFixture', }, - 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: 'securitySolutionFixture', }, }); @@ -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: 'securitySolutionFixture', }, - 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: '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 cd4e72f6f9315..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 @@ -6,10 +6,10 @@ */ 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'; +import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -21,7 +21,18 @@ import { createCase, createComment, deleteComment, + 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 => { @@ -38,8 +49,16 @@ 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 comment = await deleteComment(supertest, postedCase.id, patchedCase.comments![0].id); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); expect(comment).to.eql({}); }); @@ -48,13 +67,17 @@ 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 error = (await deleteComment( + const patchedCase = await createComment({ supertest, - 'fake-id', - patchedCase.comments![0].id, - 404 - )) as Error; + caseId: postedCase.id, + params: postCommentUserReq, + }); + const error = (await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + })) as Error; expect(error.message).to.be( `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` @@ -62,7 +85,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 () => { @@ -150,5 +178,194 @@ 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 createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should delete multiple comments from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not delete a comment from a different owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: { user: obsOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { 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, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { 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, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_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..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 @@ -6,21 +6,41 @@ */ 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 { + getPostCaseRequest, + postCaseReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; import { createCaseAction, + createComment, createSubCase, deleteAllCaseItems, deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, + createCase, } from '../../../../common/lib/utils'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, +} from '../../../../common/lib/authentication/users'; + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); @@ -79,7 +99,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 @@ -151,5 +171,222 @@ 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 + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: space1 } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: space1 } + ), + // Create case owned by the observability user + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: space1 }, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: { user: obsOnly, space: space1 }, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyRead, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); + + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: scenario.space } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: scenario.space }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // user should not be able to read comments + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(403); + }); + } + + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: 'space1' }, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + }); + + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { 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?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + expect(res.comments.length).to.be(0); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + await supertest + .get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`) + .expect(400); + + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_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..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 @@ -6,10 +6,10 @@ */ 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'; +import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -20,6 +20,17 @@ import { getAllComments, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -33,9 +44,17 @@ 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); - const comments = await getAllComments(supertest, postedCase.id); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comments = await getAllComments({ supertest, caseId: postedCase.id }); expect(comments.length).to.eql(2); }); @@ -113,5 +132,99 @@ 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 createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(comments.length).to.eql(2); + } + }); + + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnly, returnCode: 200 }, + { user: obsOnlyRead, returnCode: 200 }, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: scenario.user, space: 'space1' }, + expectedHttpCode: scenario.returnCode, + }); + + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); + } + } + }); + + it('should NOT get a comment in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_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..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 @@ -6,9 +6,9 @@ */ 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 { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { createCaseAction, createSubCase, @@ -19,6 +19,17 @@ import { getComment, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -32,14 +43,27 @@ 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 comment = await getComment(supertest, postedCase.id, patchedCase.comments![0].id); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); expect(comment).to.eql(patchedCase.comments![0]); }); it('unhappy path - 404s when comment is not there', async () => { - await getComment(supertest, '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 @@ -53,9 +77,92 @@ 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 createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { 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 createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space1' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { 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 createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_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..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 @@ -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 => { @@ -61,6 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }) .expect(400); @@ -147,14 +159,23 @@ 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, { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - type: CommentType.user, + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }, }); const userComment = updatedCase.comments![0] as AttributesTypeUser; @@ -165,16 +186,25 @@ 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 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 patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', }, }); @@ -189,43 +219,71 @@ export default ({ getService }: FtrProviderContext): void => { expect(alertComment.updated_by).to.eql(defaultUser); }); + it('should not allow updating the owner of a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: postCommentUserReq.comment, + owner: 'changedOwner', + }, + expectedHttpCode: 400, + }); + }); + it('unhappy path - 404s when comment is not there', async () => { const postedCase = await createCase(supertest, postCaseReq); - await updateComment( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: 'id', version: 'version', type: CommentType.user, comment: 'comment', + owner: 'securitySolutionFixture', }, - 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: 'securitySolutionFixture', }, - 404 - ); + expectedHttpCode: 404, + }); }); 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( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.alert, @@ -235,50 +293,64 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); }); 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( + 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 () => { 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( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, comment: 'a comment', type: CommentType.user, [attribute]: attribute, + owner: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); } }); 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, @@ -292,29 +364,33 @@ 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, + }); } }); 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( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: CommentType.alert, @@ -324,29 +400,35 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', [attribute]: attribute, }, - 400 - ); + expectedHttpCode: 400, + }); } }); 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( + await updateComment({ supertest, - postedCase.id, - { + caseId: postedCase.id, + req: { id: patchedCase.comments![0].id, version: 'version-mismatch', type: CommentType.user, comment: newComment, + owner: 'securitySolutionFixture', }, - 409 - ); + expectedHttpCode: 409, + }); }); describe('alert format', () => { @@ -359,21 +441,26 @@ 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( + await updateComment({ supertest, - patchedCase.id, - { + caseId: patchedCase.id, + req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, type: type as AlertComment, alertId, index, + owner: 'securitySolutionFixture', rule: postCommentAlertReq.rule, }, - 400 - ); + expectedHttpCode: 400, + }); }); } @@ -383,23 +470,171 @@ 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: 'securitySolutionFixture', + type: type as AlertComment, + }, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolutionFixture', + rule: postCommentAlertReq.rule, + }, }); + }); + } + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should update a comment that the user has permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { 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' }, + }); - await updateComment(supertest, postedCase.id, { + 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, - type: type as AlertComment, - alertId, - index, - rule: postCommentAlertReq.rule, + 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: 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, space: 'space1' }, + expectedHttpCode: 403, }); }); } + + it('should not update a comment in a space the user does not have permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space2' }, + // getting the case will fail in the saved object layer with a 403 + expectedHttpCode: 403, + }); + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_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..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 => { @@ -67,7 +78,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 ); @@ -80,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 @@ -89,7 +105,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 ); @@ -104,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 @@ -113,7 +134,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]); @@ -121,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}`, @@ -131,46 +156,61 @@ export default ({ getService }: FtrProviderContext): void => { }); describe('unhappy path', () => { + it('400s when attempting to create a comment with a different owner than the case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'securitySolutionFixture' }) + ); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + expectedHttpCode: 400, + }); + }); + it('400s when type is missing', async () => { const postedCase = await createCase(supertest, postCaseReq); - await createComment( + 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: 'securitySolutionFixture', }, - 400 - ); + expectedHttpCode: 400, + }); } }); @@ -185,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, + }); } }); @@ -198,10 +244,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 +256,23 @@ export default ({ getService }: FtrProviderContext): void => { id: 'id', name: 'name', }, + owner: 'securitySolutionFixture', }, - 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 +292,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 +369,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: 'securitySolutionFixture', + type: CommentType.alert, }, - type: CommentType.alert, }); const { body: updatedAlert } = await supertest @@ -360,14 +422,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: 'securitySolutionFixture', + type: CommentType.alert, }, - type: CommentType.alert, }); const { body: updatedAlert } = await supertest @@ -391,12 +458,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 +473,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, + }); }); } }); @@ -453,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, + }); + }); + }); }); }; 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/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..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,113 +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); - - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }), - 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, - }) - ); - - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...getConfigurationRequest({ - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }), - 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/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); 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', }, }); 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', }); }); }); 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); 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')); }); };