Skip to content

Commit

Permalink
Validate max user actions.
Browse files Browse the repository at this point in the history
  • Loading branch information
adcoelho committed Jul 14, 2023
1 parent 3077919 commit 1d90526
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 3 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const MAX_DELETE_IDS_LENGTH = 100 as const;
export const MAX_SUGGESTED_PROFILES = 10 as const;
export const MAX_CASES_TO_UPDATE = 100 as const;
export const MAX_BULK_CREATE_ATTACHMENTS = 100 as const;
export const MAX_USER_ACTIONS_PER_CASE = 10000 as const;

/**
* Cases features
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export * from './connectors_api';
export * from './capabilities';
export * from './validators';
29 changes: 28 additions & 1 deletion x-pack/plugins/cases/common/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
* 2.0.
*/

import Boom from '@hapi/boom';

import type { CaseUserActionService } from '../../server/services';
import type { CaseAssignees } from '../api';
import { MAX_ASSIGNEES_PER_CASE } from '../constants';
import { MAX_ASSIGNEES_PER_CASE, MAX_USER_ACTIONS_PER_CASE } from '../constants';

export const areTotalAssigneesInvalid = (assignees?: CaseAssignees): boolean => {
if (assignees == null) {
Expand All @@ -15,3 +18,27 @@ export const areTotalAssigneesInvalid = (assignees?: CaseAssignees): boolean =>

return assignees.length > MAX_ASSIGNEES_PER_CASE;
};

export const validateMaxUserActions = async ({
caseId,
userActionService,
userActionsToAdd,
}: {
caseId: string;
userActionService: CaseUserActionService;
userActionsToAdd: number;
}) => {
const result = await userActionService.getMultipleCasesUserActionsTotal({
caseIds: [caseId],
});

result.aggregations?.references.caseUserActions.buckets.forEach(
({ key, doc_count: totalUserActions }: { key: string; doc_count: number }) => {
if (key === caseId && totalUserActions + userActionsToAdd > MAX_USER_ACTIONS_PER_CASE) {
throw Boom.badRequest(
`The case with case id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
);
}
}
);
};
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/client/attachments/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { SavedObjectsUtils } from '@kbn/core/server';

import { validateMaxUserActions } from '../../../common/utils';
import type { Case } from '../../../common/api';
import { CommentRequestRt, decodeWithExcessOrThrow } from '../../../common/api';

Expand All @@ -32,11 +33,13 @@ export const addComment = async (addArgs: AddArgs, clientArgs: CasesClientArgs):
authorization,
persistableStateAttachmentTypeRegistry,
externalReferenceAttachmentTypeRegistry,
services: { userActionService },
} = clientArgs;

try {
const query = decodeWithExcessOrThrow(CommentRequestRt)(comment);

validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 });
decodeCommentRequest(comment, externalReferenceAttachmentTypeRegistry);

const savedObjectID = SavedObjectsUtils.generateId();
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/client/attachments/bulk_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { SavedObjectsUtils } from '@kbn/core/server';

import { validateMaxUserActions } from '../../../common/utils';
import type { Case, CommentRequest } from '../../../common/api';
import { BulkCreateCommentRequestRt, decodeWithExcessOrThrow } from '../../../common/api';

Expand All @@ -31,10 +32,12 @@ export const bulkCreate = async (
authorization,
externalReferenceAttachmentTypeRegistry,
persistableStateAttachmentTypeRegistry,
services: { userActionService },
} = clientArgs;

try {
decodeWithExcessOrThrow(BulkCreateCommentRequestRt)(attachments);
validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 });

attachments.forEach((attachment) => {
decodeCommentRequest(attachment, externalReferenceAttachmentTypeRegistry);
Expand Down
42 changes: 41 additions & 1 deletion x-pack/plugins/cases/server/client/cases/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
MAX_ASSIGNEES_PER_CASE,
MAX_USER_ACTIONS_PER_CASE,
} from '../../../common/constants';

import { arraysDifference, getCaseToUpdate } from '../utils';

import type { AlertService, CasesService } from '../../services';
import type { AlertService, CasesService, CaseUserActionService } from '../../services';
import { createCaseError } from '../../common/error';
import {
createAlertUpdateStatusRequest,
Expand All @@ -59,6 +60,7 @@ import { LICENSING_CASE_ASSIGNMENT_FEATURE } from '../../common/constants';
import type { LicensingService } from '../../services/licensing';
import type { CaseSavedObjectTransformed } from '../../common/types/case';
import { decodeOrThrow } from '../../../common/api/runtime_types';
import { UserActionPersister } from '../../services/user_actions/operations/create';

/**
* Throws an error if any of the requests attempt to update the owner of a case.
Expand All @@ -72,6 +74,43 @@ function throwIfUpdateOwner(requests: UpdateRequestWithOriginalCase[]) {
}
}

/**
* Throws an error if any of the requests attempt to create a number of user actions that would put
* it's case over the limit.
*/
async function throwIfMaxUserActionsReached({
query,
userActionService,
}: {
query: CasesPatchRequest;
userActionService: CaseUserActionService;
}) {
const caseIdsAndFieldsToUpdate = query.cases.reduce<Record<string, number>>(
(acc, casePatchRequest) => {
const fieldsToUpdate = Object.keys(casePatchRequest).filter((field) =>
UserActionPersister.userActionFieldsAllowed.has(field)
);
acc[casePatchRequest.id] = fieldsToUpdate.length;
return acc;
},
{}
);

const result = await userActionService.getMultipleCasesUserActionsTotal({
caseIds: Object.keys(caseIdsAndFieldsToUpdate),
});

result.aggregations?.references.caseUserActions.buckets.forEach(
({ key: caseId, doc_count: totalUserActions }: { key: string; doc_count: number }) => {
if (totalUserActions + caseIdsAndFieldsToUpdate[caseId] > MAX_USER_ACTIONS_PER_CASE) {
throw Boom.badRequest(
`The case with case id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
);
}
}
);
}

/**
* Throws an error if any of the requests attempt to update the assignees of the case
* without the appropriate license
Expand Down Expand Up @@ -368,6 +407,7 @@ export const update = async (
throwIfUpdateOwner(casesToUpdate);
throwIfUpdateAssigneesWithoutValidLicense(casesToUpdate, hasPlatinumLicense);
throwIfTotalAssigneesAreInvalid(casesToUpdate);
throwIfMaxUserActionsReached({ query, userActionService });

notifyPlatinumUsage(licensingService, casesToUpdate);

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/server/services/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => {
getAll: jest.fn(),
getUniqueConnectors: jest.fn(),
getUserActionIdsForCases: jest.fn(),
getMultipleCasesUserActionsTotal: jest.fn(),
getCaseUserActionStats: jest.fn(),
getUsers: jest.fn(),
};
Expand Down
42 changes: 42 additions & 0 deletions x-pack/plugins/cases/server/services/user_actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
ConnectorActivityAggsResult,
ConnectorFieldsBeforePushAggsResult,
GetUsersResponse,
MultipleCasesUserActionsTotalAggsResult,
ParticipantsAggsResult,
PushInfo,
PushTimeFrameInfo,
Expand Down Expand Up @@ -654,6 +655,47 @@ export class CaseUserActionService {
};
}

public async getMultipleCasesUserActionsTotal({
caseIds,
}: {
caseIds: string[];
}): Promise<SavedObjectsFindResponse<unknown, MultipleCasesUserActionsTotalAggsResult>> {
const response = await this.context.unsecuredSavedObjectsClient.find<
unknown,
MultipleCasesUserActionsTotalAggsResult
>({
type: CASE_USER_ACTION_SAVED_OBJECT,
hasReference: caseIds.map((id) => ({ type: CASE_SAVED_OBJECT, id })),
hasReferenceOperator: 'OR',
page: 1,
perPage: 1,
sortField: defaultSortField,
aggs: CaseUserActionService.buildMultipleCasesUserActionsTotalAgg(caseIds.length),
});

return response;
}

private static buildMultipleCasesUserActionsTotalAgg(
idsLength: number
): Record<string, estypes.AggregationsAggregationContainer> {
return {
references: {
nested: {
path: `${CASE_USER_ACTION_SAVED_OBJECT}.references`,
},
aggregations: {
caseIds: {
terms: {
field: `${CASE_USER_ACTION_SAVED_OBJECT}.references.id`,
size: idsLength,
},
},
},
},
};
}

public async getCaseUserActionStats({ caseId }: { caseId: string }) {
const response = await this.context.unsecuredSavedObjectsClient.find<
unknown,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type { IndexRefresh } from '../../types';
import { UserActionAuditLogger } from '../audit_logger';

export class UserActionPersister {
private static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));
public static readonly userActionFieldsAllowed: Set<string> = new Set(Object.keys(ActionTypes));

private readonly builderFactory: BuilderFactory;
private readonly auditLogger: UserActionAuditLogger;
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/cases/server/services/user_actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,17 @@ export interface UserActionsStatsAggsResult {
};
}

export interface MultipleCasesUserActionsTotalAggsResult {
references: {
caseUserActions: {
buckets: Array<{
key: string;
doc_count: number;
}>;
};
};
}

export interface ParticipantsAggsResult {
participants: {
buckets: Array<{
Expand Down

0 comments on commit 1d90526

Please sign in to comment.