diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts index 677c43193a091..12342185ab797 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts @@ -30,9 +30,9 @@ import { PersistableStateAttachmentPayloadRt, AttachmentType, AttachmentRt, - AttachmentsRt, EventAttachmentPayloadRt, } from '../../domain/attachment/v1'; +import { AttachmentRtV2 } from '../../domain/attachment/v2'; /** * Files @@ -157,7 +157,7 @@ export const BulkGetAttachmentsRequestRt = rt.strict({ }); export const BulkGetAttachmentsResponseRt = rt.strict({ - attachments: AttachmentsRt, + attachments: rt.array(AttachmentRtV2), errors: rt.array( rt.strict({ error: rt.string, diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts index 1810391a38b33..2d278924d739b 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts @@ -15,7 +15,7 @@ import { CaseUserActionBasicRt, UserActionsRt, } from '../../domain/user_action/v1'; -import type { Attachments } from '../../domain'; +import type { BulkGetAttachmentsResponse } from '../attachment/v1'; export type UserActionWithResponse = T & { id: string; version: string } & rt.TypeOf< typeof CaseUserActionInjectedIdsRt @@ -93,5 +93,5 @@ export const UserActionFindResponseRt = rt.strict({ export type UserActionFindResponse = rt.TypeOf; export interface UserActionInternalFindResponse extends UserActionFindResponse { - latestAttachments: Attachments; + latestAttachments: BulkGetAttachmentsResponse['attachments']; } diff --git a/x-pack/platform/plugins/shared/cases/public/api/utils.ts b/x-pack/platform/plugins/shared/cases/public/api/utils.ts index f50f8aac870db..80e99ce2956b1 100644 --- a/x-pack/platform/plugins/shared/cases/public/api/utils.ts +++ b/x-pack/platform/plugins/shared/cases/public/api/utils.ts @@ -90,7 +90,9 @@ export const convertCaseResolveToCamelCase = (res: CaseResolveResponse): Resolve }; }; -export const convertAttachmentsToCamelCase = (attachments: Attachment[]): AttachmentUI[] => { +export const convertAttachmentsToCamelCase = ( + attachments: Array +): AttachmentUI[] => { return attachments.map((attachment) => convertAttachmentToCamelCase(attachment)); }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx index 6d7ff0f01a137..bafb7af9cf09c 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/comment.tsx @@ -8,7 +8,7 @@ import type { EuiCommentProps } from '@elastic/eui'; import type { SnakeToCamelCase } from '../../../../common/types'; -import type { CommentUserAction } from '../../../../common/types/domain'; +import type { CommentUserAction, UnifiedAttachment } from '../../../../common/types/domain'; import { UserActionActions, AttachmentType } from '../../../../common/types/domain'; import { type AttachmentTypeRegistry } from '../../../../common/registry'; import type { UserActionBuilder, UserActionBuilderArgs } from '../types'; @@ -20,6 +20,7 @@ import { createAlertAttachmentUserActionBuilder } from '../../attachments/alert/ import { createActionAttachmentUserActionBuilder } from '../../attachments/host_isolation/actions'; import { createExternalReferenceAttachmentUserActionBuilder } from './external_reference'; import { createPersistableStateAttachmentUserActionBuilder } from './persistable_state'; +import { createUnifiedValueAttachmentUserActionBuilder } from './unified_value'; import type { AttachmentType as AttachmentFrameworkAttachmentType } from '../../../client/attachment_framework/types'; import { createEventAttachmentUserActionBuilder } from '../../attachments/event/event'; import { isLegacyAttachmentRequest } from '../../../../common/utils/attachments'; @@ -31,6 +32,7 @@ interface DeleteLabelTitle { caseData: UserActionBuilderArgs['caseData']; externalReferenceAttachmentTypeRegistry: UserActionBuilderArgs['externalReferenceAttachmentTypeRegistry']; persistableStateAttachmentTypeRegistry: UserActionBuilderArgs['persistableStateAttachmentTypeRegistry']; + unifiedAttachmentTypeRegistry: UserActionBuilderArgs['unifiedAttachmentTypeRegistry']; } const getDeleteLabelTitle = ({ @@ -38,6 +40,7 @@ const getDeleteLabelTitle = ({ caseData, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, }: DeleteLabelTitle) => { const { comment } = userAction.payload; if (isLegacyAttachmentRequest(comment)) { @@ -71,6 +74,16 @@ const getDeleteLabelTitle = ({ }), }); } + } else if (unifiedAttachmentTypeRegistry?.has(comment.type)) { + return getDeleteLabelFromRegistry({ + caseData, + registry: unifiedAttachmentTypeRegistry, + getId: () => comment.type, + getAttachmentProps: () => ({ + data: (comment as unknown as { data: Record }).data ?? {}, + metadata: (comment as unknown as { metadata?: Record }).metadata, + }), + }); } return `${i18n.REMOVED_FIELD} ${i18n.COMMENT.toLowerCase()}`; @@ -115,6 +128,7 @@ const getDeleteCommentUserAction = ({ caseData, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, handleOutlineComment, }: { userAction: SnakeToCamelCase; @@ -124,6 +138,7 @@ const getDeleteCommentUserAction = ({ | 'userProfiles' | 'externalReferenceAttachmentTypeRegistry' | 'persistableStateAttachmentTypeRegistry' + | 'unifiedAttachmentTypeRegistry' | 'caseData' >): EuiCommentProps[] => { const label = getDeleteLabelTitle({ @@ -131,6 +146,7 @@ const getDeleteCommentUserAction = ({ caseData, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, }); const commonBuilder = createCommonUpdateUserActionBuilder({ @@ -151,6 +167,7 @@ const getCreateCommentUserAction = ({ caseData, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, attachment, manageMarkdownEditIds, selectedOutlineCommentId, @@ -250,6 +267,19 @@ const getCreateCommentUserAction = ({ return persistableBuilder.build(); default: + if (unifiedAttachmentTypeRegistry?.has((attachment as unknown as { type: string }).type)) { + const unifiedBuilder = createUnifiedValueAttachmentUserActionBuilder({ + userAction, + userProfiles, + attachment: attachment as unknown as SnakeToCamelCase, + unifiedAttachmentTypeRegistry, + caseData, + isLoading: loadingCommentIds.includes((attachment as unknown as { id: string }).id), + handleDeleteComment, + }); + + return unifiedBuilder.build(); + } return []; } }; @@ -261,6 +291,7 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ userProfiles, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, userAction, manageMarkdownEditIds, selectedOutlineCommentId, @@ -288,6 +319,7 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ userProfiles, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, }); } @@ -306,6 +338,7 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({ userAction: attachmentUserAction, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, attachment, manageMarkdownEditIds, selectedOutlineCommentId, diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/registered_attachments.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/registered_attachments.tsx index 02c7cc6281f4b..aaa99ed5be42b 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/registered_attachments.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/registered_attachments.tsx @@ -26,6 +26,7 @@ import { AttachmentActionType } from '../../../client/attachment_framework/types import { UserActionTimestamp } from '../timestamp'; import type { AttachmentTypeRegistry } from '../../../../common/registry'; import type { Attachment } from '../../../../common/types/domain'; +import type { UnifiedAttachment } from '../../../../common/types/domain/attachment/v2'; import type { UserActionBuilder, UserActionBuilderArgs } from '../types'; import type { SnakeToCamelCase } from '../../../../common/types'; import { @@ -76,7 +77,7 @@ const getAttachmentRenderer = memoize((cachingKey: string) => { }); export const createRegisteredAttachmentUserActionBuilder = < - C extends Attachment, + C extends Attachment | UnifiedAttachment, // eslint-disable-next-line @typescript-eslint/no-explicit-any R extends AttachmentTypeRegistry> >({ diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/unified_value.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/unified_value.tsx new file mode 100644 index 0000000000000..400964daf3a55 --- /dev/null +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/comment/unified_value.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { UserActionBuilder, UserActionBuilderArgs } from '../types'; +import type { SnakeToCamelCase } from '../../../../common/types'; +import type { UnifiedAttachment } from '../../../../common/types/domain'; +import { createRegisteredAttachmentUserActionBuilder } from './registered_attachments'; + +type BuilderArgs = Pick< + UserActionBuilderArgs, + | 'userAction' + | 'unifiedAttachmentTypeRegistry' + | 'caseData' + | 'handleDeleteComment' + | 'userProfiles' +> & { + attachment: SnakeToCamelCase; + isLoading: boolean; +}; + +export const createUnifiedValueAttachmentUserActionBuilder = ({ + userAction, + userProfiles, + attachment, + unifiedAttachmentTypeRegistry, + caseData, + isLoading, + handleDeleteComment, +}: BuilderArgs): ReturnType => { + return createRegisteredAttachmentUserActionBuilder({ + userAction, + userProfiles, + attachment, + registry: unifiedAttachmentTypeRegistry, + caseData, + handleDeleteComment, + isLoading, + getId: () => attachment.type, + getAttachmentViewProps: () => ({ + data: (attachment as unknown as { data: Record }).data ?? {}, + metadata: (attachment as unknown as { metadata?: Record }).metadata, + }), + }); +}; diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/mock.ts b/x-pack/platform/plugins/shared/cases/public/components/user_actions/mock.ts index 32c006b7c4494..80e738ee37a88 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/mock.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/mock.ts @@ -9,6 +9,7 @@ import { UserActionActions } from '../../../common/types/domain'; import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; +import { UnifiedAttachmentTypeRegistry } from '../../client/attachment_framework/unified_attachment_registry'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; import { basicCase, getUserAction } from '../../containers/mock'; import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; @@ -53,6 +54,7 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => { const handleOutlineComment = jest.fn(); const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(); const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry(); + const unifiedAttachmentTypeRegistry = new UnifiedAttachmentTypeRegistry(); return { userAction, @@ -60,6 +62,7 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => { currentUserProfile: userProfiles[0], externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, caseData: basicCase, casesConfiguration: casesConfigurationsMock, attachments: basicCase.comments, diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/types.ts b/x-pack/platform/plugins/shared/cases/public/components/user_actions/types.ts index 28ca8f03bccf3..e99c86af048e3 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/types.ts @@ -22,6 +22,7 @@ import type { UNSUPPORTED_ACTION_TYPES } from './constants'; import type { OnUpdateFields } from '../case_view/types'; import type { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry'; import type { PersistableStateAttachmentTypeRegistry } from '../../client/attachment_framework/persistable_state_registry'; +import type { UnifiedAttachmentTypeRegistry } from '../../client/attachment_framework/unified_attachment_registry'; import type { CurrentUserProfile } from '../types'; import type { UserActivityParams } from '../user_actions_activity_bar/types'; @@ -56,6 +57,7 @@ export interface UserActionBuilderArgs { currentUserProfile: CurrentUserProfile; externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; + unifiedAttachmentTypeRegistry: UnifiedAttachmentTypeRegistry; caseConnectors: CaseConnectors; userAction: UserActionUI; attachments: AttachmentUI[]; diff --git a/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_list.tsx b/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_list.tsx index 65ee655ecbe7b..d1d112d2c505f 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_list.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/user_actions/user_actions_list.tsx @@ -102,8 +102,11 @@ export const UserActionsList = React.memo( bottomActions = [], isExpandable = false, }: UserActionListProps) => { - const { externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry } = - useCasesContext(); + const { + externalReferenceAttachmentTypeRegistry, + persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, + } = useCasesContext(); const { owner } = useCasesContext(); const { commentId } = useCaseViewParams(); const [initLoading, setInitLoading] = useState(true); @@ -142,6 +145,7 @@ export const UserActionsList = React.memo( caseConnectors, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, userAction, userProfiles, currentUserProfile, @@ -170,6 +174,7 @@ export const UserActionsList = React.memo( caseConnectors, externalReferenceAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry, + unifiedAttachmentTypeRegistry, userProfiles, currentUserProfile, attachments, diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts index 5537755bfb398..6599401a170f6 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts @@ -22,6 +22,7 @@ import type { AttachmentSavedObject, SOWithErrors } from '../../common/types'; import { partitionByCaseAssociation } from '../../common/partitioning'; import { decodeOrThrow, decodeWithExcessOrThrow } from '../../common/runtime_types'; import type { AttachmentAttributes } from '../../../common/types/domain'; +import { CASE_ATTACHMENT_SAVED_OBJECT } from '../../../common/constants'; type AttachmentSavedObjectWithErrors = Array>; @@ -50,11 +51,21 @@ export async function bulkGet( const { validAttachments, attachmentsWithErrors, invalidAssociationAttachments } = partitionAttachments(caseID, attachments); + const [unifiedAttachments, legacyAttachments] = partition( + validAttachments, + (so) => so.type === CASE_ATTACHMENT_SAVED_OBJECT + ); + const { authorized: authorizedAttachments, unauthorized: unauthorizedAttachments } = - await authorization.getAndEnsureAuthorizedEntities({ - savedObjects: validAttachments, - operation: Operations.bulkGetAttachments, - }); + legacyAttachments.length > 0 + ? await authorization.getAndEnsureAuthorizedEntities({ + savedObjects: legacyAttachments, + operation: Operations.bulkGetAttachments, + }) + : { + authorized: [] as AttachmentSavedObject[], + unauthorized: [] as AttachmentSavedObject[], + }; const errors = constructErrors({ associationErrors: invalidAssociationAttachments, @@ -63,8 +74,15 @@ export async function bulkGet( caseId: caseID, }); + const flattenedLegacy = flattenCommentSavedObjects(authorizedAttachments); + const flattenedUnified = unifiedAttachments.map((so) => ({ + id: so.id, + version: so.version ?? '0', + ...so.attributes, + })); + const res = { - attachments: flattenCommentSavedObjects(authorizedAttachments), + attachments: [...flattenedLegacy, ...flattenedUnified], errors, }; diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts index 2b304c45b4e90..de8da5ccd0eb3 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import type { AlertAttachmentPayload } from '../../../common/types/domain'; import { UserActionActions, UserActionTypes } from '../../../common/types/domain'; import { decodeOrThrow } from '../../common/runtime_types'; -import { CASE_SAVED_OBJECT } from '../../../common/constants'; +import { CASE_SAVED_OBJECT, CASE_ATTACHMENT_SAVED_OBJECT } from '../../../common/constants'; import { getAlertInfoFromComments, isCommentRequestTypeAlert } from '../../common/utils'; import type { CasesClientArgs } from '../types'; import { createCaseError } from '../../common/error'; @@ -42,13 +42,19 @@ export async function deleteAll( throw Boom.notFound(`No comments found for ${caseID}.`); } - await authorization.ensureAuthorized({ - operation: Operations.deleteAllComments, - entities: comments.saved_objects.map((comment) => ({ - owner: comment.attributes.owner, - id: comment.id, - })), - }); + const legacyComments = comments.saved_objects.filter( + (so) => so.type !== CASE_ATTACHMENT_SAVED_OBJECT + ); + + if (legacyComments.length > 0) { + await authorization.ensureAuthorized({ + operation: Operations.deleteAllComments, + entities: legacyComments.map((comment) => ({ + owner: comment.attributes.owner, + id: comment.id, + })), + }); + } await attachmentService.bulkDelete({ attachmentIds: comments.saved_objects.map((so) => so.id), @@ -62,19 +68,23 @@ export async function deleteAll( user, }); - await userActionService.creator.bulkCreateAttachmentDeletion({ - caseId: caseID, - attachments: comments.saved_objects.map((comment) => ({ - id: comment.id, - owner: comment.attributes.owner, - attachment: comment.attributes, - })), - user, - }); - - const attachments = comments.saved_objects.map((comment) => comment.attributes); + if (legacyComments.length > 0) { + await userActionService.creator.bulkCreateAttachmentDeletion({ + caseId: caseID, + attachments: legacyComments.map((comment) => ({ + id: comment.id, + owner: comment.attributes.owner, + attachment: comment.attributes, + })), + user, + }); - await handleAlerts({ alertsService, attachments, caseId: caseID }); + await handleAlerts({ + alertsService, + attachments: legacyComments.map((comment) => comment.attributes), + caseId: caseID, + }); + } } catch (error) { throw createCaseError({ message: `Failed to delete all comments case id: ${caseID}: ${error}`, @@ -107,10 +117,14 @@ export async function deleteComment( throw Boom.notFound(`This comment ${attachmentID} does not exist anymore.`); } - await authorization.ensureAuthorized({ - entities: [{ owner: attachment.attributes.owner, id: attachment.id }], - operation: Operations.deleteComment, - }); + const isUnifiedAttachment = attachment.type === CASE_ATTACHMENT_SAVED_OBJECT; + + if (!isUnifiedAttachment) { + await authorization.ensureAuthorized({ + entities: [{ owner: attachment.attributes.owner, id: attachment.id }], + operation: Operations.deleteComment, + }); + } const type = CASE_SAVED_OBJECT; const id = caseID; @@ -132,24 +146,23 @@ export async function deleteComment( user, }); - // we only want to store the fields related to the original request of the attachment, not fields like - // created_at etc. So we'll use the decode to strip off the other fields. This is necessary because we don't know - // what type of attachment this is. Depending on the type it could have various fields. - const attachmentRequestAttributes = decodeOrThrow(AttachmentRequestRt)(attachment.attributes); - - await userActionService.creator.createUserAction({ - userAction: { - type: UserActionTypes.comment, - action: UserActionActions.delete, - caseId: id, - attachmentId: attachmentID, - payload: { attachment: attachmentRequestAttributes }, - user, - owner: attachment.attributes.owner, - }, - }); - - await handleAlerts({ alertsService, attachments: [attachment.attributes], caseId: id }); + if (!isUnifiedAttachment) { + const attachmentRequestAttributes = decodeOrThrow(AttachmentRequestRt)(attachment.attributes); + + await userActionService.creator.createUserAction({ + userAction: { + type: UserActionTypes.comment, + action: UserActionActions.delete, + caseId: id, + attachmentId: attachmentID, + payload: { attachment: attachmentRequestAttributes }, + user, + owner: attachment.attributes.owner, + }, + }); + + await handleAlerts({ alertsService, attachments: [attachment.attributes], caseId: id }); + } } catch (error) { throw createCaseError({ message: `Failed to delete comment: ${caseID} comment id: ${attachmentID}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts index 307e7beca8faf..76ada62502b97 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts @@ -232,11 +232,15 @@ export async function getAll( }, }); + const legacyComments = comments.saved_objects.filter( + (so) => so.type === CASE_COMMENT_SAVED_OBJECT + ); + ensureSavedObjectsAreAuthorized( - comments.saved_objects.map((comment) => ({ id: comment.id, owner: comment.attributes.owner })) + legacyComments.map((comment) => ({ id: comment.id, owner: comment.attributes.owner })) ); - const res = flattenCommentSavedObjects(comments.saved_objects); + const res = flattenCommentSavedObjects(legacyComments); return decodeOrThrow(AttachmentsRt)(res); } catch (error) { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts index 17d104a49b10a..4595f3ad3674a 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts @@ -43,6 +43,7 @@ import type { CaseTransformedAttributes, } from '../../common/types/case'; import { CaseRt } from '../../../common/types/domain'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; /** * Parameters for finding cases IDs using an alert ID @@ -291,14 +292,21 @@ export const resolve = async ( }, }); + const legacyComments = { + ...theComments, + saved_objects: theComments.saved_objects.filter( + (so) => so.type === CASE_COMMENT_SAVED_OBJECT + ), + }; + const res = { ...resolveData, case: flattenCaseSavedObject({ savedObject: resolvedSavedObject, - comments: theComments.saved_objects, + comments: legacyComments.saved_objects, totalComment: theComments.total, - totalEvents: countEventsForID({ comments: theComments }), - totalAlerts: countAlertsForID({ comments: theComments, id: resolvedSavedObject.id }), + totalEvents: countEventsForID({ comments: legacyComments }), + totalAlerts: countAlertsForID({ comments: legacyComments, id: resolvedSavedObject.id }), }), }; diff --git a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts index 2f9e4d580457d..2687a8617559a 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts @@ -32,7 +32,11 @@ import { AttachmentType, } from '../../../common/types/domain'; -import { CASE_SAVED_OBJECT, MAX_DOCS_PER_PAGE } from '../../../common/constants'; +import { + CASE_SAVED_OBJECT, + CASE_COMMENT_SAVED_OBJECT, + MAX_DOCS_PER_PAGE, +} from '../../../common/constants'; import type { CasesClientArgs } from '../../client'; import type { RefreshSetting } from '../../services/types'; import { createCaseError } from '../error'; @@ -148,6 +152,7 @@ export class CaseCommentModel { updated_by: this.params.user, }, options, + owner, }), this.partialUpdateCaseWithAttachmentDataSkipRefresh({ date: updatedAt }), ]); @@ -263,10 +268,12 @@ export class CaseCommentModel { createdDate, commentReq, id, + owner, }: { createdDate: string; commentReq: AttachmentRequestV2; id: string; + owner: string; }): Promise { try { await this.validateCreateCommentRequest([commentReq]); @@ -291,6 +298,7 @@ export class CaseCommentModel { references, id, refresh: true, + owner, }); const commentableCase = await this.partialUpdateCaseWithAttachmentDataSkipRefresh({ @@ -301,10 +309,10 @@ export class CaseCommentModel { isLegacyAttachmentRequest(attachment) ? [ commentableCase.handleAlertComments([attachment]), - this.createCommentUserAction(comment, attachment), + this.createLegacyCommentUserAction(comment, attachment), ] : // TO-DO: handle alert comments for unified attachments - [this.createCommentUserAction(comment, attachment)] + [this.createUnifiedCommentUserAction(comment, attachment, owner)] ); return commentableCase; @@ -422,7 +430,13 @@ export class CaseCommentModel { } } - if (req.some((attachment) => attachment.owner !== this.caseInfo.attributes.owner)) { + if ( + req.some( + (attachment) => + isLegacyAttachmentRequest(attachment) && + attachment.owner !== this.caseInfo.attributes.owner + ) + ) { throw Boom.badRequest('The owner field of the comment must match the case'); } @@ -504,9 +518,9 @@ export class CaseCommentModel { }); } - private async createCommentUserAction( + private async createLegacyCommentUserAction( comment: SavedObject, - req: AttachmentRequestV2 + req: AttachmentRequest ) { await this.params.services.userActionService.creator.createUserAction({ userAction: { @@ -524,19 +538,40 @@ export class CaseCommentModel { } private async bulkCreateCommentUserAction( - attachments: Array<{ id: string } & AttachmentRequestV2> + attachments: Array<{ id: string } & AttachmentRequestV2>, + owner: string ) { await this.params.services.userActionService.creator.bulkCreateAttachmentCreation({ caseId: this.caseInfo.id, attachments: attachments.map(({ id, ...attachment }) => ({ id, - owner: attachment.owner, + owner: isLegacyAttachmentRequest(attachment) ? attachment.owner : owner, attachment, })), user: this.params.user, }); } + private async createUnifiedCommentUserAction( + comment: SavedObject, + req: UnifiedAttachmentPayload, + owner: string + ) { + await this.params.services.userActionService.creator.createUserAction({ + userAction: { + type: UserActionTypes.comment, + action: UserActionActions.create, + caseId: this.caseInfo.id, + attachmentId: comment.id, + payload: { + attachment: req, + }, + user: this.params.user, + owner, + }, + }); + } + private formatForEncoding(totalComment: number) { return { id: this.caseInfo.id, @@ -560,8 +595,12 @@ export class CaseCommentModel { const totalAlerts = countAlertsForID({ comments, id: this.caseInfo.id }) ?? 0; const totalEvents = countEventsForID({ comments }) ?? 0; + const legacyComments = comments.saved_objects.filter( + (so) => so.type === CASE_COMMENT_SAVED_OBJECT + ); + const caseResponse = { - comments: flattenCommentSavedObjects(comments.saved_objects), + comments: flattenCommentSavedObjects(legacyComments), totalAlerts, totalEvents, ...this.formatForEncoding(comments.total), @@ -579,8 +618,10 @@ export class CaseCommentModel { public async bulkCreate({ attachments, + owner, }: { attachments: CommentRequestWithId; + owner: string; }): Promise { try { await this.validateCreateCommentRequest(attachments); @@ -606,6 +647,7 @@ export class CaseCommentModel { }; }), refresh: true, + owner, }); const commentableCase = await this.partialUpdateCaseWithAttachmentDataSkipRefresh({ @@ -622,7 +664,7 @@ export class CaseCommentModel { await Promise.all([ commentableCase.handleAlertComments(attachmentsWithoutErrors), - this.bulkCreateCommentUserAction(attachmentsWithoutErrors), + this.bulkCreateCommentUserAction(attachmentsWithoutErrors, owner), ]); return commentableCase; diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts index c07dfd8789400..d799cacd94497 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts @@ -33,6 +33,8 @@ import type { ConfigType } from '../../config'; const createAttachmentServiceConfig = (attachmentsEnabled = false): ConfigType => ({ attachments: { enabled: attachmentsEnabled } } as ConfigType); +const owner = SECURITY_SOLUTION_OWNER; + describe('AttachmentService', () => { const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const mockLogger = loggerMock.create(); @@ -59,6 +61,7 @@ describe('AttachmentService', () => { attributes: createUserAttachment().attributes, references: [], id: '1', + owner, }) ).resolves.not.toThrow(); }); @@ -70,6 +73,7 @@ describe('AttachmentService', () => { attributes: createUserAttachment().attributes, references: [], id: '1', + owner, }); expect(res).toStrictEqual(createUserAttachment()); @@ -86,6 +90,7 @@ describe('AttachmentService', () => { attributes: createUserAttachment().attributes, references: [], id: '1', + owner, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` @@ -103,6 +108,7 @@ describe('AttachmentService', () => { attributes: invalidAttachment.attributes, references: [], id: '1', + owner, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\",Invalid value \\"undefined\\" supplied to \\"attachmentId\\",Invalid value \\"undefined\\" supplied to \\"data\\""` @@ -117,6 +123,7 @@ describe('AttachmentService', () => { attributes: { ...createUserAttachment().attributes, foo: 'bar' }, references: [], id: '1', + owner, }); const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1]; @@ -137,6 +144,7 @@ describe('AttachmentService', () => { attachments: [ { attributes: createUserAttachment().attributes, references: [], id: '1' }, ], + owner, }) ).resolves.not.toThrow(); }); @@ -156,6 +164,7 @@ describe('AttachmentService', () => { { attributes: createUserAttachment().attributes, references: [], id: '1' }, { attributes: createUserAttachment().attributes, references: [], id: '1' }, ], + owner, }); expect(res).toStrictEqual({ saved_objects: [errorResponseObj, createUserAttachment()] }); @@ -168,6 +177,7 @@ describe('AttachmentService', () => { const res = await service.bulkCreate({ attachments: [{ attributes: createUserAttachment().attributes, references: [], id: '1' }], + owner, }); expect(res).toStrictEqual({ saved_objects: [createUserAttachment()] }); @@ -186,6 +196,8 @@ describe('AttachmentService', () => { attachments: [ { attributes: createUserAttachment().attributes, references: [], id: '1' }, ], + + owner, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` @@ -203,6 +215,7 @@ describe('AttachmentService', () => { await expect( service.bulkCreate({ attachments: [{ attributes: invalidAttachment.attributes, references: [], id: '1' }], + owner, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\",Invalid value \\"undefined\\" supplied to \\"attachmentId\\",Invalid value \\"undefined\\" supplied to \\"data\\""` @@ -223,6 +236,7 @@ describe('AttachmentService', () => { id: '1', }, ], + owner, }); const persistedAttributes = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0]; @@ -242,7 +256,6 @@ describe('AttachmentService', () => { const unifiedAttrs = { type: 'comment', data: { content: 'hello' }, - owner: SECURITY_SOLUTION_OWNER, created_at: '2024-01-01T00:00:00.000Z', created_by: { username: 'u', full_name: null, email: null }, pushed_at: null, @@ -261,6 +274,7 @@ describe('AttachmentService', () => { attributes: unifiedAttrs, references: [], id: '1', + owner, }); expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( @@ -277,6 +291,7 @@ describe('AttachmentService', () => { attributes: createUserAttachment().attributes, references: [], id: '1', + owner, }); expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( @@ -296,7 +311,6 @@ describe('AttachmentService', () => { const unifiedAttrs = { type: 'comment', data: { content: 'hi' }, - owner: SECURITY_SOLUTION_OWNER, created_at: '2024-01-01T00:00:00.000Z', created_by: { username: 'u', full_name: null, email: null }, pushed_at: null, @@ -313,6 +327,7 @@ describe('AttachmentService', () => { await serviceWithFlagOn.bulkCreate({ attachments: [{ attributes: unifiedAttrs, references: [], id: '1' }], refresh: false, + owner, }); expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -334,6 +349,7 @@ describe('AttachmentService', () => { await service.bulkCreate({ attachments: [{ attributes: createUserAttachment().attributes, references: [], id: '1' }], refresh: false, + owner, }); expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith( @@ -371,6 +387,7 @@ describe('AttachmentService', () => { attachmentId: '1', updatedAttributes: persistableStateAttachment, options: { references: [] }, + owner, }); expect(res).toEqual({ ...soClientRes, attributes: persistableStateAttachmentAttributes }); @@ -386,6 +403,7 @@ describe('AttachmentService', () => { attachmentId: '1', updatedAttributes: externalReferenceAttachmentSO, options: { references: [] }, + owner, }); expect(res).toEqual({ ...soClientRes, attributes: externalReferenceAttachmentSOAttributes }); @@ -401,6 +419,7 @@ describe('AttachmentService', () => { attachmentId: '1', updatedAttributes: externalReferenceAttachmentESAttributes, options: { references: [] }, + owner, }); expect(res).toEqual({ ...soClientRes, attributes: externalReferenceAttachmentESAttributes }); @@ -414,6 +433,7 @@ describe('AttachmentService', () => { service.update({ updatedAttributes: createUserAttachment().attributes, attachmentId: '1', + owner, }) ).resolves.not.toThrow(); }); @@ -424,6 +444,7 @@ describe('AttachmentService', () => { const res = await service.update({ updatedAttributes: createUserAttachment().attributes, attachmentId: '1', + owner, }); expect(res).toStrictEqual(createUserAttachment()); @@ -518,6 +539,7 @@ describe('AttachmentService', () => { options: { references: [] }, }, ], + owner, }); expect(res).toEqual({ @@ -538,7 +560,7 @@ describe('AttachmentService', () => { const updatedAttributes = createUserAttachment().attributes; await expect( - service.bulkUpdate({ comments: [{ attachmentId: '1', updatedAttributes }] }) + service.bulkUpdate({ comments: [{ attachmentId: '1', updatedAttributes }], owner }) ).resolves.not.toThrow(); }); @@ -557,6 +579,7 @@ describe('AttachmentService', () => { { attachmentId: '1', updatedAttributes: userAttachment.attributes }, { attachmentId: '1', updatedAttributes: userAttachment.attributes }, ], + owner, }); expect(res).toStrictEqual({ saved_objects: [errorResponseObj, createUserAttachment()] }); @@ -571,6 +594,7 @@ describe('AttachmentService', () => { const res = await service.bulkUpdate({ comments: [{ attachmentId: '1', updatedAttributes }], + owner, }); expect(res).toStrictEqual({ saved_objects: [createUserAttachment()] }); @@ -587,7 +611,7 @@ describe('AttachmentService', () => { const updatedAttributes = createAlertAttachment().attributes; await expect( - service.bulkUpdate({ comments: [{ attachmentId: '1', updatedAttributes }] }) + service.bulkUpdate({ comments: [{ attachmentId: '1', updatedAttributes }], owner }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid attributes: expected attributes.rule.name for alert attachments"` ); @@ -609,6 +633,8 @@ describe('AttachmentService', () => { attachmentId: '1', }, ], + + owner, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid attributes: expected attributes.rule.name for alert attachments"` @@ -690,7 +716,7 @@ describe('AttachmentService', () => { ); await expect(service.find({})).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\",Invalid value \\"undefined\\" supplied to \\"attachmentId\\",Invalid value \\"undefined\\" supplied to \\"data\\""` ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts index 2d25b8aafcdc1..8fd0662b627e6 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts @@ -261,7 +261,8 @@ export class AttachmentService { references, id, refresh, - }: CreateAttachmentArgs): Promise< + owner, + }: CreateAttachmentArgs & { owner?: string }): Promise< AttachmentSavedObjectTransformed | UnifiedAttachmentSavedObjectTransformed > { try { @@ -290,7 +291,7 @@ export class AttachmentService { return Object.assign(unifiedAttachment, { attributes: validatedAttributes }); } - const legacyAttributes = transformer.toLegacySchema(decodedAttributes); + const legacyAttributes = transformer.toLegacySchema(decodedAttributes, owner); const { attributes: extractedAttributes, references: extractedReferences } = extractAttachmentSORefsFromAttributes( legacyAttributes, @@ -328,7 +329,10 @@ export class AttachmentService { public async bulkCreate({ attachments, refresh, - }: BulkCreateAttachments): Promise> { + owner, + }: BulkCreateAttachments & { owner?: string }): Promise< + SavedObjectsBulkResponse + > { try { this.context.log.debug(`Attempting to bulk create attachments`); @@ -367,7 +371,7 @@ export class AttachmentService { const transformer = getAttachmentTypeTransformers( getAttachmentTypeFromAttributes(decodedAttributes) ); - const attributesToWrite = transformer.toLegacySchema(decodedAttributes); + const attributesToWrite = transformer.toLegacySchema(decodedAttributes, owner); const { attributes: extractedAttributes, references: extractedReferences } = extractAttachmentSORefsFromAttributes( attributesToWrite, @@ -428,7 +432,10 @@ export class AttachmentService { attachmentId, updatedAttributes, options, - }: UpdateAttachmentArgs): Promise> { + owner, + }: UpdateAttachmentArgs & { owner?: string }): Promise< + SavedObjectsUpdateResponse + > { try { this.context.log.debug(`Attempting to UPDATE attachment ${attachmentId}`); @@ -456,7 +463,7 @@ export class AttachmentService { return Object.assign(res, { attributes: decodedAttributes }); } - const legacyAttributes = transformer.toLegacySchema(decodedAttributes); + const legacyAttributes = transformer.toLegacySchema(decodedAttributes, owner); const { attributes: extractedAttributes, references: extractedReferences, @@ -507,7 +514,10 @@ export class AttachmentService { public async bulkUpdate({ comments, refresh, - }: BulkUpdateAttachmentArgs): Promise> { + owner, + }: BulkUpdateAttachmentArgs & { owner?: string }): Promise< + SavedObjectsBulkUpdateResponse + > { try { this.context.log.debug( `Attempting to UPDATE attachments ${comments.map((c) => c.attachmentId).join(', ')}` @@ -536,7 +546,7 @@ export class AttachmentService { }), { refresh } ); - return this.transformAndDecodeBulkUpdateResponse(res, comments); + return this.transformAndDecodeBulkUpdateResponse(res, comments, undefined); } const res = @@ -549,7 +559,7 @@ export class AttachmentService { const transformer = getAttachmentTypeTransformers( getAttachmentTypeFromAttributes(decodedAttributes) ); - const legacyAttributes = transformer.toLegacySchema(decodedAttributes); + const legacyAttributes = transformer.toLegacySchema(decodedAttributes, owner); const { attributes: extractedAttributes, references: extractedReferences, @@ -578,7 +588,7 @@ export class AttachmentService { { refresh } ); - return this.transformAndDecodeBulkUpdateResponse(res, comments); + return this.transformAndDecodeBulkUpdateResponse(res, comments, owner); } catch (error) { this.context.log.error( `Error on UPDATE attachments ${comments.map((c) => c.attachmentId).join(', ')}: ${error}` @@ -591,7 +601,8 @@ export class AttachmentService { res: SavedObjectsBulkUpdateResponse< AttachmentPersistedAttributes | UnifiedAttachmentPersistedAttributes >, - comments: UpdateArgs[] + comments: UpdateArgs[], + owner?: string ): SavedObjectsBulkUpdateResponse { const validatedAttachments: Array< SavedObjectsUpdateResponse @@ -617,7 +628,7 @@ export class AttachmentService { const transformer = getAttachmentTypeTransformers( getAttachmentTypeFromAttributes(decodedAttributes) ); - const legacyAttributes = transformer.toLegacySchema(decodedAttributes); + const legacyAttributes = transformer.toLegacySchema(decodedAttributes, owner); const transformedAttachment = injectAttachmentSOAttributesFromRefsForPatch( legacyAttributes, attachment, @@ -647,33 +658,48 @@ export class AttachmentService { }): Promise> { try { this.context.log.debug(`Attempting to find comments`); - const res = - await this.context.unsecuredSavedObjectsClient.find({ - sortField: defaultSortField, - ...options, - type: CASE_COMMENT_SAVED_OBJECT, - }); + + const savedObjectType = getAttachmentSavedObjectType(this.context.config); + const types: string[] = + savedObjectType === CASE_ATTACHMENT_SAVED_OBJECT + ? [CASE_ATTACHMENT_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT] + : [CASE_COMMENT_SAVED_OBJECT]; + + const res = await this.context.unsecuredSavedObjectsClient.find< + AttachmentPersistedAttributes | UnifiedAttachmentAttributes + >({ + sortField: defaultSortField, + ...options, + type: types, + }); const validatedAttachments: Array> = []; for (const so of res.saved_objects) { - const transformedAttachment = injectAttachmentSOAttributesFromRefs( - so, - this.context.persistableStateAttachmentTypeRegistry - // casting here because injectAttachmentSOAttributesFromRefs returns a SavedObject but we need a SavedObjectsFindResult - // which has the score in it. The score is returned but the type is not correct - ) as SavedObjectsFindResult; - - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( - transformedAttachment.attributes - ); + if (so.type === CASE_ATTACHMENT_SAVED_OBJECT) { + const validatedAttributes = decodeOrThrow(UnifiedAttachmentAttributesRt)(so.attributes); + validatedAttachments.push( + Object.assign(so, { + attributes: validatedAttributes, + }) as SavedObjectsFindResult + ); + } else { + const transformedAttachment = injectAttachmentSOAttributesFromRefs( + so as SavedObjectsFindResult, + this.context.persistableStateAttachmentTypeRegistry + ) as SavedObjectsFindResult; + + const validatedAttributes = decodeOrThrow(AttachmentAttributesRtV2)( + transformedAttachment.attributes + ); - validatedAttachments.push( - Object.assign(transformedAttachment, { - attributes: validatedAttributes, - }) - ); + validatedAttachments.push( + Object.assign(transformedAttachment, { + attributes: validatedAttributes, + }) + ); + } } return Object.assign(res, { saved_objects: validatedAttachments }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts index 00de9f41714d2..ca722a3b3c64c 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts @@ -21,6 +21,7 @@ import type { } from '../../../common/types/attachments_v1'; import { AttachmentTransformedAttributesRt } from '../../../common/types/attachments_v1'; import { + CASE_ATTACHMENT_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, MAX_ALERTS_PER_CASE, @@ -47,6 +48,9 @@ import { import { partitionByCaseAssociation } from '../../../common/partitioning'; import type { AttachmentSavedObject } from '../../../common/types'; import { getCaseReferenceId } from '../../../common/references'; +import { getAttachmentSavedObjectType } from '../../../common/attachments'; +import { UnifiedAttachmentAttributesRt } from '../../../../common/types/domain/attachment/v2'; +import type { UnifiedAttachmentAttributes } from '../../../common/types/attachments_v2'; export class AttachmentGetter { constructor(private readonly context: ServiceContext) {} @@ -59,6 +63,37 @@ export class AttachmentGetter { `Attempting to retrieve attachments with ids: ${attachmentIds.join()}` ); + const savedObjectType = getAttachmentSavedObjectType(this.context.config); + + if (savedObjectType === CASE_ATTACHMENT_SAVED_OBJECT) { + const [newTypeResponse, legacyResponse] = await Promise.all([ + this.context.unsecuredSavedObjectsClient.bulkGet( + attachmentIds.map((id) => ({ id, type: CASE_ATTACHMENT_SAVED_OBJECT })) + ), + this.context.unsecuredSavedObjectsClient.bulkGet( + attachmentIds.map((id) => ({ id, type: CASE_COMMENT_SAVED_OBJECT })) + ), + ]); + + const merged: Array> = []; + for (let i = 0; i < attachmentIds.length; i++) { + const newSo = newTypeResponse.saved_objects[i]; + const legacySo = legacyResponse.saved_objects[i]; + + if (!isSOError(newSo)) { + merged.push(newSo as unknown as SavedObject); + } else if (!isSOError(legacySo)) { + merged.push(legacySo); + } else { + merged.push(legacySo); + } + } + + return this.transformAndDecodeBulkGetResponse( + Object.assign(legacyResponse, { saved_objects: merged }) + ); + } + const response = await this.context.unsecuredSavedObjectsClient.bulkGet( attachmentIds.map((id) => ({ id, type: CASE_COMMENT_SAVED_OBJECT })) @@ -80,10 +115,14 @@ export class AttachmentGetter { for (const so of response.saved_objects) { if (isSOError(so)) { - // Forcing the type here even though it is an error. The caller is responsible for - // determining what to do with the errors - // TODO: we should fix the return type of this bulkGet so that it can return errors validatedAttachments.push(so as AttachmentSavedObjectTransformed); + } else if (so.type === CASE_ATTACHMENT_SAVED_OBJECT) { + const validatedAttributes = decodeOrThrow(UnifiedAttachmentAttributesRt)(so.attributes); + validatedAttachments.push( + Object.assign(so, { + attributes: validatedAttributes, + }) as unknown as AttachmentSavedObjectTransformed + ); } else { const transformedAttachment = injectAttachmentAttributesAndHandleErrors( so, @@ -271,13 +310,39 @@ export class AttachmentGetter { public async get({ attachmentId }: GetAttachmentArgs): Promise { try { this.context.log.debug(`Attempting to GET attachment ${attachmentId}`); - const res = await this.context.unsecuredSavedObjectsClient.get( - CASE_COMMENT_SAVED_OBJECT, - attachmentId - ); + + const savedObjectType = getAttachmentSavedObjectType(this.context.config); + + let res: SavedObject; + + if (savedObjectType === CASE_ATTACHMENT_SAVED_OBJECT) { + try { + res = await this.context.unsecuredSavedObjectsClient.get( + CASE_ATTACHMENT_SAVED_OBJECT, + attachmentId + ); + } catch { + res = await this.context.unsecuredSavedObjectsClient.get( + CASE_COMMENT_SAVED_OBJECT, + attachmentId + ); + } + } else { + res = await this.context.unsecuredSavedObjectsClient.get( + CASE_COMMENT_SAVED_OBJECT, + attachmentId + ); + } + + if (res.type === CASE_ATTACHMENT_SAVED_OBJECT) { + const validatedAttributes = decodeOrThrow(UnifiedAttachmentAttributesRt)(res.attributes); + return Object.assign(res, { + attributes: validatedAttributes, + }) as unknown as AttachmentSavedObjectTransformed; + } const transformedAttachment = injectAttachmentSOAttributesFromRefs( - res, + res as SavedObject, this.context.persistableStateAttachmentTypeRegistry );