mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cases] Add bulk get attachments API (#149269)
This PR adds a new bulk get attachments API. ``` POST internal/cases/<case_id>/attachments/_bulk_get { "ids": ["02441860-9b66-11ed-a8df-f1edb375c327", "2"] } ``` <details><summary>Example request and response</summary> Request ``` POST http://localhost:5601/internal/cases/attachments/_bulk_get { "ids": ["283a4600-9cfd-11ed-9e3d-c96d764b0e39", "2", "382e97f0-9cfd-11ed-9e3d-c96d764b0e39"] } ``` Response ``` { "attachments": [ { "id": "283a4600-9cfd-11ed-9e3d-c96d764b0e39", "version": "WzI2MiwxXQ==", "comment": "Stack comment", "type": "user", "owner": "cases", "created_at": "2023-01-25T22:11:03.398Z", "created_by": { "email": null, "full_name": null, "username": "elastic", "profile_uid": "u_mGBROF_q5bmFCATbLXAcCwKa0k8JvONAwSruelyKA5E_0" }, "pushed_at": null, "pushed_by": null, "updated_at": null, "updated_by": null } ], "errors": [ { "error": "Not Found", "message": "Saved object [cases-comments/2] not found", "status": 404, "attachmentId": "2" }, { "error": "Bad Request", "message": "Attachment is not attached to case id=248d6aa0-9cfd-11ed-9e3d-c96d764b0e39", "status": 400, "attachmentId": "382e97f0-9cfd-11ed-9e3d-c96d764b0e39" } ] } ``` </details> <details><summary>Unauthorized example response</summary> ``` { "attachments": [], "errors": [ { "error": "Forbidden", "message": "Unauthorized to access attachment with owner: \"securitySolution\"", "status": 403, "attachmentId": "382e97f0-9cfd-11ed-9e3d-c96d764b0e39" } ] } ``` </details> ## Notable changes - Created a new internal route for retrieving attachments - Refactored the attachments service to take the saved object client in the constructor instead of each method - Refactored attachments service by moving the get style operations to their own class - Refactored the integration utilities file to move the attachment operations to their own file - The API will return a 400 if more than 10k ids are requested --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
662aa234d5
commit
bd8e62e45c
42 changed files with 1631 additions and 522 deletions
|
@ -380,6 +380,10 @@ Refer to the corresponding {es} logs for potential write errors.
|
|||
| `success` | User has accessed a case comment.
|
||||
| `failure` | User is not authorized to access a case comment.
|
||||
|
||||
.2+| `case_comment_bulk_get`
|
||||
| `success` | User has accessed multiple case comments.
|
||||
| `failure` | User is not authorized to access multiple case comments.
|
||||
|
||||
.2+| `case_comment_get_all`
|
||||
| `success` | User has accessed case comments.
|
||||
| `failure` | User is not authorized to access case comments.
|
||||
|
@ -404,7 +408,6 @@ Refer to the corresponding {es} logs for potential write errors.
|
|||
| `success` | User has accessed the user activity of a case.
|
||||
| `failure` | User is not authorized to access the user activity of a case.
|
||||
|
||||
|
||||
.2+| `case_user_actions_find`
|
||||
| `success` | User has accessed the user activity of a case as part of a search operation.
|
||||
| `failure` | User is not authorized to access the user activity of a case.
|
||||
|
|
|
@ -279,6 +279,22 @@ export const FindQueryParamsRt = rt.partial({
|
|||
|
||||
export const BulkCreateCommentRequestRt = rt.array(CommentRequestRt);
|
||||
|
||||
export const BulkGetAttachmentsRequestRt = rt.type({
|
||||
ids: rt.array(rt.string),
|
||||
});
|
||||
|
||||
export const BulkGetAttachmentsResponseRt = rt.type({
|
||||
attachments: AllCommentsResponseRt,
|
||||
errors: rt.array(
|
||||
rt.type({
|
||||
error: rt.string,
|
||||
message: rt.string,
|
||||
status: rt.union([rt.undefined, rt.number]),
|
||||
attachmentId: rt.string,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type FindQueryParams = rt.TypeOf<typeof FindQueryParamsRt>;
|
||||
export type AttributesTypeActions = rt.TypeOf<typeof AttributesTypeActionsRt>;
|
||||
export type AttributesTypeAlerts = rt.TypeOf<typeof AttributesTypeAlertsRt>;
|
||||
|
@ -317,3 +333,5 @@ export type CommentRequestExternalReferenceType = rt.TypeOf<typeof ExternalRefer
|
|||
export type CommentRequestExternalReferenceSOType = rt.TypeOf<typeof ExternalReferenceSORt>;
|
||||
export type CommentRequestExternalReferenceNoSOType = rt.TypeOf<typeof ExternalReferenceNoSORt>;
|
||||
export type CommentRequestPersistableStateType = rt.TypeOf<typeof PersistableStateAttachmentRt>;
|
||||
export type BulkGetAttachmentsResponse = rt.TypeOf<typeof BulkGetAttachmentsResponseRt>;
|
||||
export type BulkGetAttachmentsRequest = rt.TypeOf<typeof BulkGetAttachmentsRequestRt>;
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
CASE_ALERTS_URL,
|
||||
CASE_COMMENT_DELETE_URL,
|
||||
CASE_FIND_USER_ACTIONS_URL,
|
||||
INTERNAL_BULK_GET_ATTACHMENTS_URL,
|
||||
} from '../constants';
|
||||
|
||||
export const getCaseDetailsUrl = (id: string): string => {
|
||||
|
@ -57,3 +58,7 @@ export const getCaseConfigurationDetailsUrl = (configureID: string): string => {
|
|||
export const getCasesFromAlertsUrl = (alertId: string): string => {
|
||||
return CASE_ALERTS_URL.replace('{alert_id}', alertId);
|
||||
};
|
||||
|
||||
export const getCaseBulkGetAttachmentsUrl = (id: string): string => {
|
||||
return INTERNAL_BULK_GET_ATTACHMENTS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
|
|
@ -88,6 +88,8 @@ export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}` as cons
|
|||
export const CASES_INTERNAL_URL = '/internal/cases' as const;
|
||||
export const INTERNAL_BULK_CREATE_ATTACHMENTS_URL =
|
||||
`${CASES_INTERNAL_URL}/{case_id}/attachments/_bulk_create` as const;
|
||||
export const INTERNAL_BULK_GET_ATTACHMENTS_URL =
|
||||
`${CASES_INTERNAL_URL}/{case_id}/attachments/_bulk_get` as const;
|
||||
export const INTERNAL_SUGGEST_USER_PROFILES_URL =
|
||||
`${CASES_INTERNAL_URL}/_suggest_user_profiles` as const;
|
||||
export const INTERNAL_CONNECTORS_URL = `${CASES_INTERNAL_URL}/{case_id}/_connectors` as const;
|
||||
|
@ -138,6 +140,7 @@ export const OWNER_INFO = {
|
|||
* Searching
|
||||
*/
|
||||
export const MAX_DOCS_PER_PAGE = 10000 as const;
|
||||
export const MAX_BULK_GET_ATTACHMENTS = MAX_DOCS_PER_PAGE;
|
||||
export const MAX_CONCURRENT_SEARCHES = 10 as const;
|
||||
export const MAX_BULK_GET_CASES = 1000 as const;
|
||||
|
||||
|
|
|
@ -84,6 +84,90 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "bulkGetAttachments" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_comment_bulk_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to access cases-comments [id=1] as owner \\"awesome\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "bulkGetAttachments" with an error but no entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_comment_bulk_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to access a comments as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "bulkGetAttachments" without an error but with an entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_comment_bulk_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "5",
|
||||
"type": "cases-comments",
|
||||
},
|
||||
},
|
||||
"message": "User has accessed cases-comments [id=5] as owner \\"super\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "bulkGetAttachments" without an error or entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_comment_bulk_get",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "User has accessed a comments as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "bulkGetCases" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
|
@ -91,6 +91,7 @@ describe('authorization', () => {
|
|||
|
||||
describe('ensureAuthorized', () => {
|
||||
const feature = { id: '1', cases: ['a'] };
|
||||
const checkRequestReturningHasAllAsTrue = jest.fn(async () => ({ hasAllRequested: true }));
|
||||
|
||||
let securityStart: ReturnType<typeof securityMock.createStart>;
|
||||
let featuresStart: jest.Mocked<FeaturesPluginStart>;
|
||||
|
@ -101,7 +102,7 @@ describe('authorization', () => {
|
|||
securityStart = securityMock.createStart();
|
||||
securityStart.authz.mode.useRbacForRequest.mockReturnValue(true);
|
||||
securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(
|
||||
jest.fn(async () => ({ hasAllRequested: true }))
|
||||
checkRequestReturningHasAllAsTrue
|
||||
);
|
||||
|
||||
featuresStart = featuresPluginMock.createStart();
|
||||
|
@ -119,6 +120,34 @@ describe('authorization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls checkRequest with no repeated owners', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const casesGet = securityStart.authz.actions.cases.get as jest.Mock;
|
||||
casesGet.mockImplementation((owner, op) => `${owner}/${op}`);
|
||||
|
||||
try {
|
||||
await auth.ensureAuthorized({
|
||||
entities: [
|
||||
{ id: '1', owner: 'b' },
|
||||
{ id: '2', owner: 'b' },
|
||||
],
|
||||
operation: Operations.createCase,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(checkRequestReturningHasAllAsTrue).toBeCalledTimes(1);
|
||||
expect(checkRequestReturningHasAllAsTrue.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"kibana": Array [
|
||||
"b/createCase",
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error when the owner passed in is not included in the features when security is disabled', async () => {
|
||||
expect.assertions(1);
|
||||
securityStart.authz.mode.useRbacForRequest.mockReturnValue(false);
|
||||
|
@ -133,6 +162,23 @@ describe('authorization', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('throws an error with a single owner when the repeated owners passed in are not included in the features when security is disabled', async () => {
|
||||
expect.assertions(1);
|
||||
securityStart.authz.mode.useRbacForRequest.mockReturnValue(false);
|
||||
|
||||
try {
|
||||
await auth.ensureAuthorized({
|
||||
entities: [
|
||||
{ id: '1', owner: 'b' },
|
||||
{ id: '2', owner: 'b' },
|
||||
],
|
||||
operation: Operations.createCase,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('Unauthorized to create case with owners: "b"');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error when the owner passed in is not included in the features when security undefined', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
|
@ -154,6 +200,30 @@ describe('authorization', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('throws an error with a single owner when the repeated owners passed in are not included in the features when security undefined', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
auth = await Authorization.create({
|
||||
request,
|
||||
spaces: spacesStart,
|
||||
features: featuresStart,
|
||||
auditLogger: new AuthorizationAuditLogger(mockLogger),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
});
|
||||
|
||||
try {
|
||||
await auth.ensureAuthorized({
|
||||
entities: [
|
||||
{ id: '1', owner: 'b' },
|
||||
{ id: '1', owner: 'b' },
|
||||
],
|
||||
operation: Operations.createCase,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('Unauthorized to create case with owners: "b"');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error when the owner passed in is not included in the features when security is enabled', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
|
@ -167,6 +237,22 @@ describe('authorization', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('throws an error with a single owner when the repeated owners passed in are not included in the features when security is enabled', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
try {
|
||||
await auth.ensureAuthorized({
|
||||
entities: [
|
||||
{ id: '1', owner: 'b' },
|
||||
{ id: '2', owner: 'b' },
|
||||
],
|
||||
operation: Operations.createCase,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('Unauthorized to create case with owners: "b"');
|
||||
}
|
||||
});
|
||||
|
||||
it('logs the error thrown when the passed in owner is not one of the features', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
|
@ -254,6 +340,26 @@ describe('authorization', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('throws an error with a single owner listed when the user does not have all the requested privileges', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(
|
||||
jest.fn(async () => ({ hasAllRequested: false }))
|
||||
);
|
||||
|
||||
try {
|
||||
await auth.ensureAuthorized({
|
||||
entities: [
|
||||
{ id: '1', owner: 'a' },
|
||||
{ id: '2', owner: 'a' },
|
||||
],
|
||||
operation: Operations.createCase,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('Unauthorized to create case with owners: "a"');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error when owner does not exist because it was from a disabled plugin', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
|
@ -1038,6 +1144,7 @@ describe('authorization', () => {
|
|||
{ id: '1', attributes: { owner: 'b' }, type: 'test', references: [] },
|
||||
{ id: '2', attributes: { owner: 'c' }, type: 'test', references: [] },
|
||||
],
|
||||
operation: Operations.bulkGetCases,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('Unauthorized to access cases of any owner');
|
||||
|
@ -1115,6 +1222,7 @@ describe('authorization', () => {
|
|||
{ id: '1', attributes: { owner: 'a' }, type: 'test', references: [] },
|
||||
{ id: '2', attributes: { owner: 'b' }, type: 'test', references: [] },
|
||||
],
|
||||
operation: Operations.bulkGetCases,
|
||||
});
|
||||
|
||||
await expect(helpersPromise).resolves.not.toThrow();
|
||||
|
@ -1183,6 +1291,7 @@ describe('authorization', () => {
|
|||
{ id: '2', attributes: { owner: 'b' }, type: 'test', references: [] },
|
||||
{ id: '3', attributes: { owner: 'c' }, type: 'test', references: [] },
|
||||
],
|
||||
operation: Operations.bulkGetCases,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
@ -1305,6 +1414,7 @@ describe('authorization', () => {
|
|||
{ id: '2', attributes: { owner: 'b' }, type: 'test', references: [] },
|
||||
{ id: '3', attributes: { owner: 'c' }, type: 'test', references: [] },
|
||||
],
|
||||
operation: Operations.bulkGetCases,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { Space, SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
|||
import type { AuthFilterHelpers, OwnerEntity } from './types';
|
||||
import { getOwnersFilter, groupByAuthorization } from './utils';
|
||||
import type { OperationDetails } from '.';
|
||||
import { AuthorizationAuditLogger, Operations } from '.';
|
||||
import { AuthorizationAuditLogger } from '.';
|
||||
import { createCaseError } from '../common/error';
|
||||
|
||||
/**
|
||||
|
@ -109,10 +109,9 @@ export class Authorization {
|
|||
operation: OperationDetails;
|
||||
}) {
|
||||
try {
|
||||
await this._ensureAuthorized(
|
||||
entities.map((entity) => entity.owner),
|
||||
operation
|
||||
);
|
||||
const uniqueOwners = Array.from(new Set(entities.map((entity) => entity.owner)));
|
||||
|
||||
await this._ensureAuthorized(uniqueOwners, operation);
|
||||
} catch (error) {
|
||||
this.logSavedObjects({ entities, operation, error });
|
||||
throw error;
|
||||
|
@ -128,13 +127,15 @@ export class Authorization {
|
|||
*
|
||||
* @param savedObjects an array of saved objects to be authorized. Each saved objects should contain
|
||||
* an ID and an owner
|
||||
* @param operation the operation that should be authorized
|
||||
*/
|
||||
public async getAndEnsureAuthorizedEntities<T extends { owner: string }>({
|
||||
savedObjects,
|
||||
operation,
|
||||
}: {
|
||||
savedObjects: Array<SavedObject<T>>;
|
||||
operation: OperationDetails;
|
||||
}): Promise<{ authorized: Array<SavedObject<T>>; unauthorized: Array<SavedObject<T>> }> {
|
||||
const operation = Operations.bulkGetCases;
|
||||
const entities = savedObjects.map((so) => ({
|
||||
id: so.id,
|
||||
owner: so.attributes.owner,
|
||||
|
|
|
@ -230,6 +230,14 @@ const AttachmentOperations = {
|
|||
docType: 'comments',
|
||||
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.BulkGetAttachments]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_COMMENT_OPERATION,
|
||||
action: 'case_comment_bulk_get',
|
||||
verbs: accessVerbs,
|
||||
docType: 'comments',
|
||||
savedObjectType: CASE_COMMENT_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.GetAllComments]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_COMMENT_OPERATION,
|
||||
|
|
|
@ -33,6 +33,7 @@ export enum ReadOperations {
|
|||
GetCaseIDsByAlertID = 'getCaseIDsByAlertID',
|
||||
GetCaseStatuses = 'getCaseStatuses',
|
||||
GetComment = 'getComment',
|
||||
BulkGetAttachments = 'bulkGetAttachments',
|
||||
GetAllComments = 'getAllComments',
|
||||
FindComments = 'findComments',
|
||||
GetTags = 'getTags',
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
isCommentRequestTypeExternalReference,
|
||||
isCommentRequestTypePersistableState,
|
||||
} from '../../../common/utils/attachments';
|
||||
import type { CaseResponse, CommentRequest } from '../../../common/api';
|
||||
import type { CaseResponse } from '../../../common/api';
|
||||
import { CommentRequestRt, throwErrors } from '../../../common/api';
|
||||
|
||||
import { CaseCommentModel } from '../../common/models';
|
||||
|
@ -25,20 +25,7 @@ import type { CasesClientArgs } from '..';
|
|||
|
||||
import { decodeCommentRequest } from '../utils';
|
||||
import { Operations } from '../../authorization';
|
||||
|
||||
/**
|
||||
* The arguments needed for creating a new attachment to a case.
|
||||
*/
|
||||
export interface AddArgs {
|
||||
/**
|
||||
* The case ID that this attachment will be associated with
|
||||
*/
|
||||
caseId: string;
|
||||
/**
|
||||
* The attachment values.
|
||||
*/
|
||||
comment: CommentRequest;
|
||||
}
|
||||
import type { AddArgs } from './types';
|
||||
|
||||
/**
|
||||
* Create an attachment to a case.
|
||||
|
|
|
@ -12,7 +12,7 @@ import { identity } from 'fp-ts/lib/function';
|
|||
|
||||
import { SavedObjectsUtils } from '@kbn/core/server';
|
||||
|
||||
import type { BulkCreateCommentRequest, CaseResponse, CommentRequest } from '../../../common/api';
|
||||
import type { CaseResponse, CommentRequest } from '../../../common/api';
|
||||
import { BulkCreateCommentRequestRt, throwErrors } from '../../../common/api';
|
||||
|
||||
import { CaseCommentModel } from '../../common/models';
|
||||
|
@ -22,11 +22,7 @@ import type { CasesClientArgs } from '..';
|
|||
import { decodeCommentRequest } from '../utils';
|
||||
import type { OwnerEntity } from '../../authorization';
|
||||
import { Operations } from '../../authorization';
|
||||
|
||||
export interface BulkCreateArgs {
|
||||
caseId: string;
|
||||
attachments: BulkCreateCommentRequest;
|
||||
}
|
||||
import type { BulkCreateArgs } from './types';
|
||||
|
||||
/**
|
||||
* Create an attachment to a case.
|
||||
|
|
181
x-pack/plugins/cases/server/client/attachments/bulk_get.ts
Normal file
181
x-pack/plugins/cases/server/client/attachments/bulk_get.ts
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 { SavedObject, SavedObjectReference } from '@kbn/core/server';
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
|
||||
import { partition } from 'lodash';
|
||||
import { CASE_SAVED_OBJECT, MAX_BULK_GET_ATTACHMENTS } from '../../../common/constants';
|
||||
import type { BulkGetAttachmentsResponse, CommentAttributes } from '../../../common/api';
|
||||
import {
|
||||
excess,
|
||||
throwErrors,
|
||||
BulkGetAttachmentsResponseRt,
|
||||
BulkGetAttachmentsRequestRt,
|
||||
} from '../../../common/api';
|
||||
import { flattenCommentSavedObjects } from '../../common/utils';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import type { CasesClientArgs, SOWithErrors } from '../types';
|
||||
import { Operations } from '../../authorization';
|
||||
import type { BulkGetArgs } from './types';
|
||||
import type { BulkOptionalAttributes, OptionalAttributes } from '../../services/attachments/types';
|
||||
import { CASE_REF_NAME } from '../../common/constants';
|
||||
import type { CasesClient } from '../client';
|
||||
|
||||
type AttachmentSavedObjectWithErrors = SOWithErrors<CommentAttributes>;
|
||||
|
||||
type AttachmentSavedObject = SavedObject<CommentAttributes>;
|
||||
|
||||
/**
|
||||
* Retrieves multiple attachments by id.
|
||||
*/
|
||||
export async function bulkGet(
|
||||
{ attachmentIDs, caseID }: BulkGetArgs,
|
||||
clientArgs: CasesClientArgs,
|
||||
casesClient: CasesClient
|
||||
): Promise<BulkGetAttachmentsResponse> {
|
||||
const {
|
||||
services: { attachmentService },
|
||||
logger,
|
||||
authorization,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
const request = pipe(
|
||||
excess(BulkGetAttachmentsRequestRt).decode({ ids: attachmentIDs }),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
throwErrorIfIdsExceedTheLimit(request.ids);
|
||||
|
||||
// perform an authorization check for the case
|
||||
await casesClient.cases.resolve({ id: caseID });
|
||||
|
||||
const attachments = await attachmentService.getter.bulkGet(request.ids);
|
||||
|
||||
const { validAttachments, attachmentsWithErrors, invalidAssociationAttachments } =
|
||||
partitionAttachments(caseID, attachments);
|
||||
|
||||
const { authorized: authorizedAttachments, unauthorized: unauthorizedAttachments } =
|
||||
await authorization.getAndEnsureAuthorizedEntities({
|
||||
savedObjects: validAttachments,
|
||||
operation: Operations.bulkGetAttachments,
|
||||
});
|
||||
|
||||
const errors = constructErrors({
|
||||
associationErrors: invalidAssociationAttachments,
|
||||
unauthorizedAttachments,
|
||||
soBulkGetErrors: attachmentsWithErrors,
|
||||
caseId: caseID,
|
||||
});
|
||||
|
||||
return BulkGetAttachmentsResponseRt.encode({
|
||||
attachments: flattenCommentSavedObjects(authorizedAttachments),
|
||||
errors,
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to bulk get attachments for case id: ${caseID}: ${error}`,
|
||||
error,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const throwErrorIfIdsExceedTheLimit = (ids: string[]) => {
|
||||
if (ids.length > MAX_BULK_GET_ATTACHMENTS) {
|
||||
throw Boom.badRequest(
|
||||
`Maximum request limit of ${MAX_BULK_GET_ATTACHMENTS} attachments reached`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface PartitionedAttachments {
|
||||
validAttachments: AttachmentSavedObject[];
|
||||
attachmentsWithErrors: AttachmentSavedObjectWithErrors;
|
||||
invalidAssociationAttachments: AttachmentSavedObject[];
|
||||
}
|
||||
|
||||
const partitionAttachments = (
|
||||
caseId: string,
|
||||
attachments: BulkOptionalAttributes<CommentAttributes>
|
||||
): PartitionedAttachments => {
|
||||
const [attachmentsWithoutErrors, errors] = partitionBySOError(attachments.saved_objects);
|
||||
const [caseAttachments, invalidAssociationAttachments] = partitionByCaseAssociation(
|
||||
caseId,
|
||||
attachmentsWithoutErrors
|
||||
);
|
||||
|
||||
return {
|
||||
validAttachments: caseAttachments,
|
||||
attachmentsWithErrors: errors,
|
||||
invalidAssociationAttachments,
|
||||
};
|
||||
};
|
||||
|
||||
const partitionBySOError = (attachments: Array<OptionalAttributes<CommentAttributes>>) =>
|
||||
partition(
|
||||
attachments,
|
||||
(attachment) => attachment.error == null && attachment.attributes != null
|
||||
) as [AttachmentSavedObject[], AttachmentSavedObjectWithErrors];
|
||||
|
||||
const partitionByCaseAssociation = (caseId: string, attachments: AttachmentSavedObject[]) =>
|
||||
partition(attachments, (attachment) => {
|
||||
const ref = getCaseReference(attachment.references);
|
||||
|
||||
return caseId === ref?.id;
|
||||
});
|
||||
|
||||
const getCaseReference = (references: SavedObjectReference[]): SavedObjectReference | undefined => {
|
||||
return references.find((ref) => ref.name === CASE_REF_NAME && ref.type === CASE_SAVED_OBJECT);
|
||||
};
|
||||
|
||||
const constructErrors = ({
|
||||
caseId,
|
||||
soBulkGetErrors,
|
||||
associationErrors,
|
||||
unauthorizedAttachments,
|
||||
}: {
|
||||
caseId: string;
|
||||
soBulkGetErrors: AttachmentSavedObjectWithErrors;
|
||||
associationErrors: AttachmentSavedObject[];
|
||||
unauthorizedAttachments: AttachmentSavedObject[];
|
||||
}): BulkGetAttachmentsResponse['errors'] => {
|
||||
const errors: BulkGetAttachmentsResponse['errors'] = [];
|
||||
|
||||
for (const soError of soBulkGetErrors) {
|
||||
errors.push({
|
||||
error: soError.error.error,
|
||||
message: soError.error.message,
|
||||
status: soError.error.statusCode,
|
||||
attachmentId: soError.id,
|
||||
});
|
||||
}
|
||||
|
||||
for (const attachment of associationErrors) {
|
||||
errors.push({
|
||||
error: 'Bad Request',
|
||||
message: `Attachment is not attached to case id=${caseId}`,
|
||||
status: 400,
|
||||
attachmentId: attachment.id,
|
||||
});
|
||||
}
|
||||
|
||||
for (const unauthorizedAttachment of unauthorizedAttachments) {
|
||||
errors.push({
|
||||
error: 'Forbidden',
|
||||
message: `Unauthorized to access attachment with owner: "${unauthorizedAttachment.attributes.owner}"`,
|
||||
status: 403,
|
||||
attachmentId: unauthorizedAttachment.id,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
|
@ -5,21 +5,35 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { AlertResponse, CommentResponse } from '../../../common/api';
|
||||
import type {
|
||||
AlertResponse,
|
||||
AllCommentsResponse,
|
||||
BulkGetAttachmentsResponse,
|
||||
CaseResponse,
|
||||
CommentResponse,
|
||||
CommentsResponse,
|
||||
} from '../../../common/api';
|
||||
import type { CasesClient } from '../client';
|
||||
|
||||
import type { CasesClientInternal } from '../client_internal';
|
||||
import type { IAllCommentsResponse, ICaseResponse, ICommentsResponse } from '../typedoc_interfaces';
|
||||
import type { CasesClientArgs } from '../types';
|
||||
import type { AddArgs } from './add';
|
||||
import { addComment } from './add';
|
||||
import type { BulkCreateArgs } from './bulk_create';
|
||||
import type {
|
||||
BulkCreateArgs,
|
||||
AddArgs,
|
||||
DeleteAllArgs,
|
||||
DeleteArgs,
|
||||
FindArgs,
|
||||
GetAllAlertsAttachToCase,
|
||||
GetAllArgs,
|
||||
GetArgs,
|
||||
UpdateArgs,
|
||||
BulkGetArgs,
|
||||
} from './types';
|
||||
import { bulkCreate } from './bulk_create';
|
||||
import type { DeleteAllArgs, DeleteArgs } from './delete';
|
||||
import { deleteAll, deleteComment } from './delete';
|
||||
import type { FindArgs, GetAllAlertsAttachToCase, GetAllArgs, GetArgs } from './get';
|
||||
import { find, get, getAll, getAllAlertsAttachToCase } from './get';
|
||||
import type { UpdateArgs } from './update';
|
||||
import { bulkGet } from './bulk_get';
|
||||
import { update } from './update';
|
||||
|
||||
/**
|
||||
|
@ -29,8 +43,9 @@ export interface AttachmentsSubClient {
|
|||
/**
|
||||
* Adds an attachment to a case.
|
||||
*/
|
||||
add(params: AddArgs): Promise<ICaseResponse>;
|
||||
bulkCreate(params: BulkCreateArgs): Promise<ICaseResponse>;
|
||||
add(params: AddArgs): Promise<CaseResponse>;
|
||||
bulkCreate(params: BulkCreateArgs): Promise<CaseResponse>;
|
||||
bulkGet(params: BulkGetArgs): Promise<BulkGetAttachmentsResponse>;
|
||||
/**
|
||||
* Deletes all attachments associated with a single case.
|
||||
*/
|
||||
|
@ -42,7 +57,7 @@ export interface AttachmentsSubClient {
|
|||
/**
|
||||
* Retrieves all comments matching the search criteria.
|
||||
*/
|
||||
find(findArgs: FindArgs): Promise<ICommentsResponse>;
|
||||
find(findArgs: FindArgs): Promise<CommentsResponse>;
|
||||
/**
|
||||
* Retrieves all alerts attach to a case given a single case ID
|
||||
*/
|
||||
|
@ -50,7 +65,7 @@ export interface AttachmentsSubClient {
|
|||
/**
|
||||
* Gets all attachments for a single case.
|
||||
*/
|
||||
getAll(getAllArgs: GetAllArgs): Promise<IAllCommentsResponse>;
|
||||
getAll(getAllArgs: GetAllArgs): Promise<AllCommentsResponse>;
|
||||
/**
|
||||
* Retrieves a single attachment for a case.
|
||||
*/
|
||||
|
@ -60,7 +75,7 @@ export interface AttachmentsSubClient {
|
|||
*
|
||||
* The request must include all fields for the attachment. Even the fields that are not changing.
|
||||
*/
|
||||
update(updateArgs: UpdateArgs): Promise<ICaseResponse>;
|
||||
update(updateArgs: UpdateArgs): Promise<CaseResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,6 +91,7 @@ export const createAttachmentsSubClient = (
|
|||
const attachmentSubClient: AttachmentsSubClient = {
|
||||
add: (params: AddArgs) => addComment(params, clientArgs),
|
||||
bulkCreate: (params: BulkCreateArgs) => bulkCreate(params, clientArgs),
|
||||
bulkGet: (params) => bulkGet(params, clientArgs, casesClient),
|
||||
deleteAll: (deleteAllArgs: DeleteAllArgs) => deleteAll(deleteAllArgs, clientArgs),
|
||||
delete: (deleteArgs: DeleteArgs) => deleteComment(deleteArgs, clientArgs),
|
||||
find: (findArgs: FindArgs) => find(findArgs, clientArgs),
|
||||
|
|
|
@ -15,30 +15,7 @@ import { CASE_SAVED_OBJECT, MAX_CONCURRENT_SEARCHES } from '../../../common/cons
|
|||
import type { CasesClientArgs } from '../types';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { Operations } from '../../authorization';
|
||||
|
||||
/**
|
||||
* Parameters for deleting all comments of a case.
|
||||
*/
|
||||
export interface DeleteAllArgs {
|
||||
/**
|
||||
* The case ID to delete all attachments for
|
||||
*/
|
||||
caseID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for deleting a single attachment of a case.
|
||||
*/
|
||||
export interface DeleteArgs {
|
||||
/**
|
||||
* The case ID to delete an attachment from
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* The attachment ID to delete
|
||||
*/
|
||||
attachmentID: string;
|
||||
}
|
||||
import type { DeleteAllArgs, DeleteArgs } from './types';
|
||||
|
||||
/**
|
||||
* Delete all comments for a case.
|
||||
|
@ -51,7 +28,6 @@ export async function deleteAll(
|
|||
): Promise<void> {
|
||||
const {
|
||||
user,
|
||||
unsecuredSavedObjectsClient,
|
||||
services: { caseService, attachmentService, userActionService },
|
||||
logger,
|
||||
authorization,
|
||||
|
@ -76,7 +52,6 @@ export async function deleteAll(
|
|||
|
||||
const mapper = async (comment: SavedObject<CommentAttributes>) =>
|
||||
attachmentService.delete({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId: comment.id,
|
||||
refresh: false,
|
||||
});
|
||||
|
@ -115,15 +90,13 @@ export async function deleteComment(
|
|||
) {
|
||||
const {
|
||||
user,
|
||||
unsecuredSavedObjectsClient,
|
||||
services: { attachmentService, userActionService },
|
||||
logger,
|
||||
authorization,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
const myComment = await attachmentService.get({
|
||||
unsecuredSavedObjectsClient,
|
||||
const myComment = await attachmentService.getter.get({
|
||||
attachmentId: attachmentID,
|
||||
});
|
||||
|
||||
|
@ -145,7 +118,6 @@ export async function deleteComment(
|
|||
}
|
||||
|
||||
await attachmentService.delete({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId: attachmentID,
|
||||
refresh: false,
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ import type {
|
|||
AttributesTypeAlerts,
|
||||
CommentResponse,
|
||||
CommentsResponse,
|
||||
FindQueryParams,
|
||||
} from '../../../common/api';
|
||||
import { AllCommentsResponseRt, CommentResponseRt, CommentsResponseRt } from '../../../common/api';
|
||||
import {
|
||||
|
@ -29,48 +28,7 @@ import { combineFilters, stringToKueryNode } from '../utils';
|
|||
import { Operations } from '../../authorization';
|
||||
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
|
||||
import type { CasesClient } from '../client';
|
||||
|
||||
/**
|
||||
* Parameters for finding attachments of a case
|
||||
*/
|
||||
export interface FindArgs {
|
||||
/**
|
||||
* The case ID for finding associated attachments
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* Optional parameters for filtering the returned attachments
|
||||
*/
|
||||
queryParams?: FindQueryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for retrieving all attachments of a case
|
||||
*/
|
||||
export interface GetAllArgs {
|
||||
/**
|
||||
* The case ID to retrieve all attachments for
|
||||
*/
|
||||
caseID: string;
|
||||
}
|
||||
|
||||
export interface GetArgs {
|
||||
/**
|
||||
* The ID of the case to retrieve an attachment from
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* The ID of the attachment to retrieve
|
||||
*/
|
||||
attachmentID: string;
|
||||
}
|
||||
|
||||
export interface GetAllAlertsAttachToCase {
|
||||
/**
|
||||
* The ID of the case to retrieve the alerts from
|
||||
*/
|
||||
caseId: string;
|
||||
}
|
||||
import type { FindArgs, GetAllAlertsAttachToCase, GetAllArgs, GetArgs } from './types';
|
||||
|
||||
const normalizeAlertResponse = (alerts: Array<SavedObject<AttributesTypeAlerts>>): AlertResponse =>
|
||||
alerts.reduce((acc: AlertResponse, alert) => {
|
||||
|
@ -92,8 +50,6 @@ const normalizeAlertResponse = (alerts: Array<SavedObject<AttributesTypeAlerts>>
|
|||
|
||||
/**
|
||||
* Retrieves all alerts attached to a specific case.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export const getAllAlertsAttachToCase = async (
|
||||
{ caseId }: GetAllAlertsAttachToCase,
|
||||
|
@ -101,7 +57,6 @@ export const getAllAlertsAttachToCase = async (
|
|||
casesClient: CasesClient
|
||||
): Promise<AlertResponse> => {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization,
|
||||
services: { attachmentService },
|
||||
logger,
|
||||
|
@ -117,8 +72,7 @@ export const getAllAlertsAttachToCase = async (
|
|||
const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } =
|
||||
await authorization.getAuthorizationFilter(Operations.getAlertsAttachedToCase);
|
||||
|
||||
const alerts = await attachmentService.getAllAlertsAttachToCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
const alerts = await attachmentService.getter.getAllAlertsAttachToCase({
|
||||
caseId: theCase.id,
|
||||
filter: authorizationFilter,
|
||||
});
|
||||
|
@ -142,8 +96,6 @@ export const getAllAlertsAttachToCase = async (
|
|||
|
||||
/**
|
||||
* Retrieves the attachments for a case entity. This support pagination.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export async function find(
|
||||
{ caseID, queryParams }: FindArgs,
|
||||
|
@ -219,8 +171,6 @@ export async function find(
|
|||
|
||||
/**
|
||||
* Retrieves a single attachment by its ID.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export async function get(
|
||||
{ attachmentID, caseID }: GetArgs,
|
||||
|
@ -228,14 +178,12 @@ export async function get(
|
|||
): Promise<CommentResponse> {
|
||||
const {
|
||||
services: { attachmentService },
|
||||
unsecuredSavedObjectsClient,
|
||||
logger,
|
||||
authorization,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
const comment = await attachmentService.get({
|
||||
unsecuredSavedObjectsClient,
|
||||
const comment = await attachmentService.getter.get({
|
||||
attachmentId: attachmentID,
|
||||
});
|
||||
|
||||
|
@ -256,8 +204,6 @@ export async function get(
|
|||
|
||||
/**
|
||||
* Retrieves all the attachments for a case.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export async function getAll(
|
||||
{ caseID }: GetAllArgs,
|
||||
|
|
120
x-pack/plugins/cases/server/client/attachments/types.ts
Normal file
120
x-pack/plugins/cases/server/client/attachments/types.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 {
|
||||
BulkCreateCommentRequest,
|
||||
CommentPatchRequest,
|
||||
CommentRequest,
|
||||
FindQueryParams,
|
||||
} from '../../../common/api';
|
||||
|
||||
/**
|
||||
* The arguments needed for creating a new attachment to a case.
|
||||
*/
|
||||
export interface AddArgs {
|
||||
/**
|
||||
* The case ID that this attachment will be associated with
|
||||
*/
|
||||
caseId: string;
|
||||
/**
|
||||
* The attachment values.
|
||||
*/
|
||||
comment: CommentRequest;
|
||||
}
|
||||
|
||||
export interface BulkCreateArgs {
|
||||
caseId: string;
|
||||
attachments: BulkCreateCommentRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for deleting all comments of a case.
|
||||
*/
|
||||
export interface DeleteAllArgs {
|
||||
/**
|
||||
* The case ID to delete all attachments for
|
||||
*/
|
||||
caseID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for deleting a single attachment of a case.
|
||||
*/
|
||||
export interface DeleteArgs {
|
||||
/**
|
||||
* The case ID to delete an attachment from
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* The attachment ID to delete
|
||||
*/
|
||||
attachmentID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for finding attachments of a case
|
||||
*/
|
||||
export interface FindArgs {
|
||||
/**
|
||||
* The case ID for finding associated attachments
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* Optional parameters for filtering the returned attachments
|
||||
*/
|
||||
queryParams?: FindQueryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for retrieving all attachments of a case
|
||||
*/
|
||||
export interface GetAllArgs {
|
||||
/**
|
||||
* The case ID to retrieve all attachments for
|
||||
*/
|
||||
caseID: string;
|
||||
}
|
||||
|
||||
export interface GetArgs {
|
||||
/**
|
||||
* The ID of the case to retrieve an attachment from
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* The ID of the attachment to retrieve
|
||||
*/
|
||||
attachmentID: string;
|
||||
}
|
||||
|
||||
export interface BulkGetArgs {
|
||||
caseID: string;
|
||||
/**
|
||||
* The ids of the attachments
|
||||
*/
|
||||
attachmentIDs: string[];
|
||||
}
|
||||
|
||||
export interface GetAllAlertsAttachToCase {
|
||||
/**
|
||||
* The ID of the case to retrieve the alerts from
|
||||
*/
|
||||
caseId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for updating a single attachment
|
||||
*/
|
||||
export interface UpdateArgs {
|
||||
/**
|
||||
* The ID of the case that is associated with this attachment
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* The full attachment request with the fields updated with appropriate values
|
||||
*/
|
||||
updateRequest: CommentPatchRequest;
|
||||
}
|
|
@ -10,25 +10,12 @@ import Boom from '@hapi/boom';
|
|||
import { CaseCommentModel } from '../../common/models';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { isCommentRequestTypeExternalReference } from '../../../common/utils/attachments';
|
||||
import type { CaseResponse, CommentPatchRequest } from '../../../common/api';
|
||||
import type { CaseResponse } from '../../../common/api';
|
||||
import { CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import type { CasesClientArgs } from '..';
|
||||
import { decodeCommentRequest } from '../utils';
|
||||
import { Operations } from '../../authorization';
|
||||
|
||||
/**
|
||||
* Parameters for updating a single attachment
|
||||
*/
|
||||
export interface UpdateArgs {
|
||||
/**
|
||||
* The ID of the case that is associated with this attachment
|
||||
*/
|
||||
caseID: string;
|
||||
/**
|
||||
* The full attachment request with the fields updated with appropriate values
|
||||
*/
|
||||
updateRequest: CommentPatchRequest;
|
||||
}
|
||||
import type { UpdateArgs } from './types';
|
||||
|
||||
/**
|
||||
* Update an attachment.
|
||||
|
@ -41,7 +28,6 @@ export async function update(
|
|||
): Promise<CaseResponse> {
|
||||
const {
|
||||
services: { attachmentService },
|
||||
unsecuredSavedObjectsClient,
|
||||
logger,
|
||||
authorization,
|
||||
} = clientArgs;
|
||||
|
@ -55,8 +41,7 @@ export async function update(
|
|||
|
||||
decodeCommentRequest(queryRestAttributes);
|
||||
|
||||
const myComment = await attachmentService.get({
|
||||
unsecuredSavedObjectsClient,
|
||||
const myComment = await attachmentService.getter.get({
|
||||
attachmentId: queryCommentId,
|
||||
});
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('bulkGet', () => {
|
|||
const caseSO = mockCases[0];
|
||||
const clientArgs = createCasesClientMockArgs();
|
||||
clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [caseSO] });
|
||||
clientArgs.services.attachmentService.getCaseCommentStats.mockResolvedValue(new Map());
|
||||
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
|
||||
|
||||
clientArgs.authorization.getAndEnsureAuthorizedEntities.mockResolvedValue({
|
||||
authorized: [caseSO],
|
||||
|
|
|
@ -11,13 +11,13 @@ import { fold } from 'fp-ts/lib/Either';
|
|||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pick, partition } from 'lodash';
|
||||
|
||||
import type { SavedObjectError } from '@kbn/core-saved-objects-common';
|
||||
import { MAX_BULK_GET_CASES } from '../../../common/constants';
|
||||
import type {
|
||||
CasesBulkGetResponse,
|
||||
CasesBulkGetResponseCertainFields,
|
||||
CasesBulkGetRequestCertainFields,
|
||||
CaseResponse,
|
||||
CaseAttributes,
|
||||
} from '../../../common/api';
|
||||
import {
|
||||
CasesBulkGetRequestRt,
|
||||
|
@ -30,11 +30,12 @@ import {
|
|||
import { getTypeProps } from '../../../common/api/runtime_types';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import { asArray, flattenCaseSavedObject } from '../../common/utils';
|
||||
import type { CasesClientArgs } from '../types';
|
||||
import type { CasesClientArgs, SOWithErrors } from '../types';
|
||||
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
|
||||
import type { CaseSavedObject } from '../../common/types';
|
||||
import { Operations } from '../../authorization';
|
||||
|
||||
type SOWithErrors = Array<CaseSavedObject & { error: SavedObjectError }>;
|
||||
type CaseSavedObjectWithErrors = SOWithErrors<CaseAttributes>;
|
||||
|
||||
/**
|
||||
* Retrieves multiple cases by ids.
|
||||
|
@ -47,7 +48,6 @@ export const bulkGet = async <Field extends keyof CaseResponse = keyof CaseRespo
|
|||
services: { caseService, attachmentService },
|
||||
logger,
|
||||
authorization,
|
||||
unsecuredSavedObjectsClient,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
|
@ -66,18 +66,20 @@ export const bulkGet = async <Field extends keyof CaseResponse = keyof CaseRespo
|
|||
const [validCases, soBulkGetErrors] = partition(
|
||||
cases.saved_objects,
|
||||
(caseInfo) => caseInfo.error === undefined
|
||||
) as [CaseSavedObject[], SOWithErrors];
|
||||
) as [CaseSavedObject[], CaseSavedObjectWithErrors];
|
||||
|
||||
const { authorized: authorizedCases, unauthorized: unauthorizedCases } =
|
||||
await authorization.getAndEnsureAuthorizedEntities({ savedObjects: validCases });
|
||||
await authorization.getAndEnsureAuthorizedEntities({
|
||||
savedObjects: validCases,
|
||||
operation: Operations.bulkGetCases,
|
||||
});
|
||||
|
||||
const requestForTotals = ['totalComment', 'totalAlerts'].some(
|
||||
(totalKey) => !fields || fields.includes(totalKey)
|
||||
);
|
||||
|
||||
const commentTotals = requestForTotals
|
||||
? await attachmentService.getCaseCommentStats({
|
||||
unsecuredSavedObjectsClient,
|
||||
? await attachmentService.getter.getCaseCommentStats({
|
||||
caseIds: authorizedCases.map((theCase) => theCase.id),
|
||||
})
|
||||
: new Map();
|
||||
|
@ -139,7 +141,7 @@ const throwErrorIfCaseIdsReachTheLimit = (ids: string[]) => {
|
|||
};
|
||||
|
||||
const constructErrors = (
|
||||
soBulkGetErrors: SOWithErrors,
|
||||
soBulkGetErrors: CaseSavedObjectWithErrors,
|
||||
unauthorizedCases: CaseSavedObject[]
|
||||
): CasesBulkGetResponse['errors'] => {
|
||||
const errors: CasesBulkGetResponse['errors'] = [];
|
||||
|
|
|
@ -24,7 +24,6 @@ import { Operations } from '../../authorization';
|
|||
*/
|
||||
export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise<void> {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
services: { caseService, attachmentService, userActionService },
|
||||
logger,
|
||||
authorization,
|
||||
|
@ -50,9 +49,8 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
|
|||
entities: Array.from(entities.values()),
|
||||
});
|
||||
|
||||
const attachmentIds = await attachmentService.getAttachmentIdsForCases({
|
||||
const attachmentIds = await attachmentService.getter.getAttachmentIdsForCases({
|
||||
caseIds: ids,
|
||||
unsecuredSavedObjectsClient,
|
||||
});
|
||||
|
||||
const userActionIds = await userActionService.getUserActionIdsForCases(ids);
|
||||
|
|
|
@ -67,7 +67,6 @@ export const getCasesByAlertID = async (
|
|||
services: { caseService, attachmentService },
|
||||
logger,
|
||||
authorization,
|
||||
unsecuredSavedObjectsClient,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
|
@ -107,8 +106,7 @@ export const getCasesByAlertID = async (
|
|||
return [];
|
||||
}
|
||||
|
||||
const commentStats = await attachmentService.getCaseCommentStats({
|
||||
unsecuredSavedObjectsClient,
|
||||
const commentStats = await attachmentService.getter.getCaseCommentStats({
|
||||
caseIds,
|
||||
});
|
||||
|
||||
|
|
|
@ -241,7 +241,6 @@ export const push = async (
|
|||
}),
|
||||
|
||||
attachmentService.bulkUpdate({
|
||||
unsecuredSavedObjectsClient,
|
||||
comments: comments.saved_objects
|
||||
.filter((comment) => comment.attributes.pushed_at == null)
|
||||
.map((comment) => ({
|
||||
|
|
|
@ -170,10 +170,11 @@ export class CasesClientFactory {
|
|||
}): CasesServices {
|
||||
this.validateInitialization();
|
||||
|
||||
const attachmentService = new AttachmentService(
|
||||
this.logger,
|
||||
this.options.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
const attachmentService = new AttachmentService({
|
||||
log: this.logger,
|
||||
persistableStateAttachmentTypeRegistry: this.options.persistableStateAttachmentTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
});
|
||||
|
||||
const caseService = new CasesService({
|
||||
log: this.logger,
|
||||
|
|
|
@ -25,7 +25,6 @@ export class Actions extends SingleCaseAggregationHandler {
|
|||
|
||||
public async compute(): Promise<SingleCaseMetricsResponse> {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization,
|
||||
services: { attachmentService },
|
||||
logger,
|
||||
|
@ -48,7 +47,6 @@ export class Actions extends SingleCaseAggregationHandler {
|
|||
}, {});
|
||||
|
||||
const response = await attachmentService.executeCaseActionsAggregations({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: theCase.id,
|
||||
filter: authorizationFilter,
|
||||
aggregations,
|
||||
|
|
|
@ -18,7 +18,6 @@ export class AlertsCount extends SingleCaseBaseHandler {
|
|||
|
||||
public async compute(): Promise<SingleCaseMetricsResponse> {
|
||||
const {
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization,
|
||||
services: { attachmentService },
|
||||
logger,
|
||||
|
@ -38,7 +37,6 @@ export class AlertsCount extends SingleCaseBaseHandler {
|
|||
);
|
||||
|
||||
const alertsCount = await attachmentService.countAlertsAttachedToCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId: theCase.id,
|
||||
filter: authorizationFilter,
|
||||
});
|
||||
|
|
|
@ -74,6 +74,7 @@ type AttachmentsSubClientMock = jest.Mocked<AttachmentsSubClient>;
|
|||
|
||||
const createAttachmentsSubClientMock = (): AttachmentsSubClientMock => {
|
||||
return {
|
||||
bulkGet: jest.fn(),
|
||||
add: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
deleteAll: jest.fn(),
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { SavedObjectsClientContract, Logger } from '@kbn/core/server';
|
||||
import type { SavedObjectsClientContract, Logger, SavedObject } from '@kbn/core/server';
|
||||
import type { ActionsClient } from '@kbn/actions-plugin/server';
|
||||
import type { LensServerPluginSetup } from '@kbn/lens-plugin/server';
|
||||
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import type { IBasePath } from '@kbn/core-http-browser';
|
||||
import type { ISavedObjectsSerializer } from '@kbn/core-saved-objects-server';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import type { SavedObjectError } from '@kbn/core-saved-objects-common';
|
||||
import type { CasesFindRequest, User } from '../../common/api';
|
||||
import type { Authorization } from '../authorization/authorization';
|
||||
import type {
|
||||
|
@ -64,3 +65,5 @@ export type CasesFindQueryParams = Partial<
|
|||
'tags' | 'reporters' | 'status' | 'severity' | 'owner' | 'from' | 'to' | 'assignees'
|
||||
> & { sortByField?: string; authorizationFilter?: KueryNode }
|
||||
>;
|
||||
|
||||
export type SOWithErrors<T> = Array<SavedObject<T> & { error: SavedObjectError }>;
|
||||
|
|
|
@ -95,8 +95,7 @@ export class CaseCommentModel {
|
|||
};
|
||||
|
||||
if (queryRestAttributes.type === CommentType.user && queryRestAttributes?.comment) {
|
||||
const currentComment = (await this.params.services.attachmentService.get({
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
const currentComment = (await this.params.services.attachmentService.getter.get({
|
||||
attachmentId: id,
|
||||
})) as SavedObject<CommentRequestUserType>;
|
||||
|
||||
|
@ -110,7 +109,6 @@ export class CaseCommentModel {
|
|||
|
||||
const [comment, commentableCase] = await Promise.all([
|
||||
this.params.services.attachmentService.update({
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
attachmentId: id,
|
||||
updatedAttributes: {
|
||||
...queryRestAttributes,
|
||||
|
@ -212,7 +210,6 @@ export class CaseCommentModel {
|
|||
|
||||
const [comment, commentableCase] = await Promise.all([
|
||||
this.params.services.attachmentService.create({
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
attributes: transformNewComment({
|
||||
createdDate,
|
||||
...commentReq,
|
||||
|
@ -276,7 +273,6 @@ export class CaseCommentModel {
|
|||
private async validateAlertsLimitOnCase(totalAlertsInReq: number) {
|
||||
const alertsValueCount =
|
||||
await this.params.services.attachmentService.valueCountAlertsAttachedToCase({
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
caseId: this.caseInfo.id,
|
||||
});
|
||||
|
||||
|
@ -410,7 +406,6 @@ export class CaseCommentModel {
|
|||
|
||||
const [newlyCreatedAttachments, commentableCase] = await Promise.all([
|
||||
this.params.services.attachmentService.bulkCreate({
|
||||
unsecuredSavedObjectsClient: this.params.unsecuredSavedObjectsClient,
|
||||
attachments: attachments.map(({ id, ...attachment }) => {
|
||||
return {
|
||||
attributes: transformNewComment({
|
||||
|
|
|
@ -11,6 +11,7 @@ import { bulkCreateAttachmentsRoute } from './internal/bulk_create_attachments';
|
|||
import { bulkGetCasesRoute } from './internal/bulk_get_cases';
|
||||
import { suggestUserProfilesRoute } from './internal/suggest_user_profiles';
|
||||
import type { CaseRoute } from './types';
|
||||
import { bulkGetAttachmentsRoute } from './internal/bulk_get_attachments';
|
||||
|
||||
export const getInternalRoutes = (userProfileService: UserProfileService) =>
|
||||
[
|
||||
|
@ -18,4 +19,5 @@ export const getInternalRoutes = (userProfileService: UserProfileService) =>
|
|||
suggestUserProfilesRoute(userProfileService),
|
||||
getConnectorsRoute,
|
||||
bulkGetCasesRoute,
|
||||
bulkGetAttachmentsRoute,
|
||||
] as CaseRoute[];
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import type { BulkGetAttachmentsRequest } from '../../../../common/api';
|
||||
|
||||
import { INTERNAL_BULK_GET_ATTACHMENTS_URL } from '../../../../common/constants';
|
||||
import { createCaseError } from '../../../common/error';
|
||||
import { createCasesRoute } from '../create_cases_route';
|
||||
import { escapeHatch } from '../utils';
|
||||
|
||||
export const bulkGetAttachmentsRoute = createCasesRoute({
|
||||
method: 'post',
|
||||
path: INTERNAL_BULK_GET_ATTACHMENTS_URL,
|
||||
params: {
|
||||
params: schema.object({
|
||||
case_id: schema.string(),
|
||||
}),
|
||||
body: escapeHatch,
|
||||
},
|
||||
handler: async ({ context, request, response }) => {
|
||||
try {
|
||||
const caseContext = await context.cases;
|
||||
const client = await caseContext.getCasesClient();
|
||||
const body = request.body as BulkGetAttachmentsRequest;
|
||||
|
||||
return response.ok({
|
||||
body: await client.attachments.bulkGet({
|
||||
caseID: request.params.case_id,
|
||||
attachmentIDs: body.ids,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to bulk get attachments in route case id: ${request.params.case_id}: ${error}`,
|
||||
error,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
|
@ -28,7 +28,11 @@ describe('CasesService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new AttachmentService(mockLogger, persistableStateAttachmentTypeRegistry);
|
||||
service = new AttachmentService({
|
||||
log: mockLogger,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
|
@ -44,7 +48,6 @@ describe('CasesService', () => {
|
|||
unsecuredSavedObjectsClient.update.mockResolvedValue(soClientRes);
|
||||
|
||||
const res = await service.update({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId: '1',
|
||||
updatedAttributes: persistableStateAttachment,
|
||||
options: { references: [] },
|
||||
|
@ -60,7 +63,6 @@ describe('CasesService', () => {
|
|||
});
|
||||
|
||||
const res = await service.update({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId: '1',
|
||||
updatedAttributes: externalReferenceAttachmentSO,
|
||||
options: { references: [] },
|
||||
|
@ -76,7 +78,6 @@ describe('CasesService', () => {
|
|||
});
|
||||
|
||||
const res = await service.update({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId: '1',
|
||||
updatedAttributes: externalReferenceAttachmentES,
|
||||
options: { references: [] },
|
||||
|
@ -111,7 +112,6 @@ describe('CasesService', () => {
|
|||
});
|
||||
|
||||
const res = await service.bulkUpdate({
|
||||
unsecuredSavedObjectsClient,
|
||||
comments: [
|
||||
{
|
||||
attachmentId: '1',
|
||||
|
|
|
@ -6,33 +6,23 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectReference,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResponse,
|
||||
SavedObjectsUpdateOptions,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import type {
|
||||
AttachmentTotals,
|
||||
AttributesTypeAlerts,
|
||||
CommentAttributes as AttachmentAttributes,
|
||||
CommentAttributesWithoutRefs as AttachmentAttributesWithoutRefs,
|
||||
CommentPatchAttributes as AttachmentPatchAttributes,
|
||||
} from '../../../common/api';
|
||||
import { CommentType } from '../../../common/api';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
} from '../../../common/constants';
|
||||
import type { ClientArgs } from '..';
|
||||
import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { buildFilter, combineFilters } from '../../client/utils';
|
||||
import { defaultSortField } from '../../common/utils';
|
||||
import type { AggregationResponse } from '../../client/metrics/types';
|
||||
|
@ -42,15 +32,10 @@ import {
|
|||
injectAttachmentSOAttributesFromRefsForPatch,
|
||||
} from '../so_references';
|
||||
import type { SavedObjectFindOptionsKueryNode } from '../../common/types';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import type { IndexRefresh } from '../types';
|
||||
import type { AttachedToCaseArgs, GetAttachmentArgs, ServiceContext } from './types';
|
||||
import { AttachmentGetter } from './operations/get';
|
||||
|
||||
interface AttachedToCaseArgs extends ClientArgs {
|
||||
caseId: string;
|
||||
filter?: KueryNode;
|
||||
}
|
||||
|
||||
type GetAllAlertsAttachToCaseArgs = AttachedToCaseArgs;
|
||||
type AlertsAttachedToCaseArgs = AttachedToCaseArgs;
|
||||
|
||||
interface AttachmentsAttachedToCaseArgs extends AttachedToCaseArgs {
|
||||
|
@ -62,19 +47,15 @@ interface CountActionsAttachedToCaseArgs extends AttachedToCaseArgs {
|
|||
aggregations: Record<string, estypes.AggregationsAggregationContainer>;
|
||||
}
|
||||
|
||||
interface GetAttachmentArgs extends ClientArgs {
|
||||
attachmentId: string;
|
||||
}
|
||||
|
||||
interface DeleteAttachmentArgs extends GetAttachmentArgs, IndexRefresh {}
|
||||
|
||||
interface CreateAttachmentArgs extends ClientArgs, IndexRefresh {
|
||||
interface CreateAttachmentArgs extends IndexRefresh {
|
||||
attributes: AttachmentAttributes;
|
||||
references: SavedObjectReference[];
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface BulkCreateAttachments extends ClientArgs, IndexRefresh {
|
||||
interface BulkCreateAttachments extends IndexRefresh {
|
||||
attachments: Array<{
|
||||
attributes: AttachmentAttributes;
|
||||
references: SavedObjectReference[];
|
||||
|
@ -88,60 +69,28 @@ interface UpdateArgs {
|
|||
options?: Omit<SavedObjectsUpdateOptions<AttachmentAttributes>, 'upsert'>;
|
||||
}
|
||||
|
||||
export type UpdateAttachmentArgs = UpdateArgs & ClientArgs;
|
||||
export type UpdateAttachmentArgs = UpdateArgs;
|
||||
|
||||
interface BulkUpdateAttachmentArgs extends ClientArgs, IndexRefresh {
|
||||
interface BulkUpdateAttachmentArgs extends IndexRefresh {
|
||||
comments: UpdateArgs[];
|
||||
}
|
||||
|
||||
export class AttachmentService {
|
||||
constructor(
|
||||
private readonly log: Logger,
|
||||
private readonly persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
) {}
|
||||
private readonly _getter: AttachmentGetter;
|
||||
|
||||
public async getAttachmentIdsForCases({
|
||||
caseIds,
|
||||
unsecuredSavedObjectsClient,
|
||||
}: {
|
||||
caseIds: string[];
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
}) {
|
||||
try {
|
||||
this.log.debug(`Attempting to retrieve attachments associated with cases: [${caseIds}]`);
|
||||
constructor(private readonly context: ServiceContext) {
|
||||
this._getter = new AttachmentGetter(context);
|
||||
}
|
||||
|
||||
const finder = unsecuredSavedObjectsClient.createPointInTimeFinder({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: caseIds.map((id) => ({ id, type: CASE_SAVED_OBJECT })),
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
/**
|
||||
* We only care about the ids so to reduce the data returned we should limit the fields in the response. Core
|
||||
* doesn't support retrieving no fields (id would always be returned anyway) so to limit it we'll only request
|
||||
* the owner even though we don't need it.
|
||||
*/
|
||||
fields: ['owner'],
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
});
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
for await (const attachmentSavedObject of finder.find()) {
|
||||
ids.push(...attachmentSavedObject.saved_objects.map((attachment) => attachment.id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
} catch (error) {
|
||||
this.log.error(`Error retrieving attachments associated with cases: [${caseIds}]: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
public get getter() {
|
||||
return this._getter;
|
||||
}
|
||||
|
||||
public async countAlertsAttachedToCase(
|
||||
params: AlertsAttachedToCaseArgs
|
||||
): Promise<number | undefined> {
|
||||
try {
|
||||
this.log.debug(`Attempting to count alerts for case id ${params.caseId}`);
|
||||
this.context.log.debug(`Attempting to count alerts for case id ${params.caseId}`);
|
||||
const res = await this.executeCaseAggregations<{ alerts: { value: number } }>({
|
||||
...params,
|
||||
attachmentType: CommentType.alert,
|
||||
|
@ -150,7 +99,7 @@ export class AttachmentService {
|
|||
|
||||
return res?.alerts?.value;
|
||||
} catch (error) {
|
||||
this.log.error(`Error while counting alerts for case id ${params.caseId}: ${error}`);
|
||||
this.context.log.error(`Error while counting alerts for case id ${params.caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +116,7 @@ export class AttachmentService {
|
|||
|
||||
public async valueCountAlertsAttachedToCase(params: AlertsAttachedToCaseArgs): Promise<number> {
|
||||
try {
|
||||
this.log.debug(`Attempting to value count alerts for case id ${params.caseId}`);
|
||||
this.context.log.debug(`Attempting to value count alerts for case id ${params.caseId}`);
|
||||
const res = await this.executeCaseAggregations<{ alerts: { value: number } }>({
|
||||
...params,
|
||||
attachmentType: CommentType.alert,
|
||||
|
@ -176,47 +125,9 @@ export class AttachmentService {
|
|||
|
||||
return res?.alerts?.value ?? 0;
|
||||
} catch (error) {
|
||||
this.log.error(`Error while value counting alerts for case id ${params.caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the alerts attached to a case.
|
||||
*/
|
||||
public async getAllAlertsAttachToCase({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
filter,
|
||||
}: GetAllAlertsAttachToCaseArgs): Promise<Array<SavedObject<AttributesTypeAlerts>>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to GET all alerts for case id ${caseId}`);
|
||||
const alertsFilter = buildFilter({
|
||||
filters: [CommentType.alert],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const combinedFilter = combineFilters([alertsFilter, filter]);
|
||||
|
||||
const finder = unsecuredSavedObjectsClient.createPointInTimeFinder<AttributesTypeAlerts>({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
filter: combinedFilter,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
});
|
||||
|
||||
let result: Array<SavedObject<AttributesTypeAlerts>> = [];
|
||||
for await (const userActionSavedObject of finder.find()) {
|
||||
result = result.concat(userActionSavedObject.saved_objects);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.log.error(`Error on GET all alerts for case id ${caseId}: ${error}`);
|
||||
this.context.log.error(
|
||||
`Error while value counting alerts for case id ${params.caseId}: ${error}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -225,14 +136,13 @@ export class AttachmentService {
|
|||
* Executes the aggregations against a type of attachment attached to a case.
|
||||
*/
|
||||
public async executeCaseAggregations<Agg extends AggregationResponse = AggregationResponse>({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseId,
|
||||
filter,
|
||||
aggregations,
|
||||
attachmentType,
|
||||
}: AttachmentsAttachedToCaseArgs): Promise<Agg | undefined> {
|
||||
try {
|
||||
this.log.debug(`Attempting to aggregate for case id ${caseId}`);
|
||||
this.context.log.debug(`Attempting to aggregate for case id ${caseId}`);
|
||||
const attachmentFilter = buildFilter({
|
||||
filters: attachmentType,
|
||||
field: 'type',
|
||||
|
@ -242,7 +152,10 @@ export class AttachmentService {
|
|||
|
||||
const combinedFilter = combineFilters([attachmentFilter, filter]);
|
||||
|
||||
const response = await unsecuredSavedObjectsClient.find<AttachmentAttributes, Agg>({
|
||||
const response = await this.context.unsecuredSavedObjectsClient.find<
|
||||
AttachmentAttributes,
|
||||
Agg
|
||||
>({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
page: 1,
|
||||
|
@ -254,7 +167,7 @@ export class AttachmentService {
|
|||
|
||||
return response.aggregations;
|
||||
} catch (error) {
|
||||
this.log.error(`Error while executing aggregation for case id ${caseId}: ${error}`);
|
||||
this.context.log.error(`Error while executing aggregation for case id ${caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -266,133 +179,114 @@ export class AttachmentService {
|
|||
params: CountActionsAttachedToCaseArgs
|
||||
): Promise<AggregationResponse | undefined> {
|
||||
try {
|
||||
this.log.debug(`Attempting to count actions for case id ${params.caseId}`);
|
||||
this.context.log.debug(`Attempting to count actions for case id ${params.caseId}`);
|
||||
return await this.executeCaseAggregations({ ...params, attachmentType: CommentType.actions });
|
||||
} catch (error) {
|
||||
this.log.error(`Error while counting actions for case id ${params.caseId}: ${error}`);
|
||||
this.context.log.error(`Error while counting actions for case id ${params.caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async get({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId,
|
||||
}: GetAttachmentArgs): Promise<SavedObject<AttachmentAttributes>> {
|
||||
public async delete({ attachmentId, refresh }: DeleteAttachmentArgs) {
|
||||
try {
|
||||
this.log.debug(`Attempting to GET attachment ${attachmentId}`);
|
||||
const res = await unsecuredSavedObjectsClient.get<AttachmentAttributesWithoutRefs>(
|
||||
this.context.log.debug(`Attempting to DELETE attachment ${attachmentId}`);
|
||||
return await this.context.unsecuredSavedObjectsClient.delete(
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
attachmentId
|
||||
attachmentId,
|
||||
{
|
||||
refresh,
|
||||
}
|
||||
);
|
||||
|
||||
return injectAttachmentSOAttributesFromRefs(res, this.persistableStateAttachmentTypeRegistry);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on GET attachment ${attachmentId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async delete({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId,
|
||||
refresh,
|
||||
}: DeleteAttachmentArgs) {
|
||||
try {
|
||||
this.log.debug(`Attempting to DELETE attachment ${attachmentId}`);
|
||||
return await unsecuredSavedObjectsClient.delete(CASE_COMMENT_SAVED_OBJECT, attachmentId, {
|
||||
refresh,
|
||||
});
|
||||
} catch (error) {
|
||||
this.log.error(`Error on DELETE attachment ${attachmentId}: ${error}`);
|
||||
this.context.log.error(`Error on DELETE attachment ${attachmentId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async create({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
references,
|
||||
id,
|
||||
refresh,
|
||||
}: CreateAttachmentArgs): Promise<SavedObject<AttachmentAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new comment`);
|
||||
this.context.log.debug(`Attempting to POST a new comment`);
|
||||
|
||||
const { attributes: extractedAttributes, references: extractedReferences } =
|
||||
extractAttachmentSORefsFromAttributes(
|
||||
attributes,
|
||||
references,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
const attachment = await unsecuredSavedObjectsClient.create<AttachmentAttributesWithoutRefs>(
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
extractedAttributes,
|
||||
{
|
||||
references: extractedReferences,
|
||||
id,
|
||||
refresh,
|
||||
}
|
||||
);
|
||||
const attachment =
|
||||
await this.context.unsecuredSavedObjectsClient.create<AttachmentAttributesWithoutRefs>(
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
extractedAttributes,
|
||||
{
|
||||
references: extractedReferences,
|
||||
id,
|
||||
refresh,
|
||||
}
|
||||
);
|
||||
|
||||
return injectAttachmentSOAttributesFromRefs(
|
||||
attachment,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on POST a new comment: ${error}`);
|
||||
this.context.log.error(`Error on POST a new comment: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async bulkCreate({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachments,
|
||||
refresh,
|
||||
}: BulkCreateAttachments): Promise<SavedObjectsBulkResponse<AttachmentAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to bulk create attachments`);
|
||||
const res = await unsecuredSavedObjectsClient.bulkCreate<AttachmentAttributesWithoutRefs>(
|
||||
attachments.map((attachment) => {
|
||||
const { attributes: extractedAttributes, references: extractedReferences } =
|
||||
extractAttachmentSORefsFromAttributes(
|
||||
attachment.attributes,
|
||||
attachment.references,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
this.context.log.debug(`Attempting to bulk create attachments`);
|
||||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.bulkCreate<AttachmentAttributesWithoutRefs>(
|
||||
attachments.map((attachment) => {
|
||||
const { attributes: extractedAttributes, references: extractedReferences } =
|
||||
extractAttachmentSORefsFromAttributes(
|
||||
attachment.attributes,
|
||||
attachment.references,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
return {
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
...attachment,
|
||||
attributes: extractedAttributes,
|
||||
references: extractedReferences,
|
||||
};
|
||||
}),
|
||||
{ refresh }
|
||||
);
|
||||
return {
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
...attachment,
|
||||
attributes: extractedAttributes,
|
||||
references: extractedReferences,
|
||||
};
|
||||
}),
|
||||
{ refresh }
|
||||
);
|
||||
|
||||
return {
|
||||
saved_objects: res.saved_objects.map((so) => {
|
||||
return injectAttachmentSOAttributesFromRefs(
|
||||
so,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error(`Error on bulk create attachments: ${error}`);
|
||||
this.context.log.error(`Error on bulk create attachments: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async update({
|
||||
unsecuredSavedObjectsClient,
|
||||
attachmentId,
|
||||
updatedAttributes,
|
||||
options,
|
||||
}: UpdateAttachmentArgs): Promise<SavedObjectsUpdateResponse<AttachmentAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE comment ${attachmentId}`);
|
||||
this.context.log.debug(`Attempting to UPDATE comment ${attachmentId}`);
|
||||
|
||||
const {
|
||||
attributes: extractedAttributes,
|
||||
|
@ -401,165 +295,116 @@ export class AttachmentService {
|
|||
} = extractAttachmentSORefsFromAttributes(
|
||||
updatedAttributes,
|
||||
options?.references ?? [],
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
const shouldUpdateRefs = extractedReferences.length > 0 || didDeleteOperation;
|
||||
|
||||
const res = await unsecuredSavedObjectsClient.update<AttachmentAttributesWithoutRefs>(
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
attachmentId,
|
||||
extractedAttributes,
|
||||
{
|
||||
...options,
|
||||
/**
|
||||
* If options?.references are undefined and there is no field to move to the refs
|
||||
* then the extractedReferences will be an empty array. If we pass the empty array
|
||||
* on the update then all previously refs will be removed. The check below is needed
|
||||
* to prevent this.
|
||||
*/
|
||||
references: shouldUpdateRefs ? extractedReferences : undefined,
|
||||
}
|
||||
);
|
||||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.update<AttachmentAttributesWithoutRefs>(
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
attachmentId,
|
||||
extractedAttributes,
|
||||
{
|
||||
...options,
|
||||
/**
|
||||
* If options?.references are undefined and there is no field to move to the refs
|
||||
* then the extractedReferences will be an empty array. If we pass the empty array
|
||||
* on the update then all previously refs will be removed. The check below is needed
|
||||
* to prevent this.
|
||||
*/
|
||||
references: shouldUpdateRefs ? extractedReferences : undefined,
|
||||
}
|
||||
);
|
||||
|
||||
return injectAttachmentSOAttributesFromRefsForPatch(
|
||||
updatedAttributes,
|
||||
res,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
} catch (error) {
|
||||
this.log.error(`Error on UPDATE comment ${attachmentId}: ${error}`);
|
||||
this.context.log.error(`Error on UPDATE comment ${attachmentId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async bulkUpdate({
|
||||
unsecuredSavedObjectsClient,
|
||||
comments,
|
||||
refresh,
|
||||
}: BulkUpdateAttachmentArgs): Promise<SavedObjectsBulkUpdateResponse<AttachmentAttributes>> {
|
||||
try {
|
||||
this.log.debug(
|
||||
this.context.log.debug(
|
||||
`Attempting to UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}`
|
||||
);
|
||||
|
||||
const res = await unsecuredSavedObjectsClient.bulkUpdate<AttachmentAttributesWithoutRefs>(
|
||||
comments.map((c) => {
|
||||
const {
|
||||
attributes: extractedAttributes,
|
||||
references: extractedReferences,
|
||||
didDeleteOperation,
|
||||
} = extractAttachmentSORefsFromAttributes(
|
||||
c.updatedAttributes,
|
||||
c.options?.references ?? [],
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.bulkUpdate<AttachmentAttributesWithoutRefs>(
|
||||
comments.map((c) => {
|
||||
const {
|
||||
attributes: extractedAttributes,
|
||||
references: extractedReferences,
|
||||
didDeleteOperation,
|
||||
} = extractAttachmentSORefsFromAttributes(
|
||||
c.updatedAttributes,
|
||||
c.options?.references ?? [],
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
const shouldUpdateRefs = extractedReferences.length > 0 || didDeleteOperation;
|
||||
const shouldUpdateRefs = extractedReferences.length > 0 || didDeleteOperation;
|
||||
|
||||
return {
|
||||
...c.options,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
id: c.attachmentId,
|
||||
attributes: extractedAttributes,
|
||||
/* If c.options?.references are undefined and there is no field to move to the refs
|
||||
* then the extractedAttributes will be an empty array. If we pass the empty array
|
||||
* on the update then all previously refs will be removed. The check below is needed
|
||||
* to prevent this.
|
||||
*/
|
||||
references: shouldUpdateRefs ? extractedReferences : undefined,
|
||||
};
|
||||
}),
|
||||
{ refresh }
|
||||
);
|
||||
return {
|
||||
...c.options,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
id: c.attachmentId,
|
||||
attributes: extractedAttributes,
|
||||
/* If c.options?.references are undefined and there is no field to move to the refs
|
||||
* then the extractedAttributes will be an empty array. If we pass the empty array
|
||||
* on the update then all previously refs will be removed. The check below is needed
|
||||
* to prevent this.
|
||||
*/
|
||||
references: shouldUpdateRefs ? extractedReferences : undefined,
|
||||
};
|
||||
}),
|
||||
{ refresh }
|
||||
);
|
||||
|
||||
return {
|
||||
saved_objects: res.saved_objects.map((so, index) => {
|
||||
return injectAttachmentSOAttributesFromRefsForPatch(
|
||||
comments[index].updatedAttributes,
|
||||
so,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
this.context.log.error(
|
||||
`Error on UPDATE comments ${comments.map((c) => c.attachmentId).join(', ')}: ${error}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCaseCommentStats({
|
||||
unsecuredSavedObjectsClient,
|
||||
caseIds,
|
||||
}: {
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
caseIds: string[];
|
||||
}): Promise<Map<string, AttachmentTotals>> {
|
||||
if (caseIds.length <= 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
interface AggsResult {
|
||||
references: {
|
||||
caseIds: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
reverse: {
|
||||
alerts: {
|
||||
value: number;
|
||||
};
|
||||
comments: {
|
||||
doc_count: number;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const res = await unsecuredSavedObjectsClient.find<unknown, AggsResult>({
|
||||
hasReference: caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id })),
|
||||
hasReferenceOperator: 'OR',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
perPage: 0,
|
||||
aggs: AttachmentService.buildCommentStatsAggs(caseIds),
|
||||
});
|
||||
|
||||
return (
|
||||
res.aggregations?.references.caseIds.buckets.reduce((acc, idBucket) => {
|
||||
acc.set(idBucket.key, {
|
||||
userComments: idBucket.reverse.comments.doc_count,
|
||||
alerts: idBucket.reverse.alerts.value,
|
||||
});
|
||||
return acc;
|
||||
}, new Map<string, AttachmentTotals>()) ?? new Map()
|
||||
);
|
||||
}
|
||||
|
||||
public async find({
|
||||
unsecuredSavedObjectsClient,
|
||||
options,
|
||||
}: {
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
options?: SavedObjectFindOptionsKueryNode;
|
||||
}): Promise<SavedObjectsFindResponse<AttachmentAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to find comments`);
|
||||
const res = await unsecuredSavedObjectsClient.find<AttachmentAttributesWithoutRefs>({
|
||||
sortField: defaultSortField,
|
||||
...options,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
this.context.log.debug(`Attempting to find comments`);
|
||||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.find<AttachmentAttributesWithoutRefs>({
|
||||
sortField: defaultSortField,
|
||||
...options,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
return {
|
||||
...res,
|
||||
saved_objects: res.saved_objects.map((so) => {
|
||||
const injectedSO = injectAttachmentSOAttributesFromRefs(
|
||||
so,
|
||||
this.persistableStateAttachmentTypeRegistry
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -569,47 +414,8 @@ export class AttachmentService {
|
|||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error(`Error on find comments: ${error}`);
|
||||
this.context.log.error(`Error on find comments: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static buildCommentStatsAggs(
|
||||
ids: string[]
|
||||
): Record<string, estypes.AggregationsAggregationContainer> {
|
||||
return {
|
||||
references: {
|
||||
nested: {
|
||||
path: `${CASE_COMMENT_SAVED_OBJECT}.references`,
|
||||
},
|
||||
aggregations: {
|
||||
caseIds: {
|
||||
terms: {
|
||||
field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`,
|
||||
size: ids.length,
|
||||
},
|
||||
aggregations: {
|
||||
reverse: {
|
||||
reverse_nested: {},
|
||||
aggregations: {
|
||||
alerts: {
|
||||
cardinality: {
|
||||
field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
filter: {
|
||||
term: {
|
||||
[`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`]: CommentType.user,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* 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 { SavedObject } from '@kbn/core/server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
CASE_SAVED_OBJECT,
|
||||
MAX_DOCS_PER_PAGE,
|
||||
} from '../../../../common/constants';
|
||||
import { buildFilter, combineFilters } from '../../../client/utils';
|
||||
import type {
|
||||
AttachmentTotals,
|
||||
AttributesTypeAlerts,
|
||||
CommentAttributes as AttachmentAttributes,
|
||||
CommentAttributesWithoutRefs as AttachmentAttributesWithoutRefs,
|
||||
CommentAttributes,
|
||||
} from '../../../../common/api';
|
||||
import { CommentType } from '../../../../common/api';
|
||||
import type {
|
||||
AttachedToCaseArgs,
|
||||
BulkOptionalAttributes,
|
||||
GetAttachmentArgs,
|
||||
ServiceContext,
|
||||
} from '../types';
|
||||
import {
|
||||
injectAttachmentAttributesAndHandleErrors,
|
||||
injectAttachmentSOAttributesFromRefs,
|
||||
} from '../../so_references';
|
||||
|
||||
type GetAllAlertsAttachToCaseArgs = AttachedToCaseArgs;
|
||||
|
||||
export class AttachmentGetter {
|
||||
constructor(private readonly context: ServiceContext) {}
|
||||
|
||||
public async bulkGet(
|
||||
attachmentIds: string[]
|
||||
): Promise<BulkOptionalAttributes<CommentAttributes>> {
|
||||
try {
|
||||
this.context.log.debug(
|
||||
`Attempting to retrieve attachments with ids: ${attachmentIds.join()}`
|
||||
);
|
||||
|
||||
const response =
|
||||
await this.context.unsecuredSavedObjectsClient.bulkGet<AttachmentAttributesWithoutRefs>(
|
||||
attachmentIds.map((id) => ({ id, type: CASE_COMMENT_SAVED_OBJECT }))
|
||||
);
|
||||
|
||||
return {
|
||||
saved_objects: response.saved_objects.map((so) =>
|
||||
injectAttachmentAttributesAndHandleErrors(
|
||||
so,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
)
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
this.context.log.error(
|
||||
`Error retrieving attachments with ids ${attachmentIds.join()}: ${error}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAttachmentIdsForCases({ caseIds }: { caseIds: string[] }) {
|
||||
try {
|
||||
this.context.log.debug(
|
||||
`Attempting to retrieve attachments associated with cases: [${caseIds}]`
|
||||
);
|
||||
|
||||
const finder = this.context.unsecuredSavedObjectsClient.createPointInTimeFinder({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: caseIds.map((id) => ({ id, type: CASE_SAVED_OBJECT })),
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
/**
|
||||
* We only care about the ids so to reduce the data returned we should limit the fields in the response. Core
|
||||
* doesn't support retrieving no fields (id would always be returned anyway) so to limit it we'll only request
|
||||
* the owner even though we don't need it.
|
||||
*/
|
||||
fields: ['owner'],
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
});
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
for await (const attachmentSavedObject of finder.find()) {
|
||||
ids.push(...attachmentSavedObject.saved_objects.map((attachment) => attachment.id));
|
||||
}
|
||||
|
||||
return ids;
|
||||
} catch (error) {
|
||||
this.context.log.error(
|
||||
`Error retrieving attachments associated with cases: [${caseIds}]: ${error}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the alerts attached to a case.
|
||||
*/
|
||||
public async getAllAlertsAttachToCase({
|
||||
caseId,
|
||||
filter,
|
||||
}: GetAllAlertsAttachToCaseArgs): Promise<Array<SavedObject<AttributesTypeAlerts>>> {
|
||||
try {
|
||||
this.context.log.debug(`Attempting to GET all alerts for case id ${caseId}`);
|
||||
const alertsFilter = buildFilter({
|
||||
filters: [CommentType.alert],
|
||||
field: 'type',
|
||||
operator: 'or',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
});
|
||||
|
||||
const combinedFilter = combineFilters([alertsFilter, filter]);
|
||||
|
||||
const finder =
|
||||
this.context.unsecuredSavedObjectsClient.createPointInTimeFinder<AttributesTypeAlerts>({
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
sortField: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
filter: combinedFilter,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
});
|
||||
|
||||
let result: Array<SavedObject<AttributesTypeAlerts>> = [];
|
||||
for await (const userActionSavedObject of finder.find()) {
|
||||
result = result.concat(userActionSavedObject.saved_objects);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.context.log.error(`Error on GET all alerts for case id ${caseId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async get({
|
||||
attachmentId,
|
||||
}: GetAttachmentArgs): Promise<SavedObject<AttachmentAttributes>> {
|
||||
try {
|
||||
this.context.log.debug(`Attempting to GET attachment ${attachmentId}`);
|
||||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.get<AttachmentAttributesWithoutRefs>(
|
||||
CASE_COMMENT_SAVED_OBJECT,
|
||||
attachmentId
|
||||
);
|
||||
|
||||
return injectAttachmentSOAttributesFromRefs(
|
||||
res,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
} catch (error) {
|
||||
this.context.log.error(`Error on GET attachment ${attachmentId}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getCaseCommentStats({
|
||||
caseIds,
|
||||
}: {
|
||||
caseIds: string[];
|
||||
}): Promise<Map<string, AttachmentTotals>> {
|
||||
if (caseIds.length <= 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
interface AggsResult {
|
||||
references: {
|
||||
caseIds: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
reverse: {
|
||||
alerts: {
|
||||
value: number;
|
||||
};
|
||||
comments: {
|
||||
doc_count: number;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const res = await this.context.unsecuredSavedObjectsClient.find<unknown, AggsResult>({
|
||||
hasReference: caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id })),
|
||||
hasReferenceOperator: 'OR',
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
perPage: 0,
|
||||
aggs: AttachmentGetter.buildCommentStatsAggs(caseIds),
|
||||
});
|
||||
|
||||
return (
|
||||
res.aggregations?.references.caseIds.buckets.reduce((acc, idBucket) => {
|
||||
acc.set(idBucket.key, {
|
||||
userComments: idBucket.reverse.comments.doc_count,
|
||||
alerts: idBucket.reverse.alerts.value,
|
||||
});
|
||||
return acc;
|
||||
}, new Map<string, AttachmentTotals>()) ?? new Map()
|
||||
);
|
||||
}
|
||||
|
||||
private static buildCommentStatsAggs(
|
||||
ids: string[]
|
||||
): Record<string, estypes.AggregationsAggregationContainer> {
|
||||
return {
|
||||
references: {
|
||||
nested: {
|
||||
path: `${CASE_COMMENT_SAVED_OBJECT}.references`,
|
||||
},
|
||||
aggregations: {
|
||||
caseIds: {
|
||||
terms: {
|
||||
field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`,
|
||||
size: ids.length,
|
||||
},
|
||||
aggregations: {
|
||||
reverse: {
|
||||
reverse_nested: {},
|
||||
aggregations: {
|
||||
alerts: {
|
||||
cardinality: {
|
||||
field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
filter: {
|
||||
term: {
|
||||
[`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`]: CommentType.user,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
38
x-pack/plugins/cases/server/services/attachments/types.ts
Normal file
38
x-pack/plugins/cases/server/services/attachments/types.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 {
|
||||
Logger,
|
||||
SavedObject,
|
||||
SavedObjectsBulkResponse,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
|
||||
import type { PartialField } from '../../types';
|
||||
|
||||
export interface ServiceContext {
|
||||
log: Logger;
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
|
||||
unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
export interface AttachedToCaseArgs {
|
||||
caseId: string;
|
||||
filter?: KueryNode;
|
||||
}
|
||||
|
||||
export interface GetAttachmentArgs {
|
||||
attachmentId: string;
|
||||
}
|
||||
|
||||
export type OptionalAttributes<T> = PartialField<SavedObject<T>, 'attributes'>;
|
||||
|
||||
export interface BulkOptionalAttributes<T>
|
||||
extends Omit<SavedObjectsBulkResponse<T>, 'saved_objects'> {
|
||||
saved_objects: Array<OptionalAttributes<T>>;
|
||||
}
|
|
@ -153,10 +153,11 @@ describe('CasesService', () => {
|
|||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const mockLogger = loggerMock.create();
|
||||
const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry();
|
||||
const attachmentService = new AttachmentService(
|
||||
mockLogger,
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
const attachmentService = new AttachmentService({
|
||||
log: mockLogger,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
});
|
||||
|
||||
let service: CasesService;
|
||||
|
||||
|
|
|
@ -226,8 +226,7 @@ export class CasesService {
|
|||
return accMap;
|
||||
}, new Map<string, SavedObjectsFindResult<CaseAttributes>>());
|
||||
|
||||
const commentTotals = await this.attachmentService.getCaseCommentStats({
|
||||
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
|
||||
const commentTotals = await this.attachmentService.getter.getCaseCommentStats({
|
||||
caseIds: Array.from(casesMap.keys()),
|
||||
});
|
||||
|
||||
|
@ -411,7 +410,6 @@ export class CasesService {
|
|||
this.log.debug(`Attempting to GET all comments internal for id ${JSON.stringify(id)}`);
|
||||
if (options?.page !== undefined || options?.perPage !== undefined) {
|
||||
return this.attachmentService.find({
|
||||
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
|
||||
options: {
|
||||
sortField: defaultSortField,
|
||||
...options,
|
||||
|
@ -420,7 +418,6 @@ export class CasesService {
|
|||
}
|
||||
|
||||
return this.attachmentService.find({
|
||||
unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient,
|
||||
options: {
|
||||
page: 1,
|
||||
perPage: MAX_DOCS_PER_PAGE,
|
||||
|
@ -521,7 +518,6 @@ export class CasesService {
|
|||
username,
|
||||
full_name: user.full_name ?? null,
|
||||
email: user.email ?? null,
|
||||
// TODO: verify that adding a new field is ok, shouldn't be a breaking change
|
||||
profile_uid: user.profile_uid,
|
||||
};
|
||||
}) ?? []
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
ConnectorMappingsService,
|
||||
AttachmentService,
|
||||
} from '.';
|
||||
import type { AttachmentGetter } from './attachments/operations/get';
|
||||
import type { LicensingService } from './licensing';
|
||||
import type { EmailNotificationService } from './notifications/email_notification_service';
|
||||
import type { UserActionPersister } from './user_actions/operations/create';
|
||||
|
@ -24,6 +25,12 @@ interface UserActionServiceOperations {
|
|||
finder: CaseUserActionFinderServiceMock;
|
||||
}
|
||||
|
||||
interface AttachmentServiceOperations {
|
||||
getter: AttachmentGetterServiceMock;
|
||||
}
|
||||
|
||||
export type AttachmentGetterServiceMock = jest.Mocked<AttachmentGetter>;
|
||||
|
||||
export type CaseServiceMock = jest.Mocked<CasesService>;
|
||||
export type CaseConfigureServiceMock = jest.Mocked<CaseConfigureService>;
|
||||
export type ConnectorMappingsServiceMock = jest.Mocked<ConnectorMappingsService>;
|
||||
|
@ -33,7 +40,7 @@ export type CaseUserActionServiceMock = jest.Mocked<
|
|||
export type CaseUserActionPersisterServiceMock = jest.Mocked<UserActionPersister>;
|
||||
export type CaseUserActionFinderServiceMock = jest.Mocked<UserActionFinder>;
|
||||
export type AlertServiceMock = jest.Mocked<AlertService>;
|
||||
export type AttachmentServiceMock = jest.Mocked<AttachmentService>;
|
||||
export type AttachmentServiceMock = jest.Mocked<AttachmentService & AttachmentServiceOperations>;
|
||||
export type LicensingServiceMock = jest.Mocked<LicensingService>;
|
||||
export type NotificationServiceMock = jest.Mocked<EmailNotificationService>;
|
||||
|
||||
|
@ -134,22 +141,33 @@ export const createAlertServiceMock = (): AlertServiceMock => {
|
|||
return service as unknown as AlertServiceMock;
|
||||
};
|
||||
|
||||
export const createAttachmentServiceMock = (): AttachmentServiceMock => {
|
||||
const service: PublicMethodsOf<AttachmentService> = {
|
||||
const createAttachmentGetterServiceMock = (): AttachmentGetterServiceMock => {
|
||||
const service: PublicMethodsOf<AttachmentGetter> = {
|
||||
get: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
getAllAlertsAttachToCase: jest.fn(),
|
||||
getCaseCommentStats: jest.fn(),
|
||||
getAttachmentIdsForCases: jest.fn(),
|
||||
};
|
||||
|
||||
return service as unknown as AttachmentGetterServiceMock;
|
||||
};
|
||||
|
||||
type FakeAttachmentService = PublicMethodsOf<AttachmentService> & AttachmentServiceOperations;
|
||||
|
||||
export const createAttachmentServiceMock = (): AttachmentServiceMock => {
|
||||
const service: FakeAttachmentService = {
|
||||
getter: createAttachmentGetterServiceMock(),
|
||||
delete: jest.fn(),
|
||||
create: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
update: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
find: jest.fn(),
|
||||
getAllAlertsAttachToCase: jest.fn(),
|
||||
countAlertsAttachedToCase: jest.fn(),
|
||||
executeCaseActionsAggregations: jest.fn(),
|
||||
getCaseCommentStats: jest.fn(),
|
||||
valueCountAlertsAttachedToCase: jest.fn(),
|
||||
executeCaseAggregations: jest.fn(),
|
||||
getAttachmentIdsForCases: jest.fn(),
|
||||
};
|
||||
|
||||
// the cast here is required because jest.Mocked tries to include private members and would throw an error
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '../attachment_framework/so_references';
|
||||
import { EXTERNAL_REFERENCE_REF_NAME } from '../common/constants';
|
||||
import { isCommentRequestTypeExternalReferenceSO } from '../common/utils';
|
||||
import type { PartialField } from '../types';
|
||||
import { SOReferenceExtractor } from './so_reference_extractor';
|
||||
|
||||
export const getAttachmentSOExtractor = (attachment: Partial<CommentRequest>) => {
|
||||
|
@ -38,6 +39,30 @@ export const getAttachmentSOExtractor = (attachment: Partial<CommentRequest>) =>
|
|||
return new SOReferenceExtractor(fieldsToExtract);
|
||||
};
|
||||
|
||||
type OptionalAttributes<T> = PartialField<SavedObject<T>, 'attributes'>;
|
||||
|
||||
/**
|
||||
* This function should be used when the attributes field could be undefined. Specifically when
|
||||
* performing a bulkGet within the core saved object library. If one of the requested ids does not exist in elasticsearch
|
||||
* then the error field will be set and attributes will be undefined.
|
||||
*/
|
||||
export const injectAttachmentAttributesAndHandleErrors = (
|
||||
savedObject: OptionalAttributes<CommentAttributesWithoutRefs>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
): OptionalAttributes<CommentAttributes> => {
|
||||
if (!hasAttributes(savedObject)) {
|
||||
// we don't actually have an attributes field here so the type doesn't matter, this cast is to get the types to stop
|
||||
// complaining though
|
||||
return savedObject as OptionalAttributes<CommentAttributes>;
|
||||
}
|
||||
|
||||
return injectAttachmentSOAttributesFromRefs(savedObject, persistableStateAttachmentTypeRegistry);
|
||||
};
|
||||
|
||||
const hasAttributes = <T>(savedObject: OptionalAttributes<T>): savedObject is SavedObject<T> => {
|
||||
return savedObject.error == null && savedObject.attributes != null;
|
||||
};
|
||||
|
||||
export const injectAttachmentSOAttributesFromRefs = (
|
||||
savedObject: SavedObject<CommentAttributesWithoutRefs>,
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry
|
||||
|
|
|
@ -56,3 +56,5 @@ export interface PluginStartContract {
|
|||
export interface PluginSetupContract {
|
||||
attachmentFramework: AttachmentFramework;
|
||||
}
|
||||
|
||||
export type PartialField<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
|
38
x-pack/test/cases_api_integration/common/lib/attachments.ts
Normal file
38
x-pack/test/cases_api_integration/common/lib/attachments.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 SuperTest from 'supertest';
|
||||
import {
|
||||
BulkGetAttachmentsResponse,
|
||||
getCaseBulkGetAttachmentsUrl,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import { User } from './authentication/types';
|
||||
import { superUser } from './authentication/users';
|
||||
import { getSpaceUrlPrefix } from './utils';
|
||||
|
||||
export const bulkGetAttachments = async ({
|
||||
supertest,
|
||||
attachmentIds,
|
||||
caseId,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
attachmentIds: string[];
|
||||
caseId: string;
|
||||
auth?: { user: User; space: string | null };
|
||||
expectedHttpCode?: number;
|
||||
}): Promise<BulkGetAttachmentsResponse> => {
|
||||
const { body: comments } = await supertest
|
||||
.post(`${getSpaceUrlPrefix(auth.space)}${getCaseBulkGetAttachmentsUrl(caseId)}`)
|
||||
.send({ ids: attachmentIds })
|
||||
.set('kbn-xsrf', 'abc')
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return comments;
|
||||
};
|
|
@ -46,6 +46,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
|
||||
loadTestFile(require.resolve('./internal/bulk_create_attachments'));
|
||||
loadTestFile(require.resolve('./internal/bulk_get_cases'));
|
||||
loadTestFile(require.resolve('./internal/bulk_get_attachments'));
|
||||
loadTestFile(require.resolve('./internal/get_connectors'));
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,459 @@
|
|||
/*
|
||||
* 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 {
|
||||
AttributesTypeExternalReference,
|
||||
AttributesTypeExternalReferenceSO,
|
||||
CaseResponse,
|
||||
CommentResponseTypePersistableState,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
import {
|
||||
postCaseReq,
|
||||
getPostCaseRequest,
|
||||
postCommentUserReq,
|
||||
postCommentAlertReq,
|
||||
persistableStateAttachment,
|
||||
postExternalReferenceSOReq,
|
||||
postExternalReferenceESReq,
|
||||
} from '../../../../common/lib/mock';
|
||||
import {
|
||||
deleteAllCaseItems,
|
||||
createCase,
|
||||
createComment,
|
||||
bulkCreateAttachments,
|
||||
ensureSavedObjectIsAuthorized,
|
||||
} from '../../../../common/lib/utils';
|
||||
import { bulkGetAttachments } from '../../../../common/lib/attachments';
|
||||
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 => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('bulk_get_attachments', () => {
|
||||
describe('setup using two comments', () => {
|
||||
let updatedCase: CaseResponse;
|
||||
|
||||
before(async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
updatedCase = await bulkCreateAttachments({
|
||||
caseId: postedCase.id,
|
||||
params: [postCommentUserReq, postCommentAlertReq],
|
||||
supertest,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('should retrieve a single attachment', async () => {
|
||||
const response = await bulkGetAttachments({
|
||||
attachmentIds: [updatedCase.comments![0].id],
|
||||
caseId: updatedCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
expect(response.attachments.length).to.be(1);
|
||||
expect(response.errors.length).to.be(0);
|
||||
expect(response.attachments[0].id).to.eql(updatedCase.comments![0].id);
|
||||
});
|
||||
|
||||
it('should retrieve a multiple attachments', async () => {
|
||||
const response = await bulkGetAttachments({
|
||||
attachmentIds: [updatedCase.comments![0].id, updatedCase.comments![1].id],
|
||||
caseId: updatedCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
expect(response.attachments.length).to.be(2);
|
||||
expect(response.errors.length).to.be(0);
|
||||
expect(response.attachments[0].id).to.eql(updatedCase.comments![0].id);
|
||||
expect(response.attachments[1].id).to.eql(updatedCase.comments![1].id);
|
||||
});
|
||||
|
||||
it('returns an empty array when no ids are requested', async () => {
|
||||
const { attachments, errors } = await bulkGetAttachments({
|
||||
attachmentIds: [],
|
||||
caseId: updatedCase.id,
|
||||
supertest,
|
||||
expectedHttpCode: 200,
|
||||
});
|
||||
|
||||
expect(attachments.length).to.be(0);
|
||||
expect(errors.length).to.be(0);
|
||||
});
|
||||
|
||||
it('returns a 400 when more than 10k ids are requested', async () => {
|
||||
await bulkGetAttachments({
|
||||
attachmentIds: Array.from(Array(10001).keys()).map((item) => item.toString()),
|
||||
caseId: updatedCase.id,
|
||||
supertest,
|
||||
expectedHttpCode: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('populates the errors field with attachments that could not be found', async () => {
|
||||
const response = await bulkGetAttachments({
|
||||
attachmentIds: [updatedCase.comments![0].id, 'does-not-exist'],
|
||||
caseId: updatedCase.id,
|
||||
supertest,
|
||||
expectedHttpCode: 200,
|
||||
});
|
||||
|
||||
expect(response.attachments.length).to.be(1);
|
||||
expect(response.errors.length).to.be(1);
|
||||
expect(response.errors[0]).to.eql({
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [cases-comments/does-not-exist] not found',
|
||||
status: 404,
|
||||
attachmentId: 'does-not-exist',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inject references into attributes', () => {
|
||||
it('should inject the persistable state attachment references into the attributes', async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
const patchedCase = await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: persistableStateAttachment,
|
||||
});
|
||||
|
||||
const response = await bulkGetAttachments({
|
||||
attachmentIds: [patchedCase.comments![0].id],
|
||||
caseId: patchedCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
const persistableState = response.attachments[0] as CommentResponseTypePersistableState;
|
||||
|
||||
expect(persistableState.persistableStateAttachmentState).to.eql(
|
||||
persistableStateAttachment.persistableStateAttachmentState
|
||||
);
|
||||
});
|
||||
|
||||
it("should inject saved object external reference style attachment's references into the attributes", async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
const patchedCase = await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: postExternalReferenceSOReq,
|
||||
});
|
||||
|
||||
const response = await bulkGetAttachments({
|
||||
attachmentIds: [patchedCase.comments![0].id],
|
||||
caseId: patchedCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
const externalRefSO = response.attachments[0] as AttributesTypeExternalReferenceSO;
|
||||
|
||||
expect(externalRefSO.externalReferenceId).to.eql(
|
||||
postExternalReferenceSOReq.externalReferenceId
|
||||
);
|
||||
expect(externalRefSO.externalReferenceStorage.soType).to.eql(
|
||||
postExternalReferenceSOReq.externalReferenceStorage.soType
|
||||
);
|
||||
expect(externalRefSO.externalReferenceStorage.type).to.eql(
|
||||
postExternalReferenceSOReq.externalReferenceStorage.type
|
||||
);
|
||||
});
|
||||
|
||||
it("should inject the elasticsearch external reference style attachment's references into the attributes", async () => {
|
||||
const postedCase = await createCase(supertest, postCaseReq);
|
||||
const patchedCase = await createComment({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
params: postExternalReferenceESReq,
|
||||
});
|
||||
|
||||
const response = await bulkGetAttachments({
|
||||
attachmentIds: [patchedCase.comments![0].id],
|
||||
caseId: patchedCase.id,
|
||||
supertest,
|
||||
});
|
||||
|
||||
const externalRefES = response.attachments[0] as AttributesTypeExternalReference;
|
||||
|
||||
expect(externalRefES.externalReferenceId).to.eql(
|
||||
postExternalReferenceESReq.externalReferenceId
|
||||
);
|
||||
expect(externalRefES.externalReferenceStorage.type).to.eql(
|
||||
postExternalReferenceESReq.externalReferenceStorage.type
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rbac', () => {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
describe('security and observability cases', () => {
|
||||
let secCase: CaseResponse;
|
||||
let obsCase: CaseResponse;
|
||||
let secAttachmentId: string;
|
||||
let obsAttachmentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
[secCase, obsCase] = await Promise.all([
|
||||
// Create case owned by the security solution user
|
||||
createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: 'space1',
|
||||
}
|
||||
),
|
||||
// Create case owned by the observability user
|
||||
createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'observabilityFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: 'space1',
|
||||
}
|
||||
),
|
||||
]);
|
||||
|
||||
[secCase, obsCase] = await Promise.all([
|
||||
createComment({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: secCase.id,
|
||||
params: postCommentUserReq,
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
}),
|
||||
createComment({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: obsCase.id,
|
||||
params: { ...postCommentUserReq, owner: 'observabilityFixture' },
|
||||
auth: { user: superUser, space: 'space1' },
|
||||
}),
|
||||
]);
|
||||
|
||||
secAttachmentId = secCase.comments![0].id;
|
||||
obsAttachmentId = obsCase.comments![0].id;
|
||||
});
|
||||
|
||||
it('should be able to read attachments', async () => {
|
||||
for (const scenario of [
|
||||
{
|
||||
user: globalRead,
|
||||
owners: ['securitySolutionFixture'],
|
||||
caseId: secCase.id,
|
||||
attachmentIds: [secAttachmentId],
|
||||
},
|
||||
{
|
||||
user: globalRead,
|
||||
owners: ['observabilityFixture'],
|
||||
caseId: obsCase.id,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
},
|
||||
{
|
||||
user: superUser,
|
||||
owners: ['securitySolutionFixture'],
|
||||
caseId: secCase.id,
|
||||
attachmentIds: [secAttachmentId],
|
||||
},
|
||||
{
|
||||
user: superUser,
|
||||
owners: ['observabilityFixture'],
|
||||
caseId: obsCase.id,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
},
|
||||
{
|
||||
user: secOnlyRead,
|
||||
owners: ['securitySolutionFixture'],
|
||||
caseId: secCase.id,
|
||||
attachmentIds: [secAttachmentId],
|
||||
},
|
||||
{
|
||||
user: obsOnlyRead,
|
||||
owners: ['observabilityFixture'],
|
||||
caseId: obsCase.id,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
},
|
||||
{
|
||||
user: obsSecRead,
|
||||
owners: ['securitySolutionFixture'],
|
||||
caseId: secCase.id,
|
||||
attachmentIds: [secAttachmentId],
|
||||
},
|
||||
{
|
||||
user: obsSecRead,
|
||||
owners: ['observabilityFixture'],
|
||||
caseId: obsCase.id,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
},
|
||||
{
|
||||
user: obsSec,
|
||||
owners: ['securitySolutionFixture'],
|
||||
caseId: secCase.id,
|
||||
attachmentIds: [secAttachmentId],
|
||||
},
|
||||
{
|
||||
user: obsSec,
|
||||
owners: ['observabilityFixture'],
|
||||
caseId: obsCase.id,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
},
|
||||
{
|
||||
user: secOnly,
|
||||
owners: ['securitySolutionFixture'],
|
||||
caseId: secCase.id,
|
||||
attachmentIds: [secAttachmentId],
|
||||
},
|
||||
{
|
||||
user: obsOnly,
|
||||
owners: ['observabilityFixture'],
|
||||
caseId: obsCase.id,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
},
|
||||
]) {
|
||||
const { attachments, errors } = await bulkGetAttachments({
|
||||
supertest: supertestWithoutAuth,
|
||||
attachmentIds: scenario.attachmentIds,
|
||||
caseId: scenario.caseId,
|
||||
auth: { user: scenario.user, space: 'space1' },
|
||||
});
|
||||
|
||||
const numberOfCasesThatShouldBeReturned = 1;
|
||||
ensureSavedObjectIsAuthorized(
|
||||
attachments,
|
||||
numberOfCasesThatShouldBeReturned,
|
||||
scenario.owners
|
||||
);
|
||||
|
||||
expect(errors.length).to.be(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an association error when an observability attachment is requested for a security case', async () => {
|
||||
const { attachments, errors } = await bulkGetAttachments({
|
||||
supertest: supertestWithoutAuth,
|
||||
attachmentIds: [secAttachmentId, obsAttachmentId],
|
||||
caseId: secCase.id,
|
||||
auth: { user: secOnlyRead, space: 'space1' },
|
||||
});
|
||||
|
||||
expect(attachments.length).to.be(1);
|
||||
expect(attachments[0].owner).to.be('securitySolutionFixture');
|
||||
expect(errors.length).to.be(1);
|
||||
expect(errors[0]).to.eql({
|
||||
attachmentId: obsAttachmentId,
|
||||
error: 'Bad Request',
|
||||
message: `Attachment is not attached to case id=${secCase.id}`,
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an authorization error when a security solution user is attempting to retrieve an observability attachment', async () => {
|
||||
await bulkGetAttachments({
|
||||
supertest: supertestWithoutAuth,
|
||||
attachmentIds: [obsAttachmentId],
|
||||
caseId: obsCase.id,
|
||||
auth: { user: secOnlyRead, space: 'space1' },
|
||||
expectedHttpCode: 403,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return authorization error when a observability user requests a security attachment for a security case', async () => {
|
||||
await bulkGetAttachments({
|
||||
supertest: supertestWithoutAuth,
|
||||
attachmentIds: [secAttachmentId],
|
||||
caseId: secCase.id,
|
||||
auth: { user: obsOnlyRead, space: 'space1' },
|
||||
expectedHttpCode: 403,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for the observability attachment that is not associated to the case, and an error for an unknown attachment', async () => {
|
||||
const { attachments, errors } = await bulkGetAttachments({
|
||||
supertest: supertestWithoutAuth,
|
||||
attachmentIds: [obsAttachmentId, 'does-not-exist'],
|
||||
caseId: secCase.id,
|
||||
auth: { user: secOnly, space: 'space1' },
|
||||
});
|
||||
|
||||
expect(attachments.length).to.be(0);
|
||||
expect(errors.length).to.be(2);
|
||||
expect(errors[0]).to.eql({
|
||||
error: 'Not Found',
|
||||
message: 'Saved object [cases-comments/does-not-exist] not found',
|
||||
status: 404,
|
||||
attachmentId: 'does-not-exist',
|
||||
});
|
||||
expect(errors[1]).to.eql({
|
||||
attachmentId: obsAttachmentId,
|
||||
error: 'Bad Request',
|
||||
message: `Attachment is not attached to case id=${secCase.id}`,
|
||||
status: 400,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 bulk get attachments`, async () => {
|
||||
// super user creates a case at the appropriate space
|
||||
const newCase = await createCase(
|
||||
supertestWithoutAuth,
|
||||
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
|
||||
200,
|
||||
{
|
||||
user: superUser,
|
||||
space: scenario.space,
|
||||
}
|
||||
);
|
||||
|
||||
const patchedCase = await createComment({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseId: newCase.id,
|
||||
params: postCommentUserReq,
|
||||
auth: { user: superUser, space: scenario.space },
|
||||
});
|
||||
|
||||
await bulkGetAttachments({
|
||||
supertest: supertestWithoutAuth,
|
||||
attachmentIds: [patchedCase.comments![0].id],
|
||||
caseId: newCase.id,
|
||||
auth: {
|
||||
user: scenario.user,
|
||||
space: scenario.space,
|
||||
},
|
||||
expectedHttpCode: 403,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue