mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cases] Create internal endpoint to get user action stats (#149863)
Fixes #149390 ## Summary This PR creates an internal API to get the count of the different user actions associated with the current case. This will be used to help filter and paginate the case activity. <img width="1025" alt="aux" src="https://user-images.githubusercontent.com/1533137/215549427-373f1626-3f7a-417d-ad95-ddb47b259617.png"> **Endpoint:** `GET /internal/cases/<case_id>/user_actions/_stats` Example Response: ``` { total: 3 total_comments: 2 total_other_actions: 1 } ``` ### 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)
This commit is contained in:
parent
4795910ef3
commit
7ae33a75ac
19 changed files with 623 additions and 1 deletions
|
@ -8,6 +8,7 @@
|
|||
import * as rt from 'io-ts';
|
||||
|
||||
import type { ActionsRt, ActionTypeValues } from './common';
|
||||
|
||||
import {
|
||||
CaseUserActionInjectedIdsRt,
|
||||
CaseUserActionInjectedDeprecatedIdsRt,
|
||||
|
@ -25,6 +26,7 @@ import { StatusUserActionRt } from './status';
|
|||
import { DeleteCaseUserActionRt } from './delete_case';
|
||||
import { SeverityUserActionRt } from './severity';
|
||||
import { AssigneesUserActionRt } from './assignees';
|
||||
import { CaseUserActionStatsRt } from './stats';
|
||||
|
||||
const CommonUserActionsRt = rt.union([
|
||||
DescriptionUserActionRt,
|
||||
|
@ -83,11 +85,13 @@ const CaseUserActionResponseRt = rt.intersection([
|
|||
const CaseUserActionAttributesRt = CaseUserActionBasicRt;
|
||||
export const CaseUserActionsResponseRt = rt.array(CaseUserActionResponseRt);
|
||||
export const CaseUserActionsDeprecatedResponseRt = rt.array(CaseUserActionDeprecatedResponseRt);
|
||||
export const CaseUserActionStatsResponseRt = CaseUserActionStatsRt;
|
||||
|
||||
export type CaseUserActionAttributes = rt.TypeOf<typeof CaseUserActionAttributesRt>;
|
||||
export type CaseUserActionAttributesWithoutConnectorId = rt.TypeOf<
|
||||
typeof CaseUserActionBasicWithoutConnectorIdRt
|
||||
>;
|
||||
export type CaseUserActionStatsResponse = rt.TypeOf<typeof CaseUserActionStatsRt>;
|
||||
export type CaseUserActionsResponse = rt.TypeOf<typeof CaseUserActionsResponseRt>;
|
||||
export type CaseUserActionResponse = rt.TypeOf<typeof CaseUserActionResponseRt>;
|
||||
export type CaseUserActionsDeprecatedResponse = rt.TypeOf<
|
||||
|
|
16
x-pack/plugins/cases/common/api/cases/user_actions/stats.ts
Normal file
16
x-pack/plugins/cases/common/api/cases/user_actions/stats.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
|
||||
export const CaseUserActionStatsRt = rt.type({
|
||||
total: rt.number,
|
||||
total_comments: rt.number,
|
||||
total_other_actions: rt.number,
|
||||
});
|
||||
|
||||
export type CaseUserActionStats = rt.TypeOf<typeof CaseUserActionStatsRt>;
|
|
@ -16,6 +16,7 @@ import {
|
|||
CASE_ALERTS_URL,
|
||||
CASE_COMMENT_DELETE_URL,
|
||||
CASE_FIND_USER_ACTIONS_URL,
|
||||
INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL,
|
||||
INTERNAL_BULK_GET_ATTACHMENTS_URL,
|
||||
INTERNAL_CONNECTORS_URL,
|
||||
} from '../constants';
|
||||
|
@ -44,6 +45,10 @@ export const getCaseUserActionUrl = (id: string): string => {
|
|||
return CASE_USER_ACTIONS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
||||
export const getCaseUserActionStatsUrl = (id: string): string => {
|
||||
return INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
||||
export const getCaseFindUserActionsUrl = (id: string): string => {
|
||||
return CASE_FIND_USER_ACTIONS_URL.replace('{case_id}', id);
|
||||
};
|
||||
|
|
|
@ -94,6 +94,8 @@ 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;
|
||||
export const INTERNAL_BULK_GET_CASES_URL = `${CASES_INTERNAL_URL}/_bulk_get` as const;
|
||||
export const INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL =
|
||||
`${CASES_INTERNAL_URL}/{case_id}/user_actions/_stats` as const;
|
||||
|
||||
/**
|
||||
* Action routes
|
||||
|
|
|
@ -2184,6 +2184,90 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_user_action_get_stats",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "1",
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
},
|
||||
"message": "Failed attempt to access cases-user-actions [id=1] as owner \\"awesome\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" with an error but no entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
"code": "Error",
|
||||
"message": "an error",
|
||||
},
|
||||
"event": Object {
|
||||
"action": "case_user_action_get_stats",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "failure",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "Failed attempt to access a user actions as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" without an error but with an entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_get_stats",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"kibana": Object {
|
||||
"saved_object": Object {
|
||||
"id": "5",
|
||||
"type": "cases-user-actions",
|
||||
},
|
||||
},
|
||||
"message": "User has accessed cases-user-actions [id=5] as owner \\"super\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActionStats" without an error or entity 1`] = `
|
||||
Object {
|
||||
"event": Object {
|
||||
"action": "case_user_action_get_stats",
|
||||
"category": Array [
|
||||
"database",
|
||||
],
|
||||
"outcome": "success",
|
||||
"type": Array [
|
||||
"access",
|
||||
],
|
||||
},
|
||||
"message": "User has accessed a user actions as any owners",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`audit_logger log function event structure creates the correct audit event for operation: "getUserActions" with an error and entity 1`] = `
|
||||
Object {
|
||||
"error": Object {
|
||||
|
|
|
@ -359,4 +359,12 @@ export const Operations: Record<ReadOperations | WriteOperations, OperationDetai
|
|||
docType: 'user actions',
|
||||
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
},
|
||||
[ReadOperations.GetUserActionStats]: {
|
||||
ecsType: EVENT_TYPES.access,
|
||||
name: ACCESS_USER_ACTION_OPERATION,
|
||||
action: 'case_user_action_get_stats',
|
||||
verbs: accessVerbs,
|
||||
docType: 'user actions',
|
||||
savedObjectType: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -47,6 +47,7 @@ export enum ReadOperations {
|
|||
GetCaseMetrics = 'getCaseMetrics',
|
||||
GetCasesMetrics = 'getCasesMetrics',
|
||||
GetUserActionMetrics = 'getUserActionMetrics',
|
||||
GetUserActionStats = 'getUserActionStats',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -91,9 +91,10 @@ type UserActionsSubClientMock = jest.Mocked<UserActionsSubClient>;
|
|||
|
||||
const createUserActionsSubClientMock = (): UserActionsSubClientMock => {
|
||||
return {
|
||||
find: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
getConnectors: jest.fn(),
|
||||
find: jest.fn(),
|
||||
stats: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
|
||||
import type {
|
||||
GetCaseConnectorsResponse,
|
||||
CaseUserActionStatsResponse,
|
||||
UserActionFindResponse,
|
||||
CaseUserActionsDeprecatedResponse,
|
||||
} from '../../../common/api';
|
||||
import type { CasesClientArgs } from '../types';
|
||||
import { get } from './get';
|
||||
import { getConnectors } from './connectors';
|
||||
import { getStats } from './stats';
|
||||
import type { GetConnectorsRequest, UserActionFind, UserActionGet } from './types';
|
||||
import { find } from './find';
|
||||
import type { CasesClient } from '../client';
|
||||
|
@ -30,6 +32,11 @@ export interface UserActionsSubClient {
|
|||
* Retrieves all the connectors used within a given case
|
||||
*/
|
||||
getConnectors(params: GetConnectorsRequest): Promise<GetCaseConnectorsResponse>;
|
||||
|
||||
/**
|
||||
* Retrieves the total of comments and user actions in a given case
|
||||
*/
|
||||
stats(params: UserActionGet): Promise<CaseUserActionStatsResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,6 +50,7 @@ export const createUserActionsSubClient = (
|
|||
find: (params) => find(params, casesClient, clientArgs),
|
||||
getAll: (params) => get(params, clientArgs),
|
||||
getConnectors: (params) => getConnectors(params, clientArgs),
|
||||
stats: (params) => getStats(params, casesClient, clientArgs),
|
||||
};
|
||||
|
||||
return Object.freeze(attachmentSubClient);
|
||||
|
|
39
x-pack/plugins/cases/server/client/user_actions/stats.ts
Normal file
39
x-pack/plugins/cases/server/client/user_actions/stats.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { CaseUserActionStatsResponse } from '../../../common/api';
|
||||
import { CaseUserActionStatsResponseRt } from '../../../common/api';
|
||||
import { createCaseError } from '../../common/error';
|
||||
import type { CasesClientArgs } from '..';
|
||||
import type { UserActionGet } from './types';
|
||||
import type { CasesClient } from '../client';
|
||||
|
||||
export const getStats = async (
|
||||
{ caseId }: UserActionGet,
|
||||
casesClient: CasesClient,
|
||||
clientArgs: CasesClientArgs
|
||||
): Promise<CaseUserActionStatsResponse> => {
|
||||
const {
|
||||
services: { userActionService },
|
||||
logger,
|
||||
} = clientArgs;
|
||||
|
||||
try {
|
||||
await casesClient.cases.resolve({ id: caseId, includeComments: false });
|
||||
const totals = await userActionService.getCaseUserActionStats({
|
||||
caseId,
|
||||
});
|
||||
|
||||
return CaseUserActionStatsResponseRt.encode(totals);
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to retrieve user action stats for case id: ${caseId}: ${error}`,
|
||||
error,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { UserProfileService } from '../../services';
|
||||
import { getConnectorsRoute } from './internal/get_connectors';
|
||||
import { getCaseUserActionStatsRoute } from './internal/get_case_user_actions_stats';
|
||||
import { bulkCreateAttachmentsRoute } from './internal/bulk_create_attachments';
|
||||
import { bulkGetCasesRoute } from './internal/bulk_get_cases';
|
||||
import { suggestUserProfilesRoute } from './internal/suggest_user_profiles';
|
||||
|
@ -19,5 +20,6 @@ export const getInternalRoutes = (userProfileService: UserProfileService) =>
|
|||
suggestUserProfilesRoute(userProfileService),
|
||||
getConnectorsRoute,
|
||||
bulkGetCasesRoute,
|
||||
getCaseUserActionStatsRoute,
|
||||
bulkGetAttachmentsRoute,
|
||||
] as CaseRoute[];
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL } from '../../../../common/constants';
|
||||
import { createCaseError } from '../../../common/error';
|
||||
import { createCasesRoute } from '../create_cases_route';
|
||||
|
||||
export const getCaseUserActionStatsRoute = createCasesRoute({
|
||||
method: 'get',
|
||||
path: INTERNAL_GET_CASE_USER_ACTIONS_STATS_URL,
|
||||
params: {
|
||||
params: schema.object({
|
||||
case_id: schema.string(),
|
||||
}),
|
||||
},
|
||||
handler: async ({ context, request, response }) => {
|
||||
try {
|
||||
const casesContext = await context.cases;
|
||||
const casesClient = await casesContext.getCasesClient();
|
||||
const caseId = request.params.case_id;
|
||||
|
||||
return response.ok({
|
||||
body: await casesClient.userActions.stats({ caseId }),
|
||||
});
|
||||
} catch (error) {
|
||||
throw createCaseError({
|
||||
message: `Failed to retrieve stats in route case id: ${request.params.case_id}: ${error}`,
|
||||
error,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
|
@ -124,6 +124,7 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => {
|
|||
getAll: jest.fn(),
|
||||
getUniqueConnectors: jest.fn(),
|
||||
getUserActionIdsForCases: jest.fn(),
|
||||
getCaseUserActionStats: jest.fn(),
|
||||
};
|
||||
|
||||
// the cast here is required because jest.Mocked tries to include private members and would throw an error
|
||||
|
|
|
@ -88,6 +88,16 @@ interface ConnectorFieldsBeforePushAggsResult {
|
|||
};
|
||||
}
|
||||
|
||||
interface UserActionsStatsAggsResult {
|
||||
total: number;
|
||||
totals: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export class CaseUserActionService {
|
||||
private readonly _creator: UserActionPersister;
|
||||
private readonly _finder: UserActionFinder;
|
||||
|
@ -661,4 +671,48 @@ export class CaseUserActionService {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async getCaseUserActionStats({ caseId }: { caseId: string }) {
|
||||
const response = await this.context.unsecuredSavedObjectsClient.find<
|
||||
CaseUserActionAttributesWithoutConnectorId,
|
||||
UserActionsStatsAggsResult
|
||||
>({
|
||||
type: CASE_USER_ACTION_SAVED_OBJECT,
|
||||
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
sortField: defaultSortField,
|
||||
aggs: CaseUserActionService.buildUserActionStatsAgg(),
|
||||
});
|
||||
|
||||
const result = {
|
||||
total: response.total,
|
||||
total_comments: 0,
|
||||
total_other_actions: 0,
|
||||
};
|
||||
|
||||
response.aggregations?.totals.buckets.forEach(({ key, doc_count: docCount }) => {
|
||||
if (key === 'user') {
|
||||
result.total_comments = docCount;
|
||||
}
|
||||
});
|
||||
|
||||
result.total_other_actions = result.total - result.total_comments;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static buildUserActionStatsAgg(): Record<
|
||||
string,
|
||||
estypes.AggregationsAggregationContainer
|
||||
> {
|
||||
return {
|
||||
totals: {
|
||||
terms: {
|
||||
field: `${CASE_USER_ACTION_SAVED_OBJECT}.attributes.payload.comment.type`,
|
||||
size: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
getCaseUserActionUrl,
|
||||
CaseUserActionDeprecatedResponse,
|
||||
CaseUserActionsDeprecatedResponse,
|
||||
getCaseUserActionStatsUrl,
|
||||
CaseUserActionStatsResponse,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import type SuperTest from 'supertest';
|
||||
import { User } from './authentication/types';
|
||||
|
@ -68,3 +70,22 @@ export const findCaseUserActions = async ({
|
|||
|
||||
return userActions;
|
||||
};
|
||||
|
||||
export const getCaseUserActionStats = async ({
|
||||
supertest,
|
||||
caseID,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseID: string;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<CaseUserActionStatsResponse> => {
|
||||
const { body: userActionStats } = await supertest
|
||||
.get(`${getSpaceUrlPrefix(auth.space)}${getCaseUserActionStatsUrl(caseID)}`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return userActionStats;
|
||||
};
|
||||
|
|
|
@ -31,6 +31,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./cases/tags/get_tags'));
|
||||
loadTestFile(require.resolve('./user_actions/get_all_user_actions'));
|
||||
loadTestFile(require.resolve('./user_actions/find_user_actions'));
|
||||
loadTestFile(require.resolve('./user_actions/get_user_action_stats'));
|
||||
loadTestFile(require.resolve('./configure/get_configure'));
|
||||
loadTestFile(require.resolve('./configure/patch_configure'));
|
||||
loadTestFile(require.resolve('./configure/post_configure'));
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* 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 {
|
||||
CaseResponse,
|
||||
CaseSeverity,
|
||||
CaseStatuses,
|
||||
ConnectorTypes,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
|
||||
import {
|
||||
globalRead,
|
||||
obsSec,
|
||||
obsSecRead,
|
||||
noKibanaPrivileges,
|
||||
secOnly,
|
||||
secOnlyRead,
|
||||
superUser,
|
||||
obsOnly,
|
||||
obsOnlyRead,
|
||||
} from '../../../../common/lib/authentication/users';
|
||||
import { getCaseUserActionStats } from '../../../../common/lib/user_actions';
|
||||
import {
|
||||
getPostCaseRequest,
|
||||
persistableStateAttachment,
|
||||
postCaseReq,
|
||||
postCommentActionsReq,
|
||||
postCommentAlertReq,
|
||||
postCommentUserReq,
|
||||
postExternalReferenceESReq,
|
||||
} from '../../../../common/lib/mock';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import {
|
||||
bulkCreateAttachments,
|
||||
createCase,
|
||||
createComment,
|
||||
deleteAllCaseItems,
|
||||
updateCase,
|
||||
superUserSpace1Auth,
|
||||
} from '../../../../common/lib/utils';
|
||||
|
||||
const getCaseUpdateData = (id: string, version: string) => ({
|
||||
status: CaseStatuses.open,
|
||||
severity: CaseSeverity.MEDIUM,
|
||||
title: 'new title',
|
||||
description: 'new desc',
|
||||
settings: {
|
||||
syncAlerts: false,
|
||||
},
|
||||
tags: ['one', 'two'],
|
||||
connector: {
|
||||
id: 'my-id',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira as const,
|
||||
fields: null,
|
||||
},
|
||||
id,
|
||||
version,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('get_user_action_stats', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('returns correct total for comments', async () => {
|
||||
// 1 creation action
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [
|
||||
// Only this one should show up in total_comments
|
||||
postCommentUserReq,
|
||||
// The ones below count as total_other_actions
|
||||
postExternalReferenceESReq,
|
||||
persistableStateAttachment,
|
||||
postCommentActionsReq,
|
||||
postCommentAlertReq,
|
||||
],
|
||||
});
|
||||
|
||||
const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id });
|
||||
|
||||
expect(userActionTotals.total).to.equal(6);
|
||||
expect(userActionTotals.total_comments).to.equal(1);
|
||||
expect(userActionTotals.total_other_actions).to.equal(5);
|
||||
expect(userActionTotals.total).to.equal(
|
||||
userActionTotals.total_comments + userActionTotals.total_other_actions
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the correct stats when a case update occurs', async () => {
|
||||
// 1 creation action
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
// this update should account for 7 "other actions"
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [getCaseUpdateData(theCase.id, theCase.version)],
|
||||
},
|
||||
});
|
||||
|
||||
await bulkCreateAttachments({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: [
|
||||
// only this one should show up in total_comments
|
||||
postCommentUserReq,
|
||||
// the ones below count as total_other_actions
|
||||
postExternalReferenceESReq,
|
||||
persistableStateAttachment,
|
||||
postCommentActionsReq,
|
||||
postCommentAlertReq,
|
||||
],
|
||||
});
|
||||
|
||||
const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id });
|
||||
|
||||
expect(userActionTotals.total).to.equal(13);
|
||||
expect(userActionTotals.total_comments).to.equal(1);
|
||||
expect(userActionTotals.total_other_actions).to.equal(12);
|
||||
expect(userActionTotals.total).to.equal(
|
||||
userActionTotals.total_comments + userActionTotals.total_other_actions
|
||||
);
|
||||
});
|
||||
|
||||
describe('rbac', () => {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
let theCase: CaseResponse;
|
||||
beforeEach(async () => {
|
||||
theCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, {
|
||||
user: superUser,
|
||||
space: 'space1',
|
||||
});
|
||||
|
||||
await updateCase({
|
||||
supertest: supertestWithoutAuth,
|
||||
params: {
|
||||
cases: [getCaseUpdateData(theCase.id, theCase.version)],
|
||||
},
|
||||
auth: superUserSpace1Auth,
|
||||
});
|
||||
|
||||
await createComment({
|
||||
supertest,
|
||||
caseId: theCase.id,
|
||||
params: postCommentUserReq,
|
||||
auth: superUserSpace1Auth,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('should get the user actions for a case when the user has the correct permissions', async () => {
|
||||
for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) {
|
||||
const userActionTotals = await getCaseUserActionStats({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseID: theCase.id,
|
||||
auth: { user, space: 'space1' },
|
||||
});
|
||||
|
||||
expect(userActionTotals.total).to.equal(9);
|
||||
expect(userActionTotals.total_comments).to.equal(1);
|
||||
expect(userActionTotals.total_other_actions).to.equal(8);
|
||||
expect(userActionTotals.total).to.equal(
|
||||
userActionTotals.total_comments + userActionTotals.total_other_actions
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (const scenario of [
|
||||
{ user: noKibanaPrivileges, space: 'space1' },
|
||||
{ user: secOnly, space: 'space2' },
|
||||
{ user: obsOnly, space: 'space1' },
|
||||
{ user: obsOnlyRead, space: 'space1' },
|
||||
]) {
|
||||
it(`should 403 when requesting the user action stats of a case with user ${
|
||||
scenario.user.username
|
||||
} with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => {
|
||||
await getCaseUserActionStats({
|
||||
supertest: supertestWithoutAuth,
|
||||
caseID: theCase.id,
|
||||
auth: { user: scenario.user, space: scenario.space },
|
||||
expectedHttpCode: 403,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
|
@ -38,6 +38,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./user_profiles/get_current'));
|
||||
|
||||
// Internal routes
|
||||
loadTestFile(require.resolve('./internal/get_user_action_stats'));
|
||||
loadTestFile(require.resolve('./internal/suggest_user_profiles'));
|
||||
loadTestFile(require.resolve('./internal/get_connectors'));
|
||||
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 http from 'http';
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/api';
|
||||
import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { createCase, deleteAllCaseItems, pushCase, updateCase } from '../../../../common/lib/utils';
|
||||
import { postCaseReq } from '../../../../common/lib/mock';
|
||||
import {
|
||||
createCaseWithConnector,
|
||||
createConnector,
|
||||
getJiraConnector,
|
||||
getServiceNowSimulationServer,
|
||||
} from '../../../../common/lib/connectors';
|
||||
import { getCaseUserActionStats } from '../../../../common/lib/user_actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const actionsRemover = new ActionsRemover(supertest);
|
||||
|
||||
describe('get_case_user_action_stats', () => {
|
||||
let serviceNowSimulatorURL: string = '';
|
||||
let serviceNowServer: http.Server;
|
||||
|
||||
before(async () => {
|
||||
const { server, url } = await getServiceNowSimulationServer();
|
||||
serviceNowServer = server;
|
||||
serviceNowSimulatorURL = url;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
await actionsRemover.removeAll();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
serviceNowServer.close();
|
||||
});
|
||||
|
||||
it('connectors are counted in total_other_actions', async () => {
|
||||
const [{ postedCase, connector: serviceNowConnector }, jiraConnector] = await Promise.all([
|
||||
createCaseWithConnector({
|
||||
supertest,
|
||||
serviceNowSimulatorURL,
|
||||
actionsRemover,
|
||||
}),
|
||||
createConnector({
|
||||
supertest,
|
||||
req: {
|
||||
...getJiraConnector(),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
actionsRemover.add('default', jiraConnector.id, 'action', 'actions');
|
||||
|
||||
const theCase = await pushCase({
|
||||
supertest,
|
||||
caseId: postedCase.id,
|
||||
connectorId: serviceNowConnector.id,
|
||||
});
|
||||
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
connector: {
|
||||
id: jiraConnector.id,
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: { issueType: 'Task', priority: null, parent: null },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id });
|
||||
|
||||
expect(userActionTotals.total).to.equal(3);
|
||||
expect(userActionTotals.total_comments).to.equal(0);
|
||||
expect(userActionTotals.total_other_actions).to.equal(3);
|
||||
expect(userActionTotals.total).to.equal(
|
||||
userActionTotals.total_comments + userActionTotals.total_other_actions
|
||||
);
|
||||
});
|
||||
|
||||
it('assignees are counted in total_other_actions', async () => {
|
||||
// 1 creation action
|
||||
const theCase = await createCase(supertest, postCaseReq);
|
||||
|
||||
// 1 assignee action
|
||||
await updateCase({
|
||||
supertest,
|
||||
params: {
|
||||
cases: [
|
||||
{
|
||||
assignees: [
|
||||
{
|
||||
uid: '123',
|
||||
},
|
||||
],
|
||||
id: theCase.id,
|
||||
version: theCase.version,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const userActionTotals = await getCaseUserActionStats({ supertest, caseID: theCase.id });
|
||||
|
||||
expect(userActionTotals.total).to.equal(2);
|
||||
expect(userActionTotals.total_comments).to.equal(0);
|
||||
expect(userActionTotals.total_other_actions).to.equal(2);
|
||||
expect(userActionTotals.total).to.equal(
|
||||
userActionTotals.total_comments + userActionTotals.total_other_actions
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue