mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Cases] Validate attributes before create SOs (#158590)
## Summary This PR uses `io-ts`'s `decode` utitlity function to validate the attributes before creating cases saved objects. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0ea97e2e8c
commit
b5adaecb6a
22 changed files with 785 additions and 119 deletions
|
@ -184,15 +184,21 @@ const CommentAttributesWithoutRefsRt = rt.union([
|
|||
AttributesTypePersistableStateRt,
|
||||
]);
|
||||
|
||||
export const CommentRequestRt = rt.union([
|
||||
const BasicCommentRequestRt = rt.union([
|
||||
ContextTypeUserRt,
|
||||
AlertCommentRequestRt,
|
||||
ActionsCommentRequestRt,
|
||||
ExternalReferenceNoSORt,
|
||||
ExternalReferenceSORt,
|
||||
PersistableStateAttachmentRt,
|
||||
]);
|
||||
|
||||
export const CommentRequestRt = rt.union([BasicCommentRequestRt, ExternalReferenceSORt]);
|
||||
|
||||
export const CommentRequestWithoutRefsRt = rt.union([
|
||||
BasicCommentRequestRt,
|
||||
ExternalReferenceSOWithoutRefsRt,
|
||||
]);
|
||||
|
||||
export const CommentRt = rt.intersection([
|
||||
CommentAttributesRt,
|
||||
rt.strict({
|
||||
|
|
|
@ -6,15 +6,26 @@
|
|||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { CommentRequestRt } from '../comment';
|
||||
import { CommentRequestRt, CommentRequestWithoutRefsRt } from '../comment';
|
||||
import type { UserActionWithAttributes } from './common';
|
||||
import { ActionTypes } from './common';
|
||||
|
||||
export const CommentUserActionPayloadRt = rt.strict({ comment: CommentRequestRt });
|
||||
export const CommentUserActionPayloadWithoutIdsRt = rt.strict({
|
||||
comment: CommentRequestWithoutRefsRt,
|
||||
});
|
||||
|
||||
export const CommentUserActionRt = rt.strict({
|
||||
type: rt.literal(ActionTypes.comment),
|
||||
payload: CommentUserActionPayloadRt,
|
||||
});
|
||||
|
||||
export const CommentUserActionWithoutIdsRt = rt.strict({
|
||||
type: rt.literal(ActionTypes.comment),
|
||||
payload: CommentUserActionPayloadWithoutIdsRt,
|
||||
});
|
||||
|
||||
export type CommentUserAction = UserActionWithAttributes<rt.TypeOf<typeof CommentUserActionRt>>;
|
||||
export type CommentUserActionPayloadWithoutIds = UserActionWithAttributes<
|
||||
rt.TypeOf<typeof CommentUserActionPayloadWithoutIdsRt>
|
||||
>;
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from './common';
|
||||
import { CreateCaseUserActionRt, CreateCaseUserActionWithoutConnectorIdRt } from './create_case';
|
||||
import { DescriptionUserActionRt } from './description';
|
||||
import { CommentUserActionRt } from './comment';
|
||||
import { CommentUserActionRt, CommentUserActionWithoutIdsRt } from './comment';
|
||||
import { ConnectorUserActionRt, ConnectorUserActionWithoutConnectorIdRt } from './connector';
|
||||
import { PushedUserActionRt, PushedUserActionWithoutConnectorIdRt } from './pushed';
|
||||
import { TagsUserActionRt } from './tags';
|
||||
|
@ -28,36 +28,39 @@ import { SeverityUserActionRt } from './severity';
|
|||
import { AssigneesUserActionRt } from './assignees';
|
||||
import { CaseUserActionStatsRt } from './stats';
|
||||
|
||||
const CommonUserActionsRt = rt.union([
|
||||
const BasicUserActionsRt = rt.union([
|
||||
DescriptionUserActionRt,
|
||||
CommentUserActionRt,
|
||||
TagsUserActionRt,
|
||||
TitleUserActionRt,
|
||||
SettingsUserActionRt,
|
||||
StatusUserActionRt,
|
||||
SeverityUserActionRt,
|
||||
AssigneesUserActionRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
const CommonUserActionsWithIdsRt = rt.union([BasicUserActionsRt, CommentUserActionRt]);
|
||||
|
||||
const CommonUserActionsWithoutIdsRt = rt.union([BasicUserActionsRt, CommentUserActionWithoutIdsRt]);
|
||||
|
||||
const UserActionPayloadRt = rt.union([
|
||||
CommonUserActionsRt,
|
||||
CommonUserActionsWithIdsRt,
|
||||
CreateCaseUserActionRt,
|
||||
ConnectorUserActionRt,
|
||||
PushedUserActionRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
const UserActionsWithoutConnectorIdRt = rt.union([
|
||||
CommonUserActionsRt,
|
||||
const UserActionsWithoutIdsRt = rt.union([
|
||||
CommonUserActionsWithoutIdsRt,
|
||||
CreateCaseUserActionWithoutConnectorIdRt,
|
||||
ConnectorUserActionWithoutConnectorIdRt,
|
||||
PushedUserActionWithoutConnectorIdRt,
|
||||
DeleteCaseUserActionRt,
|
||||
]);
|
||||
|
||||
const CaseUserActionBasicRt = rt.intersection([UserActionPayloadRt, UserActionCommonAttributesRt]);
|
||||
const CaseUserActionBasicWithoutConnectorIdRt = rt.intersection([
|
||||
UserActionsWithoutConnectorIdRt,
|
||||
|
||||
export const CaseUserActionWithoutReferenceIdsRt = rt.intersection([
|
||||
UserActionsWithoutIdsRt,
|
||||
UserActionCommonAttributesRt,
|
||||
]);
|
||||
|
||||
|
@ -86,8 +89,8 @@ export const UserActionsRt = rt.array(UserActionRt);
|
|||
export const CaseUserActionsDeprecatedResponseRt = rt.array(CaseUserActionDeprecatedResponseRt);
|
||||
export const CaseUserActionStatsResponseRt = CaseUserActionStatsRt;
|
||||
|
||||
export type CaseUserActionAttributesWithoutConnectorId = rt.TypeOf<
|
||||
typeof CaseUserActionBasicWithoutConnectorIdRt
|
||||
export type CaseUserActionWithoutReferenceIds = rt.TypeOf<
|
||||
typeof CaseUserActionWithoutReferenceIdsRt
|
||||
>;
|
||||
export type CaseUserActionStatsResponse = rt.TypeOf<typeof CaseUserActionStatsRt>;
|
||||
export type UserActions = rt.TypeOf<typeof UserActionsRt>;
|
||||
|
|
|
@ -9,7 +9,6 @@ import type {
|
|||
Comment,
|
||||
CommentResponseAlertsType,
|
||||
CaseUserActionsDeprecatedResponse,
|
||||
ConnectorMappings,
|
||||
} from '../../../common/api';
|
||||
import {
|
||||
CommentType,
|
||||
|
@ -242,24 +241,6 @@ export const basicParams = {
|
|||
...entity,
|
||||
};
|
||||
|
||||
export const mappings: ConnectorMappings = [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'append',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
|
||||
export const userActions: CaseUserActionsDeprecatedResponse = [
|
||||
{
|
||||
action: Actions.create,
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
userActions,
|
||||
commentAlert,
|
||||
commentAlertMultipleIds,
|
||||
mappings,
|
||||
isolateCommentActions,
|
||||
releaseCommentActions,
|
||||
isolateCommentActionsMultipleTargets,
|
||||
|
@ -34,7 +33,7 @@ import { flattenCaseSavedObject } from '../../common/utils';
|
|||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { casesConnectors } from '../../connectors';
|
||||
import { userProfiles, userProfilesMap } from '../user_profiles.mock';
|
||||
import { mockCases } from '../../mocks';
|
||||
import { mappings, mockCases } from '../../mocks';
|
||||
|
||||
const allComments = [
|
||||
commentObj,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { SavedObject } from '@kbn/core/server';
|
||||
import type { UserActionAttributes } from '../../../common/api';
|
||||
import { UserActionAttributesRt } from '../../../common/api';
|
||||
import { CaseUserActionWithoutReferenceIdsRt, UserActionAttributesRt } from '../../../common/api';
|
||||
import type { User } from './user';
|
||||
|
||||
interface UserActionCommonPersistedAttributes {
|
||||
|
@ -23,6 +23,7 @@ export interface UserActionPersistedAttributes extends UserActionCommonPersisted
|
|||
}
|
||||
|
||||
export const UserActionTransformedAttributesRt = UserActionAttributesRt;
|
||||
export const UserActionPersistedAttributesRt = CaseUserActionWithoutReferenceIdsRt;
|
||||
|
||||
export type UserActionTransformedAttributes = UserActionAttributes;
|
||||
export type UserActionSavedObjectTransformed = SavedObject<UserActionTransformedAttributes>;
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
CommentAttributes,
|
||||
CommentRequestAlertType,
|
||||
CommentRequestUserType,
|
||||
ConnectorMappings,
|
||||
} from '../common/api';
|
||||
import { CaseSeverity, CaseStatuses, CommentType, ConnectorTypes } from '../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../common/constants';
|
||||
|
@ -448,6 +449,24 @@ export const multipleAlert: CommentRequestAlertType = {
|
|||
index: ['test-index-3', 'test-index-4', 'test-index-5'],
|
||||
};
|
||||
|
||||
export const mappings: ConnectorMappings = [
|
||||
{
|
||||
source: 'title',
|
||||
target: 'short_description',
|
||||
action_type: 'overwrite',
|
||||
},
|
||||
{
|
||||
source: 'description',
|
||||
target: 'description',
|
||||
action_type: 'append',
|
||||
},
|
||||
{
|
||||
source: 'comments',
|
||||
target: 'comments',
|
||||
action_type: 'append',
|
||||
},
|
||||
];
|
||||
|
||||
const casesClientMock = createCasesClientMock();
|
||||
|
||||
export const mockCasesContract = (): CasesStart => ({
|
||||
|
|
|
@ -13,7 +13,7 @@ import type {
|
|||
SavedObjectsExportTransformContext,
|
||||
} from '@kbn/core/server';
|
||||
import type {
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionWithoutReferenceIds,
|
||||
CommentAttributesWithoutRefs,
|
||||
} from '../../../common/api';
|
||||
import {
|
||||
|
@ -40,9 +40,7 @@ export async function handleExport({
|
|||
}): Promise<
|
||||
Array<
|
||||
SavedObject<
|
||||
| CasePersistedAttributes
|
||||
| CommentAttributesWithoutRefs
|
||||
| CaseUserActionAttributesWithoutConnectorId
|
||||
CasePersistedAttributes | CommentAttributesWithoutRefs | CaseUserActionWithoutReferenceIds
|
||||
>
|
||||
>
|
||||
> {
|
||||
|
@ -75,9 +73,7 @@ export async function handleExport({
|
|||
async function getAttachmentsAndUserActionsForCases(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
caseIds: string[]
|
||||
): Promise<
|
||||
Array<SavedObject<CommentAttributesWithoutRefs | CaseUserActionAttributesWithoutConnectorId>>
|
||||
> {
|
||||
): Promise<Array<SavedObject<CommentAttributesWithoutRefs | CaseUserActionWithoutReferenceIds>>> {
|
||||
const [attachments, userActions] = await Promise.all([
|
||||
getAssociatedObjects<CommentAttributesWithoutRefs>({
|
||||
savedObjectsClient,
|
||||
|
@ -85,7 +81,7 @@ async function getAttachmentsAndUserActionsForCases(
|
|||
sortField: defaultSortField,
|
||||
type: CASE_COMMENT_SAVED_OBJECT,
|
||||
}),
|
||||
getAssociatedObjects<CaseUserActionAttributesWithoutConnectorId>({
|
||||
getAssociatedObjects<CaseUserActionWithoutReferenceIds>({
|
||||
savedObjectsClient,
|
||||
caseIds,
|
||||
sortField: defaultSortField,
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('AttachmentService', () => {
|
|||
|
||||
describe('create', () => {
|
||||
describe('Decoding', () => {
|
||||
it('does not throw when the response has the required fields', async () => {
|
||||
it('does not throw when the response and the request has the required fields', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue(createUserAttachment());
|
||||
|
||||
await expect(
|
||||
|
@ -54,7 +54,7 @@ describe('AttachmentService', () => {
|
|||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('strips excess fields', async () => {
|
||||
it('strips excess fields from the response', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue(createUserAttachment({ foo: 'bar' }));
|
||||
|
||||
const res = await service.create({
|
||||
|
@ -82,12 +82,43 @@ describe('AttachmentService', () => {
|
|||
`"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the request is missing the attributes.comment', async () => {
|
||||
const invalidAttachment = createUserAttachment();
|
||||
unset(invalidAttachment, 'attributes.comment');
|
||||
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue(createUserAttachment());
|
||||
|
||||
await expect(
|
||||
service.create({
|
||||
attributes: invalidAttachment.attributes,
|
||||
references: [],
|
||||
id: '1',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('strips excess fields from the request', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue(createUserAttachment());
|
||||
|
||||
await service.create({
|
||||
// @ts-expect-error: excess attributes
|
||||
attributes: { ...createUserAttachment().attributes, foo: 'bar' },
|
||||
references: [],
|
||||
id: '1',
|
||||
});
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreate', () => {
|
||||
describe('Decoding', () => {
|
||||
it('does not throw when the response has the required fields', async () => {
|
||||
it('does not throw when the response and the request has the required fields', async () => {
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [createUserAttachment()],
|
||||
});
|
||||
|
@ -150,6 +181,43 @@ describe('AttachmentService', () => {
|
|||
`"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the request is missing the attributes.comment', async () => {
|
||||
const invalidAttachment = createUserAttachment();
|
||||
unset(invalidAttachment, 'attributes.comment');
|
||||
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [createUserAttachment()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.bulkCreate({
|
||||
attachments: [{ attributes: invalidAttachment.attributes, references: [], id: '1' }],
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('strips excess fields from the request', async () => {
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [createUserAttachment()],
|
||||
});
|
||||
|
||||
await service.bulkCreate({
|
||||
attachments: [
|
||||
{
|
||||
// @ts-expect-error: excess attributes
|
||||
attributes: { ...createUserAttachment().attributes, foo: 'bar' },
|
||||
references: [],
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0];
|
||||
expect(persistedAttributes.attributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -205,7 +273,7 @@ describe('AttachmentService', () => {
|
|||
});
|
||||
|
||||
describe('Decoding', () => {
|
||||
it('does not throw when the response has the required fields', async () => {
|
||||
it('does not throw when the response and the request has the required fields', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValue(createUserAttachment());
|
||||
|
||||
await expect(
|
||||
|
@ -235,13 +303,42 @@ describe('AttachmentService', () => {
|
|||
|
||||
await expect(
|
||||
service.update({
|
||||
updatedAttributes: createUserAttachment().attributes,
|
||||
updatedAttributes: createAlertAttachment().attributes,
|
||||
attachmentId: '1',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid value \\"alert\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"rule,name\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the request is missing the attributes.rule.name', async () => {
|
||||
const invalidAttachment = createAlertAttachment();
|
||||
unset(invalidAttachment, 'attributes.rule.name');
|
||||
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValue(createAlertAttachment());
|
||||
|
||||
await expect(
|
||||
service.update({
|
||||
updatedAttributes: invalidAttachment.attributes,
|
||||
attachmentId: '1',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid value \\"alert\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"rule,name\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('strips excess fields from the request', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValue(createUserAttachment());
|
||||
|
||||
await service.update({
|
||||
// @ts-expect-error: excess attributes
|
||||
updatedAttributes: { ...createUserAttachment().attributes, foo: 'bar' },
|
||||
attachmentId: '1',
|
||||
});
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.update.mock.calls[0][2];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -299,7 +396,7 @@ describe('AttachmentService', () => {
|
|||
});
|
||||
|
||||
describe('Decoding', () => {
|
||||
it('does not throw when the response has the required fields', async () => {
|
||||
it('does not throw when the response and the request has the required fields', async () => {
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({
|
||||
saved_objects: [createUserAttachment()],
|
||||
});
|
||||
|
@ -360,6 +457,49 @@ describe('AttachmentService', () => {
|
|||
`"Invalid value \\"alert\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"rule,name\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the request is missing the attributes.rule.name', async () => {
|
||||
const invalidAttachment = createAlertAttachment();
|
||||
unset(invalidAttachment, 'attributes.rule.name');
|
||||
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({
|
||||
saved_objects: [createAlertAttachment()],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.bulkUpdate({
|
||||
comments: [
|
||||
{
|
||||
updatedAttributes: invalidAttachment.attributes,
|
||||
attachmentId: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid value \\"alert\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"rule,name\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('strips excess fields from the request', async () => {
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({
|
||||
saved_objects: [createUserAttachment()],
|
||||
});
|
||||
|
||||
await service.bulkUpdate({
|
||||
comments: [
|
||||
{
|
||||
// @ts-expect-error: excess attributes
|
||||
updatedAttributes: { ...createUserAttachment().attributes, foo: 'bar' },
|
||||
attachmentId: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const persistedAttributes =
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mock.calls[0][0][0].attributes;
|
||||
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import type {
|
|||
} from '@kbn/core/server';
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { CommentType, decodeOrThrow } from '../../../common/api';
|
||||
import { CommentAttributesRt, CommentType, decodeOrThrow } from '../../../common/api';
|
||||
import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../common/constants';
|
||||
import { buildFilter, combineFilters } from '../../client/utils';
|
||||
import { defaultSortField, isSOError } from '../../common/utils';
|
||||
|
@ -167,9 +167,11 @@ export class AttachmentService {
|
|||
try {
|
||||
this.context.log.debug(`Attempting to POST a new comment`);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(CommentAttributesRt)(attributes);
|
||||
|
||||
const { attributes: extractedAttributes, references: extractedReferences } =
|
||||
extractAttachmentSORefsFromAttributes(
|
||||
attributes,
|
||||
decodedAttributes,
|
||||
references,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
@ -210,9 +212,11 @@ export class AttachmentService {
|
|||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.bulkCreate<AttachmentPersistedAttributes>(
|
||||
attachments.map((attachment) => {
|
||||
const decodedAttributes = decodeOrThrow(CommentAttributesRt)(attachment.attributes);
|
||||
|
||||
const { attributes: extractedAttributes, references: extractedReferences } =
|
||||
extractAttachmentSORefsFromAttributes(
|
||||
attachment.attributes,
|
||||
decodedAttributes,
|
||||
attachment.references,
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
@ -267,12 +271,14 @@ export class AttachmentService {
|
|||
try {
|
||||
this.context.log.debug(`Attempting to UPDATE comment ${attachmentId}`);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(AttachmentPartialAttributesRt)(updatedAttributes);
|
||||
|
||||
const {
|
||||
attributes: extractedAttributes,
|
||||
references: extractedReferences,
|
||||
didDeleteOperation,
|
||||
} = extractAttachmentSORefsFromAttributes(
|
||||
updatedAttributes,
|
||||
decodedAttributes,
|
||||
options?.references ?? [],
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
@ -327,12 +333,16 @@ export class AttachmentService {
|
|||
const res =
|
||||
await this.context.unsecuredSavedObjectsClient.bulkUpdate<AttachmentPersistedAttributes>(
|
||||
comments.map((c) => {
|
||||
const decodedAttributes = decodeOrThrow(AttachmentPartialAttributesRt)(
|
||||
c.updatedAttributes
|
||||
);
|
||||
|
||||
const {
|
||||
attributes: extractedAttributes,
|
||||
references: extractedReferences,
|
||||
didDeleteOperation,
|
||||
} = extractAttachmentSORefsFromAttributes(
|
||||
c.updatedAttributes,
|
||||
decodedAttributes,
|
||||
c.options?.references ?? [],
|
||||
this.context.persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
|
|
@ -2705,4 +2705,134 @@ describe('CasesService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Decoding requests', () => {
|
||||
describe('create case', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue(createCaseSavedObjectResponse());
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const attributes = createCasePostParams({ connector: createJiraConnector() });
|
||||
|
||||
await expect(service.postNewCase({ id: 'a', attributes })).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if title is omitted', async () => {
|
||||
const attributes = createCasePostParams({ connector: createJiraConnector() });
|
||||
unset(attributes, 'title');
|
||||
|
||||
await expect(
|
||||
service.postNewCase({
|
||||
attributes,
|
||||
id: '1',
|
||||
})
|
||||
).rejects.toThrow(`Invalid value "undefined" supplied to "title"`);
|
||||
});
|
||||
|
||||
it('remove excess fields', async () => {
|
||||
const attributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
await expect(service.postNewCase({ id: 'a', attributes })).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch case', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValue(
|
||||
{} as SavedObjectsUpdateResponse<CasePersistedAttributes>
|
||||
);
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const updatedAttributes = createCasePostParams({
|
||||
connector: createJiraConnector(),
|
||||
status: CaseStatuses['in-progress'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.patchCase({
|
||||
caseId: '1',
|
||||
updatedAttributes,
|
||||
originalCase: {} as CaseSavedObjectTransformed,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('remove excess fields', async () => {
|
||||
const updatedAttributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.patchCase({
|
||||
caseId: '1',
|
||||
updatedAttributes,
|
||||
originalCase: {} as CaseSavedObjectTransformed,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.update.mock.calls[0][2];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch cases', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({
|
||||
saved_objects: [createCaseSavedObjectResponse({ caseId: '1' })],
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const updatedAttributes = createCasePostParams({
|
||||
connector: createJiraConnector(),
|
||||
status: CaseStatuses['in-progress'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.patchCases({
|
||||
cases: [
|
||||
{
|
||||
caseId: '1',
|
||||
updatedAttributes,
|
||||
originalCase: {} as CaseSavedObjectTransformed,
|
||||
},
|
||||
],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('remove excess fields', async () => {
|
||||
const updatedAttributes = {
|
||||
...createCasePostParams({ connector: createJiraConnector() }),
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.patchCases({
|
||||
cases: [
|
||||
{
|
||||
caseId: '1',
|
||||
updatedAttributes,
|
||||
originalCase: {} as CaseSavedObjectTransformed,
|
||||
},
|
||||
],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes =
|
||||
unsecuredSavedObjectsClient.bulkUpdate.mock.calls[0][0][0].attributes;
|
||||
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -73,6 +73,8 @@ import type {
|
|||
import type { AttachmentTransformedAttributes } from '../../common/types/attachments';
|
||||
import { bulkDecodeSOAttributes } from '../utils';
|
||||
|
||||
const PartialCaseTransformedAttributesRt = getPartialCaseTransformedAttributesRt();
|
||||
|
||||
export class CasesService {
|
||||
private readonly log: Logger;
|
||||
private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract;
|
||||
|
@ -562,7 +564,9 @@ export class CasesService {
|
|||
}: PostCaseArgs): Promise<CaseSavedObjectTransformed> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new case`);
|
||||
const transformedAttributes = transformAttributesToESModel(attributes);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(CaseTransformedAttributesRt)(attributes);
|
||||
const transformedAttributes = transformAttributesToESModel(decodedAttributes);
|
||||
|
||||
transformedAttributes.attributes.total_alerts = -1;
|
||||
transformedAttributes.attributes.total_comments = -1;
|
||||
|
@ -592,7 +596,11 @@ export class CasesService {
|
|||
}: PatchCaseArgs): Promise<SavedObjectsUpdateResponse<CaseTransformedAttributes>> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE case ${caseId}`);
|
||||
const transformedAttributes = transformAttributesToESModel(updatedAttributes);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(PartialCaseTransformedAttributesRt)(
|
||||
updatedAttributes
|
||||
);
|
||||
const transformedAttributes = transformAttributesToESModel(decodedAttributes);
|
||||
|
||||
const updatedCase = await this.unsecuredSavedObjectsClient.update<CasePersistedAttributes>(
|
||||
CASE_SAVED_OBJECT,
|
||||
|
@ -606,7 +614,7 @@ export class CasesService {
|
|||
);
|
||||
|
||||
const res = transformUpdateResponseToExternalModel(updatedCase);
|
||||
const decodeRes = decodeOrThrow(getPartialCaseTransformedAttributesRt())(res.attributes);
|
||||
const decodeRes = decodeOrThrow(PartialCaseTransformedAttributesRt)(res.attributes);
|
||||
|
||||
return {
|
||||
...res,
|
||||
|
@ -626,7 +634,11 @@ export class CasesService {
|
|||
this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`);
|
||||
|
||||
const bulkUpdate = cases.map(({ caseId, updatedAttributes, version, originalCase }) => {
|
||||
const { attributes, referenceHandler } = transformAttributesToESModel(updatedAttributes);
|
||||
const decodedAttributes = decodeOrThrow(PartialCaseTransformedAttributesRt)(
|
||||
updatedAttributes
|
||||
);
|
||||
|
||||
const { attributes, referenceHandler } = transformAttributesToESModel(decodedAttributes);
|
||||
return {
|
||||
type: CASE_SAVED_OBJECT,
|
||||
id: caseId,
|
||||
|
@ -648,7 +660,7 @@ export class CasesService {
|
|||
}
|
||||
|
||||
const so = Object.assign(theCase, transformUpdateResponseToExternalModel(theCase));
|
||||
const decodeRes = decodeOrThrow(getPartialCaseTransformedAttributesRt())(so.attributes);
|
||||
const decodeRes = decodeOrThrow(PartialCaseTransformedAttributesRt)(so.attributes);
|
||||
const soWithDecodedRes = Object.assign(so, { attributes: decodeRes });
|
||||
|
||||
acc.push(soWithDecodedRes);
|
||||
|
|
|
@ -29,6 +29,7 @@ import { getNoneCaseConnector } from '../../common/utils';
|
|||
import type { ESCaseConnectorWithId } from '../test_utils';
|
||||
import { createESJiraConnector, createJiraConnector } from '../test_utils';
|
||||
import type { ConfigurationPersistedAttributes } from '../../common/types/configure';
|
||||
import { unset } from 'lodash';
|
||||
|
||||
const basicConfigFields = {
|
||||
closure_type: 'close-by-pushing' as const,
|
||||
|
@ -744,4 +745,94 @@ describe('CaseConfigureService', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Decoding requests', () => {
|
||||
describe('post', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue({
|
||||
attributes: createConfigPostParams(createJiraConnector()),
|
||||
id: '1',
|
||||
type: CASE_CONFIGURE_SAVED_OBJECT,
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const attributes = createConfigPostParams(createJiraConnector());
|
||||
|
||||
await expect(
|
||||
service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
id: '1',
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if closure_type is omitted', async () => {
|
||||
const attributes = createConfigPostParams(createJiraConnector());
|
||||
unset(attributes, 'closure_type');
|
||||
|
||||
await expect(
|
||||
service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
id: '1',
|
||||
})
|
||||
).rejects.toThrow(`Invalid value "undefined" supplied to "closure_type"`);
|
||||
});
|
||||
|
||||
it('strips out excess attributes', async () => {
|
||||
const attributes = { ...createConfigPostParams(createJiraConnector()), foo: 'bar' };
|
||||
|
||||
await expect(
|
||||
service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
id: '1',
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.update.mockReturnValue(
|
||||
Promise.resolve({} as SavedObjectsUpdateResponse<ConfigurationPatchRequest>)
|
||||
);
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const updatedAttributes = createConfigPostParams(createJiraConnector());
|
||||
|
||||
await expect(
|
||||
service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes,
|
||||
originalConfiguration: {} as SavedObject<ConfigurationAttributes>,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('strips out excess attributes', async () => {
|
||||
const updatedAttributes = { ...createConfigPostParams(createJiraConnector()), foo: 'bar' };
|
||||
|
||||
await expect(
|
||||
service.patch({
|
||||
configurationId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
updatedAttributes,
|
||||
originalConfiguration: {} as SavedObject<ConfigurationAttributes>,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.update.mock.calls[0][2];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -118,7 +118,10 @@ export class CaseConfigureService {
|
|||
}: PostCaseConfigureArgs): Promise<ConfigurationSavedObjectTransformed> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new case configuration`);
|
||||
const esConfigInfo = transformAttributesToESModel(attributes);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(ConfigurationTransformedAttributesRt)(attributes);
|
||||
const esConfigInfo = transformAttributesToESModel(decodedAttributes);
|
||||
|
||||
const createdConfig =
|
||||
await unsecuredSavedObjectsClient.create<ConfigurationPersistedAttributes>(
|
||||
CASE_CONFIGURE_SAVED_OBJECT,
|
||||
|
@ -144,7 +147,9 @@ export class CaseConfigureService {
|
|||
> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`);
|
||||
const esUpdateInfo = transformAttributesToESModel(updatedAttributes);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(ConfigurationPartialAttributesRt)(updatedAttributes);
|
||||
const esUpdateInfo = transformAttributesToESModel(decodedAttributes);
|
||||
|
||||
const updatedConfiguration =
|
||||
await unsecuredSavedObjectsClient.update<ConfigurationPersistedAttributes>(
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants';
|
||||
import type { SavedObjectsUpdateResponse } from '@kbn/core-saved-objects-api-server';
|
||||
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { unset } from 'lodash';
|
||||
import { ConnectorMappingsService } from '.';
|
||||
import { mappings } from '../../mocks';
|
||||
import type { ConnectorMappingsPersistedAttributes } from '../../common/types/connector_mappings';
|
||||
|
||||
describe('CaseConfigureService', () => {
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const mockLogger = loggerMock.create();
|
||||
|
||||
let service: ConnectorMappingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
service = new ConnectorMappingsService(mockLogger);
|
||||
});
|
||||
|
||||
describe('Decoding requests', () => {
|
||||
describe('post', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue({
|
||||
attributes: { mappings, owner: 'cases' },
|
||||
id: '1',
|
||||
type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const attributes = { mappings, owner: 'cases' };
|
||||
|
||||
await expect(
|
||||
service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
references: [],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if mappings is omitted', async () => {
|
||||
const attributes = { mappings, owner: 'cases' };
|
||||
unset(attributes, 'mappings');
|
||||
|
||||
await expect(
|
||||
service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
references: [],
|
||||
})
|
||||
).rejects.toThrow(`Invalid value "undefined" supplied to "mappings"`);
|
||||
});
|
||||
|
||||
it('strips out excess attributes', async () => {
|
||||
const attributes = { mappings, owner: 'cases', foo: 'bar' };
|
||||
|
||||
await expect(
|
||||
service.post({
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes,
|
||||
references: [],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock.calls[0][1];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValue({
|
||||
attributes: { mappings, owner: 'cases' },
|
||||
} as SavedObjectsUpdateResponse<ConnectorMappingsPersistedAttributes>);
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
const updatedAttributes = { mappings, owner: 'cases' };
|
||||
|
||||
await expect(
|
||||
service.update({
|
||||
mappingId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: updatedAttributes,
|
||||
references: [],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('strips out excess attributes', async () => {
|
||||
const updatedAttributes = { mappings, owner: 'cases', foo: 'bar' };
|
||||
|
||||
await expect(
|
||||
service.update({
|
||||
mappingId: '1',
|
||||
unsecuredSavedObjectsClient,
|
||||
attributes: updatedAttributes,
|
||||
references: [],
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.update.mock.calls[0][2];
|
||||
expect(persistedAttributes).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -73,10 +73,13 @@ export class ConnectorMappingsService {
|
|||
}: PostConnectorMappingsArgs): Promise<ConnectorMappingsSavedObjectTransformed> {
|
||||
try {
|
||||
this.log.debug(`Attempting to POST a new connector mappings`);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(ConnectorMappingsAttributesTransformedRt)(attributes);
|
||||
|
||||
const connectorMappings =
|
||||
await unsecuredSavedObjectsClient.create<ConnectorMappingsPersistedAttributes>(
|
||||
CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
attributes,
|
||||
decodedAttributes,
|
||||
{
|
||||
references,
|
||||
refresh,
|
||||
|
@ -105,11 +108,14 @@ export class ConnectorMappingsService {
|
|||
> {
|
||||
try {
|
||||
this.log.debug(`Attempting to UPDATE connector mappings ${mappingId}`);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(ConnectorMappingsAttributesPartialRt)(attributes);
|
||||
|
||||
const updatedMappings =
|
||||
await unsecuredSavedObjectsClient.update<ConnectorMappingsPersistedAttributes>(
|
||||
CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT,
|
||||
mappingId,
|
||||
attributes,
|
||||
decodedAttributes,
|
||||
{
|
||||
references,
|
||||
refresh,
|
||||
|
|
|
@ -15,10 +15,7 @@ import type {
|
|||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import type {
|
||||
CaseAttributes,
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
} from '../../../common/api';
|
||||
import type { CaseAttributes, CaseUserActionWithoutReferenceIds } from '../../../common/api';
|
||||
import { Actions, ActionTypes, CaseSeverity, CaseStatuses } from '../../../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
|
||||
|
@ -1654,9 +1651,7 @@ describe('CaseUserActionService', () => {
|
|||
});
|
||||
|
||||
describe('getConnectorFieldsBeforeLatestPush', () => {
|
||||
const getAggregations = (
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>
|
||||
) => {
|
||||
const getAggregations = (userAction: SavedObject<CaseUserActionWithoutReferenceIds>) => {
|
||||
const connectors = set({}, 'servicenow.mostRecent.hits.hits', [userAction]);
|
||||
|
||||
const aggregations = set({}, 'references.connectors.reverse.ids.buckets', connectors);
|
||||
|
@ -1861,8 +1856,8 @@ describe('CaseUserActionService', () => {
|
|||
|
||||
describe('getCaseConnectorInformation', () => {
|
||||
const getAggregations = (
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>,
|
||||
pushUserAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>
|
||||
userAction: SavedObject<CaseUserActionWithoutReferenceIds>,
|
||||
pushUserAction: SavedObject<CaseUserActionWithoutReferenceIds>
|
||||
) => {
|
||||
const changeConnector = set({}, 'mostRecent.hits.hits', [userAction]);
|
||||
const createCase = set({}, 'mostRecent.hits.hits', []);
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 { CASE_USER_ACTION_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { PersistableStateAttachmentTypeRegistry } from '../../../attachment_framework/persistable_state_registry';
|
||||
import { createSavedObjectsSerializerMock } from '../../../client/mocks';
|
||||
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { set, unset } from 'lodash';
|
||||
import { createConnectorObject } from '../../test_utils';
|
||||
import { UserActionPersister } from './create';
|
||||
import { createUserActionSO } from '../test_utils';
|
||||
import type { BulkCreateAttachmentUserAction, CreateUserActionClient } from '../types';
|
||||
import type { UserActionPersistedAttributes } from '../../../common/types/user_actions';
|
||||
import { CommentType } from '../../../../common';
|
||||
|
||||
describe('UserActionPersister', () => {
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const mockLogger = loggerMock.create();
|
||||
const auditMockLocker = auditLoggerMock.create();
|
||||
const persistableStateAttachmentTypeRegistry = new PersistableStateAttachmentTypeRegistry();
|
||||
const savedObjectsSerializer = createSavedObjectsSerializerMock();
|
||||
|
||||
let persister: UserActionPersister;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
persister = new UserActionPersister({
|
||||
log: mockLogger,
|
||||
unsecuredSavedObjectsClient,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
savedObjectsSerializer,
|
||||
auditLogger: auditMockLocker,
|
||||
});
|
||||
});
|
||||
|
||||
const getRequest = () =>
|
||||
({
|
||||
action: 'update' as const,
|
||||
type: 'connector' as const,
|
||||
caseId: 'test',
|
||||
payload: { connector: createConnectorObject().connector },
|
||||
connectorId: '1',
|
||||
owner: 'cases',
|
||||
user: { email: '', full_name: '', username: '' },
|
||||
} as CreateUserActionClient<'connector'>);
|
||||
|
||||
const getBulkCreateAttachmentRequest = (): BulkCreateAttachmentUserAction => ({
|
||||
caseId: 'test',
|
||||
attachments: [
|
||||
{
|
||||
id: '1',
|
||||
owner: 'cases',
|
||||
attachment: { comment: 'test', type: CommentType.user, owner: 'cases' },
|
||||
},
|
||||
],
|
||||
user: { email: '', full_name: '', username: '' },
|
||||
});
|
||||
|
||||
describe('Decoding requests', () => {
|
||||
describe('createUserAction', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValue({
|
||||
attributes: createUserActionSO(),
|
||||
id: '1',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
await expect(persister.createUserAction(getRequest())).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if fields is omitted', async () => {
|
||||
const req = getRequest();
|
||||
unset(req, 'payload.connector.fields');
|
||||
|
||||
await expect(persister.createUserAction(req)).rejects.toThrow(
|
||||
'Invalid value "undefined" supplied to "payload,connector,fields"'
|
||||
);
|
||||
});
|
||||
|
||||
it('strips out excess attributes', async () => {
|
||||
const req = getRequest();
|
||||
set(req, 'payload.foo', 'bar');
|
||||
|
||||
await expect(persister.createUserAction(req)).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.create.mock
|
||||
.calls[0][1] as UserActionPersistedAttributes;
|
||||
|
||||
expect(persistedAttributes.payload).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAttachmentCreation', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
attributes: createUserActionSO(),
|
||||
id: '1',
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes correctly the requested attributes', async () => {
|
||||
await expect(
|
||||
persister.bulkCreateAttachmentCreation(getBulkCreateAttachmentRequest())
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if owner is omitted', async () => {
|
||||
const req = getBulkCreateAttachmentRequest();
|
||||
unset(req, 'attachments[0].owner');
|
||||
|
||||
await expect(persister.bulkCreateAttachmentCreation(req)).rejects.toThrow(
|
||||
'Invalid value "undefined" supplied to "owner"'
|
||||
);
|
||||
});
|
||||
|
||||
it('strips out excess attributes', async () => {
|
||||
const req = getBulkCreateAttachmentRequest();
|
||||
set(req, 'attachments[0].attachment.foo', 'bar');
|
||||
|
||||
await expect(persister.bulkCreateAttachmentCreation(req)).resolves.not.toThrow();
|
||||
|
||||
const persistedAttributes = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0]
|
||||
.attributes as UserActionPersistedAttributes;
|
||||
|
||||
expect(persistedAttributes.payload).not.toHaveProperty('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@
|
|||
import type { SavedObject, SavedObjectsBulkResponse } from '@kbn/core/server';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import type { UserActionPersistedAttributes } from '../../../common/types/user_actions';
|
||||
import { UserActionPersistedAttributesRt } from '../../../common/types/user_actions';
|
||||
import { CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT } from '../../../../common/constants';
|
||||
import { arraysDifference } from '../../../client/utils';
|
||||
import { isUserActionType } from '../../../../common/utils/user_actions';
|
||||
|
@ -17,7 +18,7 @@ import type {
|
|||
CaseUserProfile,
|
||||
ActionCategory,
|
||||
} from '../../../../common/api';
|
||||
import { Actions, ActionTypes } from '../../../../common/api';
|
||||
import { Actions, ActionTypes, decodeOrThrow } from '../../../../common/api';
|
||||
import { BuilderFactory } from '../builder_factory';
|
||||
import type {
|
||||
BuilderParameters,
|
||||
|
@ -300,17 +301,24 @@ export class UserActionPersister {
|
|||
}
|
||||
|
||||
try {
|
||||
this.context.log.debug(`Attempting to POST a new case user action`);
|
||||
this.context.log.debug(`Attempting to bulk create user actions`);
|
||||
|
||||
return await this.context.unsecuredSavedObjectsClient.bulkCreate<UserActionPersistedAttributes>(
|
||||
actions.map((action) => ({
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
...action.parameters,
|
||||
})),
|
||||
actions.map((action) => {
|
||||
const decodedAttributes = decodeOrThrow(UserActionPersistedAttributesRt)(
|
||||
action.parameters.attributes
|
||||
);
|
||||
|
||||
return {
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
attributes: decodedAttributes,
|
||||
references: action.parameters.references,
|
||||
};
|
||||
}),
|
||||
{ refresh }
|
||||
);
|
||||
} catch (error) {
|
||||
this.context.log.error(`Error on POST a new case user action: ${error}`);
|
||||
this.context.log.error(`Error on bulk creating user action: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -367,9 +375,11 @@ export class UserActionPersister {
|
|||
try {
|
||||
this.context.log.debug(`Attempting to POST a new case user action`);
|
||||
|
||||
const decodedAttributes = decodeOrThrow(UserActionPersistedAttributesRt)(attributes);
|
||||
|
||||
return await this.context.unsecuredSavedObjectsClient.create<T>(
|
||||
CASE_USER_ACTION_SAVED_OBJECT,
|
||||
attributes,
|
||||
decodedAttributes as unknown as T,
|
||||
{
|
||||
references: references ?? [],
|
||||
refresh,
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
SECURITY_SOLUTION_OWNER,
|
||||
} from '../../../common/constants';
|
||||
import type {
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionWithoutReferenceIds,
|
||||
ConnectorUserAction,
|
||||
ActionCategory,
|
||||
} from '../../../common/api';
|
||||
|
@ -45,15 +45,15 @@ import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_fr
|
|||
import { transformFindResponseToExternalModel } from './transform';
|
||||
|
||||
export const createUserActionFindSO = (
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>
|
||||
): SavedObjectsFindResult<CaseUserActionAttributesWithoutConnectorId> => ({
|
||||
userAction: SavedObject<CaseUserActionWithoutReferenceIds>
|
||||
): SavedObjectsFindResult<CaseUserActionWithoutReferenceIds> => ({
|
||||
...userAction,
|
||||
score: 0,
|
||||
});
|
||||
|
||||
export const createConnectorUserAction = (
|
||||
overrides?: Partial<CaseUserActionAttributesWithoutConnectorId>
|
||||
): SavedObject<CaseUserActionAttributesWithoutConnectorId> => {
|
||||
overrides?: Partial<CaseUserActionWithoutReferenceIds>
|
||||
): SavedObject<CaseUserActionWithoutReferenceIds> => {
|
||||
const { id, ...restConnector } = createConnectorObject().connector;
|
||||
return {
|
||||
...createUserActionSO({
|
||||
|
@ -79,12 +79,12 @@ export const createUserActionSO = ({
|
|||
action?: ActionCategory;
|
||||
type?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
attributesOverrides?: Partial<CaseUserActionAttributesWithoutConnectorId>;
|
||||
attributesOverrides?: Partial<CaseUserActionWithoutReferenceIds>;
|
||||
commentId?: string;
|
||||
connectorId?: string;
|
||||
pushedConnectorId?: string;
|
||||
references?: SavedObjectReference[];
|
||||
} = {}): SavedObject<CaseUserActionAttributesWithoutConnectorId> => {
|
||||
} = {}): SavedObject<CaseUserActionWithoutReferenceIds> => {
|
||||
const defaultParams = {
|
||||
action,
|
||||
created_at: 'abc',
|
||||
|
@ -140,14 +140,14 @@ export const createUserActionSO = ({
|
|||
]
|
||||
: []),
|
||||
],
|
||||
} as SavedObject<CaseUserActionAttributesWithoutConnectorId>;
|
||||
} as SavedObject<CaseUserActionWithoutReferenceIds>;
|
||||
};
|
||||
|
||||
export const updateConnectorUserAction = ({
|
||||
overrides,
|
||||
}: {
|
||||
overrides?: Partial<CaseUserActionAttributesWithoutConnectorId>;
|
||||
} = {}): SavedObject<CaseUserActionAttributesWithoutConnectorId> => {
|
||||
overrides?: Partial<CaseUserActionWithoutReferenceIds>;
|
||||
} = {}): SavedObject<CaseUserActionWithoutReferenceIds> => {
|
||||
const { id, ...restConnector } = createJiraConnector();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
|
@ -163,8 +163,8 @@ export const updateConnectorUserAction = ({
|
|||
export const pushConnectorUserAction = ({
|
||||
overrides,
|
||||
}: {
|
||||
overrides?: Partial<CaseUserActionAttributesWithoutConnectorId>;
|
||||
} = {}): SavedObject<CaseUserActionAttributesWithoutConnectorId> => {
|
||||
overrides?: Partial<CaseUserActionWithoutReferenceIds>;
|
||||
} = {}): SavedObject<CaseUserActionWithoutReferenceIds> => {
|
||||
const { connector_id: connectorId, ...restExternalService } = createExternalService();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
|
@ -177,7 +177,7 @@ export const pushConnectorUserAction = ({
|
|||
};
|
||||
};
|
||||
|
||||
export const createCaseUserAction = (): SavedObject<CaseUserActionAttributesWithoutConnectorId> => {
|
||||
export const createCaseUserAction = (): SavedObject<CaseUserActionWithoutReferenceIds> => {
|
||||
const { id, ...restConnector } = createJiraConnector();
|
||||
return {
|
||||
...createUserActionSO({
|
||||
|
@ -231,7 +231,7 @@ export const createExternalReferenceUserAction = () => {
|
|||
|
||||
export const testConnectorId = (
|
||||
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry,
|
||||
userAction: SavedObject<CaseUserActionAttributesWithoutConnectorId>,
|
||||
userAction: SavedObject<CaseUserActionWithoutReferenceIds>,
|
||||
path: string,
|
||||
expectedConnectorId = '1'
|
||||
) => {
|
||||
|
@ -252,9 +252,7 @@ export const testConnectorId = (
|
|||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(
|
||||
invalidUserAction as SavedObject<CaseUserActionAttributesWithoutConnectorId>
|
||||
),
|
||||
createUserActionFindSO(invalidUserAction as SavedObject<CaseUserActionWithoutReferenceIds>),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
);
|
||||
|
@ -269,9 +267,7 @@ export const testConnectorId = (
|
|||
};
|
||||
const transformed = transformFindResponseToExternalModel(
|
||||
createSOFindResponse([
|
||||
createUserActionFindSO(
|
||||
invalidUserAction as SavedObject<CaseUserActionAttributesWithoutConnectorId>
|
||||
),
|
||||
createUserActionFindSO(invalidUserAction as SavedObject<CaseUserActionWithoutReferenceIds>),
|
||||
]),
|
||||
persistableStateAttachmentTypeRegistry
|
||||
) as SavedObjectsFindResponse<ConnectorUserAction>;
|
||||
|
|
|
@ -23,7 +23,7 @@ import type {
|
|||
CaseSettings,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionWithoutReferenceIds,
|
||||
CommentRequest,
|
||||
CommentUserAction,
|
||||
ConnectorUserAction,
|
||||
|
@ -174,7 +174,7 @@ export interface PushInfo {
|
|||
}
|
||||
|
||||
export interface UserActionItem {
|
||||
attributes: CaseUserActionAttributesWithoutConnectorId;
|
||||
attributes: CaseUserActionWithoutReferenceIds;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
CreateCaseUserAction,
|
||||
CaseStatuses,
|
||||
CaseSeverity,
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
CaseUserActionWithoutReferenceIds,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import {
|
||||
CasePersistedSeverity,
|
||||
|
@ -164,15 +164,11 @@ const expectImportToHaveOneCase = async (supertestService: supertest.SuperTest<s
|
|||
expect(findResponse.cases[0].description).to.eql('super description');
|
||||
};
|
||||
|
||||
const expectImportToHaveCreateCaseUserAction = (
|
||||
userAction: CaseUserActionAttributesWithoutConnectorId
|
||||
) => {
|
||||
const expectImportToHaveCreateCaseUserAction = (userAction: CaseUserActionWithoutReferenceIds) => {
|
||||
expect(userAction.action).to.eql('create');
|
||||
};
|
||||
|
||||
const expectImportToHavePushUserAction = (
|
||||
userAction: CaseUserActionAttributesWithoutConnectorId
|
||||
) => {
|
||||
const expectImportToHavePushUserAction = (userAction: CaseUserActionWithoutReferenceIds) => {
|
||||
const pushedUserAction = userAction as PushedUserAction;
|
||||
expect(userAction.action).to.eql('push_to_service');
|
||||
expect(userAction.type).to.eql('pushed');
|
||||
|
@ -183,9 +179,7 @@ const expectImportToHavePushUserAction = (
|
|||
);
|
||||
};
|
||||
|
||||
const expectImportToHaveUpdateConnector = (
|
||||
userAction: CaseUserActionAttributesWithoutConnectorId
|
||||
) => {
|
||||
const expectImportToHaveUpdateConnector = (userAction: CaseUserActionWithoutReferenceIds) => {
|
||||
const connectorUserAction = userAction as ConnectorUserAction;
|
||||
expect(userAction.action).to.eql('update');
|
||||
expect(userAction.type).to.eql('connector');
|
||||
|
@ -220,7 +214,7 @@ const expectExportToHaveCaseSavedObject = (
|
|||
};
|
||||
|
||||
const expectExportToHaveUserActions = (objects: SavedObject[], caseRequest: CasePostRequest) => {
|
||||
const userActionSOs = findSavedObjectsByType<CaseUserActionAttributesWithoutConnectorId>(
|
||||
const userActionSOs = findSavedObjectsByType<CaseUserActionWithoutReferenceIds>(
|
||||
objects,
|
||||
CASE_USER_ACTION_SAVED_OBJECT
|
||||
);
|
||||
|
@ -232,7 +226,7 @@ const expectExportToHaveUserActions = (objects: SavedObject[], caseRequest: Case
|
|||
};
|
||||
|
||||
const expectCaseCreateUserAction = (
|
||||
userActions: Array<SavedObject<CaseUserActionAttributesWithoutConnectorId>>,
|
||||
userActions: Array<SavedObject<CaseUserActionWithoutReferenceIds>>,
|
||||
caseRequest: CasePostRequest
|
||||
) => {
|
||||
const userActionForCaseCreate = findUserActionSavedObject(userActions, 'create', 'create_case');
|
||||
|
@ -259,7 +253,7 @@ const expectCaseCreateUserAction = (
|
|||
};
|
||||
|
||||
const expectCreateCommentUserAction = (
|
||||
userActions: Array<SavedObject<CaseUserActionAttributesWithoutConnectorId>>
|
||||
userActions: Array<SavedObject<CaseUserActionWithoutReferenceIds>>
|
||||
) => {
|
||||
const userActionForComment = findUserActionSavedObject(userActions, 'create', 'comment');
|
||||
const createCommentUserAction = userActionForComment!.attributes as CommentUserAction;
|
||||
|
@ -287,9 +281,9 @@ const findSavedObjectsByType = <ReturnType>(
|
|||
};
|
||||
|
||||
const findUserActionSavedObject = (
|
||||
savedObjects: Array<SavedObject<CaseUserActionAttributesWithoutConnectorId>>,
|
||||
savedObjects: Array<SavedObject<CaseUserActionWithoutReferenceIds>>,
|
||||
action: string,
|
||||
type: string
|
||||
): SavedObject<CaseUserActionAttributesWithoutConnectorId> | undefined => {
|
||||
): SavedObject<CaseUserActionWithoutReferenceIds> | undefined => {
|
||||
return savedObjects.find((so) => so.attributes.action === action && so.attributes.type === type);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue