[Cases] Add telemetry to the cases APIs (#125928)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2022-02-25 09:56:33 +02:00 committed by GitHub
parent 45a003fa06
commit 61fc407e90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1123 additions and 832 deletions

View file

@ -11,7 +11,8 @@
"kibanaVersion":"kibana",
"optionalPlugins":[
"security",
"spaces"
"spaces",
"usageCollection"
],
"owner":{
"githubTeam":"response-ops",

View file

@ -25,7 +25,7 @@ import {
getIDsAndIndicesAsArrays,
} from '../../common/utils';
import { createCaseError } from '../../common/error';
import { defaultPage, defaultPerPage } from '../../routes/api';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../routes/api';
import { CasesClientArgs } from '../types';
import { combineFilters, stringToKueryNode } from '../utils';
import { Operations } from '../../authorization';
@ -170,8 +170,8 @@ export async function find(
// We need this because the default behavior of getAllCaseComments is to return all the comments
// unless the page and/or perPage is specified. Since we're spreading the query after the request can
// still override this behavior.
page: defaultPage,
perPage: defaultPerPage,
page: DEFAULT_PAGE,
perPage: DEFAULT_PER_PAGE,
sortField: 'created_at',
filter: combinedFilter,
...queryWithoutFilter,
@ -183,8 +183,8 @@ export async function find(
unsecuredSavedObjectsClient,
id,
options: {
page: defaultPage,
perPage: defaultPerPage,
page: DEFAULT_PAGE,
perPage: DEFAULT_PER_PAGE,
sortField: 'created_at',
filter: combinedFilter,
},

View file

@ -8,6 +8,7 @@
import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server';
import { CoreSetup, CoreStart } from 'src/core/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import {
PluginSetupContract as ActionsPluginSetup,
@ -15,7 +16,6 @@ import {
} from '../../actions/server';
import { APP_ID } from '../common/constants';
import { initCaseApi } from './routes/api';
import {
createCaseCommentSavedObjectType,
caseConfigureSavedObjectType,
@ -30,11 +30,14 @@ import { CasesClientFactory } from './client/factory';
import { SpacesPluginStart } from '../../spaces/server';
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
import { LensServerPluginSetup } from '../../lens/server';
import { registerRoutes } from './routes/api/register_routes';
import { getExternalRoutes } from './routes/api/get_external_routes';
export interface PluginsSetup {
security?: SecurityPluginSetup;
actions: ActionsPluginSetup;
lens: LensServerPluginSetup;
usageCollection?: UsageCollectionSetup;
security?: SecurityPluginSetup;
}
export interface PluginsStart {
@ -100,10 +103,14 @@ export class CasePlugin {
);
const router = core.http.createRouter<CasesRequestHandlerContext>();
initCaseApi({
logger: this.log,
const telemetryUsageCounter = plugins.usageCollection?.createUsageCounter(APP_ID);
registerRoutes({
router,
routes: getExternalRoutes(),
logger: this.log,
kibanaVersion: this.kibanaVersion,
telemetryUsageCounter,
});
}

View file

@ -6,42 +6,35 @@
*/
import { schema } from '@kbn/config-schema';
import Boom from '@hapi/boom';
import { RouteDeps } from '../../types';
import { escapeHatch, wrapError } from '../../utils';
import { CasesByAlertIDRequest } from '../../../../../common/api';
import { CASE_ALERTS_URL } from '../../../../../common/constants';
import { createCaseError } from '../../../../common/error';
import { createCasesRoute } from '../../create_cases_route';
export function initGetCasesByAlertIdApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_ALERTS_URL,
validate: {
params: schema.object({
alert_id: schema.string(),
}),
query: escapeHatch,
},
},
async (context, request, response) => {
try {
const alertID = request.params.alert_id;
if (alertID == null || alertID === '') {
throw Boom.badRequest('The `alertId` is not valid');
}
const casesClient = await context.cases.getCasesClient();
const options = request.query as CasesByAlertIDRequest;
export const getCasesByAlertIdRoute = createCasesRoute({
method: 'get',
path: CASE_ALERTS_URL,
params: {
params: schema.object({
alert_id: schema.string({ minLength: 1 }),
}),
},
handler: async ({ context, request, response }) => {
try {
const alertID = request.params.alert_id;
return response.ok({
body: await casesClient.cases.getCasesByAlertID({ alertID, options }),
});
} catch (error) {
logger.error(
`Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}`
);
return response.customError(wrapError(error));
}
const casesClient = await context.cases.getCasesClient();
const options = request.query as CasesByAlertIDRequest;
return response.ok({
body: await casesClient.cases.getCasesByAlertID({ alertID, options }),
});
} catch (error) {
throw createCaseError({
message: `Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,32 +7,31 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASES_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initDeleteCasesApi({ router, logger }: RouteDeps) {
router.delete(
{
path: CASES_URL,
validate: {
query: schema.object({
ids: schema.arrayOf(schema.string()),
}),
},
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
await client.cases.delete(request.query.ids);
export const deleteCaseRoute = createCasesRoute({
method: 'delete',
path: CASES_URL,
params: {
query: schema.object({
ids: schema.arrayOf(schema.string()),
}),
},
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
await client.cases.delete(request.query.ids);
return response.noContent();
} catch (error) {
logger.error(
`Failed to delete cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}`
);
return response.customError(wrapError(error));
}
return response.noContent();
} catch (error) {
throw createCaseError({
message: `Failed to delete cases in route ids: ${JSON.stringify(
request.query.ids
)}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,32 +7,25 @@
import { CasesFindRequest } from '../../../../common/api';
import { CASES_URL } from '../../../../common/constants';
import { wrapError, escapeHatch } from '../utils';
import { RouteDeps } from '../types';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initFindCasesApi({ router, logger }: RouteDeps) {
router.get(
{
path: `${CASES_URL}/_find`,
validate: {
query: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
const casesClient = await context.cases.getCasesClient();
const options = request.query as CasesFindRequest;
export const findCaseRoute = createCasesRoute({
method: 'get',
path: `${CASES_URL}/_find`,
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const options = request.query as CasesFindRequest;
return response.ok({
body: await casesClient.cases.find({ ...options }),
});
} catch (error) {
logger.error(`Failed to find cases in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await casesClient.cases.find({ ...options }),
});
} catch (error) {
throw createCaseError({
message: `Failed to find cases in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,91 +7,83 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils';
import { getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { CASE_DETAILS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initGetCaseApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_DETAILS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
}),
query: schema.object({
/**
* @deprecated since version 8.1.0
*/
includeComments: schema.boolean({ defaultValue: true }),
}),
},
},
async (context, request, response) => {
try {
const isIncludeCommentsParamProvidedByTheUser =
request.url.searchParams.has('includeComments');
const params = {
params: schema.object({
case_id: schema.string(),
}),
query: schema.object({
/**
* @deprecated since version 8.1.0
*/
includeComments: schema.boolean({ defaultValue: true }),
}),
};
if (isIncludeCommentsParamProvidedByTheUser) {
logDeprecatedEndpoint(
logger,
request.headers,
`The query parameter 'includeComments' of the get case API '${CASE_DETAILS_URL}' is deprecated`
);
}
export const getCaseRoute = createCasesRoute({
method: 'get',
path: CASE_DETAILS_URL,
params,
handler: async ({ context, request, response, logger, kibanaVersion }) => {
try {
const isIncludeCommentsParamProvidedByTheUser =
request.url.searchParams.has('includeComments');
const casesClient = await context.cases.getCasesClient();
const id = request.params.case_id;
return response.ok({
...(isIncludeCommentsParamProvidedByTheUser && {
headers: {
...getWarningHeader(kibanaVersion, 'Deprecated query parameter includeComments'),
},
}),
body: await casesClient.cases.get({
id,
includeComments: request.query.includeComments,
}),
});
} catch (error) {
logger.error(
`Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}`
if (isIncludeCommentsParamProvidedByTheUser) {
logDeprecatedEndpoint(
logger,
request.headers,
`The query parameter 'includeComments' of the get case API '${CASE_DETAILS_URL}' is deprecated`
);
return response.customError(wrapError(error));
}
}
);
router.get(
{
path: `${CASE_DETAILS_URL}/resolve`,
validate: {
params: schema.object({
case_id: schema.string(),
}),
query: schema.object({
includeComments: schema.boolean({ defaultValue: true }),
}),
},
},
async (context, request, response) => {
try {
const casesClient = await context.cases.getCasesClient();
const id = request.params.case_id;
const casesClient = await context.cases.getCasesClient();
const id = request.params.case_id;
return response.ok({
body: await casesClient.cases.resolve({
id,
includeComments: request.query.includeComments,
}),
});
} catch (error) {
logger.error(
`Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}`
);
return response.customError(wrapError(error));
}
return response.ok({
...(isIncludeCommentsParamProvidedByTheUser && {
headers: {
...getWarningHeader(kibanaVersion, 'Deprecated query parameter includeComments'),
},
}),
body: await casesClient.cases.get({
id,
includeComments: request.query.includeComments,
}),
});
} catch (error) {
throw createCaseError({
message: `Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}`,
error,
});
}
);
}
},
});
export const resolveCaseRoute = createCasesRoute({
method: 'get',
path: `${CASE_DETAILS_URL}/resolve`,
params,
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const id = request.params.case_id;
return response.ok({
body: await casesClient.cases.resolve({
id,
includeComments: request.query.includeComments,
}),
});
} catch (error) {
throw createCaseError({
message: `Failed to retrieve case in resolve route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments}: ${error}`,
error,
});
}
},
});

View file

@ -5,35 +5,27 @@
* 2.0.
*/
import { escapeHatch, wrapError } from '../utils';
import { RouteDeps } from '../types';
import { CasesPatchRequest } from '../../../../common/api';
import { CASES_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPatchCasesApi({ router, logger }: RouteDeps) {
router.patch(
{
path: CASES_URL,
validate: {
body: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
export const patchCaseRoute = createCasesRoute({
method: 'patch',
path: CASES_URL,
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const cases = request.body as CasesPatchRequest;
const casesClient = await context.cases.getCasesClient();
const cases = request.body as CasesPatchRequest;
return response.ok({
body: await casesClient.cases.update(cases),
});
} catch (error) {
logger.error(`Failed to patch cases in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await casesClient.cases.update(cases),
});
} catch (error) {
throw createCaseError({
message: `Failed to patch cases in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -5,35 +5,27 @@
* 2.0.
*/
import { wrapError, escapeHatch } from '../utils';
import { RouteDeps } from '../types';
import { CasePostRequest } from '../../../../common/api';
import { CASES_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPostCaseApi({ router, logger }: RouteDeps) {
router.post(
{
path: CASES_URL,
validate: {
body: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
const casesClient = await context.cases.getCasesClient();
const theCase = request.body as CasePostRequest;
export const postCaseRoute = createCasesRoute({
method: 'post',
path: CASES_URL,
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const theCase = request.body as CasePostRequest;
return response.ok({
body: await casesClient.cases.create({ ...theCase }),
});
} catch (error) {
logger.error(`Failed to post case in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await casesClient.cases.create({ ...theCase }),
});
} catch (error) {
throw createCaseError({
message: `Failed to post case in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -10,44 +10,35 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { wrapError, escapeHatch } from '../utils';
import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api';
import { CASE_PUSH_URL } from '../../../../common/constants';
import { RouteDeps } from '../types';
import { CaseRoute } from '../types';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPushCaseApi({ router, logger }: RouteDeps) {
router.post(
{
path: CASE_PUSH_URL,
validate: {
params: escapeHatch,
body: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
export const pushCaseRoute: CaseRoute = createCasesRoute({
method: 'post',
path: CASE_PUSH_URL,
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const casesClient = await context.cases.getCasesClient();
const params = pipe(
CasePushRequestParamsRt.decode(request.params),
fold(throwErrors(Boom.badRequest), identity)
);
const params = pipe(
CasePushRequestParamsRt.decode(request.params),
fold(throwErrors(Boom.badRequest), identity)
);
return response.ok({
body: await casesClient.cases.push({
caseId: params.case_id,
connectorId: params.connector_id,
}),
});
} catch (error) {
logger.error(`Failed to push case in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await casesClient.cases.push({
caseId: params.case_id,
connectorId: params.connector_id,
}),
});
} catch (error) {
throw createCaseError({
message: `Failed to push case in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -5,33 +5,25 @@
* 2.0.
*/
import { RouteDeps } from '../../types';
import { wrapError, escapeHatch } from '../../utils';
import { AllReportersFindRequest } from '../../../../../common/api';
import { CASE_REPORTERS_URL } from '../../../../../common/constants';
import { createCaseError } from '../../../../common/error';
import { createCasesRoute } from '../../create_cases_route';
export function initGetReportersApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_REPORTERS_URL,
validate: {
query: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
export const getReportersRoute = createCasesRoute({
method: 'get',
path: CASE_REPORTERS_URL,
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
const options = request.query as AllReportersFindRequest;
const client = await context.cases.getCasesClient();
const options = request.query as AllReportersFindRequest;
return response.ok({ body: await client.cases.getReporters({ ...options }) });
} catch (error) {
logger.error(`Failed to get reporters in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({ body: await client.cases.getReporters({ ...options }) });
} catch (error) {
throw createCaseError({
message: `Failed to find cases in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -5,33 +5,25 @@
* 2.0.
*/
import { RouteDeps } from '../../types';
import { wrapError, escapeHatch } from '../../utils';
import { AllTagsFindRequest } from '../../../../../common/api';
import { CASE_TAGS_URL } from '../../../../../common/constants';
import { createCaseError } from '../../../../common/error';
import { createCasesRoute } from '../../create_cases_route';
export function initGetTagsApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_TAGS_URL,
validate: {
query: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
export const getTagsRoute = createCasesRoute({
method: 'get',
path: CASE_TAGS_URL,
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
const options = request.query as AllTagsFindRequest;
const client = await context.cases.getCasesClient();
const options = request.query as AllTagsFindRequest;
return response.ok({ body: await client.cases.getTags({ ...options }) });
} catch (error) {
logger.error(`Failed to retrieve tags in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({ body: await client.cases.getTags({ ...options }) });
} catch (error) {
throw createCaseError({
message: `Failed to retrieve tags in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -6,35 +6,32 @@
*/
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASE_COMMENTS_URL } from '../../../../common/constants';
import { createCasesRoute } from '../create_cases_route';
import { createCaseError } from '../../../common/error';
export function initDeleteAllCommentsApi({ router, logger }: RouteDeps) {
router.delete(
{
path: CASE_COMMENTS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
}),
},
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
export const deleteAllCommentsRoute = createCasesRoute({
method: 'delete',
path: CASE_COMMENTS_URL,
params: {
params: schema.object({
case_id: schema.string(),
}),
},
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
await client.attachments.deleteAll({
caseID: request.params.case_id,
});
await client.attachments.deleteAll({
caseID: request.params.case_id,
});
return response.noContent();
} catch (error) {
logger.error(
`Failed to delete all comments in route case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
return response.noContent();
} catch (error) {
throw createCaseError({
message: `Failed to delete all comments in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,36 +7,33 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants';
import { createCasesRoute } from '../create_cases_route';
import { createCaseError } from '../../../common/error';
export function initDeleteCommentApi({ router, logger }: RouteDeps) {
router.delete(
{
path: CASE_COMMENT_DETAILS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
comment_id: schema.string(),
}),
},
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
await client.attachments.delete({
attachmentID: request.params.comment_id,
caseID: request.params.case_id,
});
export const deleteCommentRoute = createCasesRoute({
method: 'delete',
path: CASE_COMMENT_DETAILS_URL,
params: {
params: schema.object({
case_id: schema.string(),
comment_id: schema.string(),
}),
},
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
await client.attachments.delete({
attachmentID: request.params.comment_id,
caseID: request.params.case_id,
});
return response.noContent();
} catch (error) {
logger.error(
`Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}`
);
return response.customError(wrapError(error));
}
return response.noContent();
} catch (error) {
throw createCaseError({
message: `Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -14,40 +14,36 @@ import { identity } from 'fp-ts/lib/function';
import { FindQueryParamsRt, throwErrors, excess } from '../../../../common/api';
import { CASE_COMMENTS_URL } from '../../../../common/constants';
import { RouteDeps } from '../types';
import { escapeHatch, wrapError } from '../utils';
import { createCasesRoute } from '../create_cases_route';
import { createCaseError } from '../../../common/error';
export function initFindCaseCommentsApi({ router, logger }: RouteDeps) {
router.get(
{
path: `${CASE_COMMENTS_URL}/_find`,
validate: {
params: schema.object({
case_id: schema.string(),
export const findCommentsRoute = createCasesRoute({
method: 'get',
path: `${CASE_COMMENTS_URL}/_find`,
params: {
params: schema.object({
case_id: schema.string(),
}),
},
handler: async ({ context, request, response }) => {
try {
const query = pipe(
excess(FindQueryParamsRt).decode(request.query),
fold(throwErrors(Boom.badRequest), identity)
);
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.attachments.find({
caseID: request.params.case_id,
queryParams: query,
}),
query: escapeHatch,
},
},
async (context, request, response) => {
try {
const query = pipe(
excess(FindQueryParamsRt).decode(request.query),
fold(throwErrors(Boom.badRequest), identity)
);
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.attachments.find({
caseID: request.params.case_id,
queryParams: query,
}),
});
} catch (error) {
logger.error(
`Failed to find comments in route case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
});
} catch (error) {
throw createCaseError({
message: `Failed to find comments in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,35 +7,32 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASE_DETAILS_ALERTS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initGetAllAlertsAttachToCaseApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_DETAILS_ALERTS_URL,
validate: {
params: schema.object({
case_id: schema.string({ minLength: 1 }),
}),
},
},
async (context, request, response) => {
try {
const caseId = request.params.case_id;
export const getAllAlertsAttachedToCaseRoute = createCasesRoute({
method: 'get',
path: CASE_DETAILS_ALERTS_URL,
params: {
params: schema.object({
case_id: schema.string({ minLength: 1 }),
}),
},
handler: async ({ context, request, response }) => {
try {
const caseId = request.params.case_id;
const casesClient = await context.cases.getCasesClient();
const casesClient = await context.cases.getCasesClient();
return response.ok({
body: await casesClient.attachments.getAllAlertsAttachToCase({ caseId }),
});
} catch (error) {
logger.error(
`Failed to retrieve alert ids for this case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
return response.ok({
body: await casesClient.attachments.getAllAlertsAttachToCase({ caseId }),
});
} catch (error) {
throw createCaseError({
message: `Failed to retrieve alert ids for this case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,47 +7,45 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { CASE_COMMENTS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
/**
* @deprecated since version 8.1.0
*/
export function initGetAllCommentsApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_COMMENTS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
export const getAllCommentsRoute = createCasesRoute({
method: 'get',
path: CASE_COMMENTS_URL,
params: {
params: schema.object({
case_id: schema.string(),
}),
},
handler: async ({ context, request, response, logger, kibanaVersion }) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get all cases comments API '${CASE_COMMENTS_URL}' is deprecated.`
);
const client = await context.cases.getCasesClient();
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await client.attachments.getAll({
caseID: request.params.case_id,
}),
},
},
async (context, request, response) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get all cases comments API '${CASE_COMMENTS_URL}' is deprecated.`
);
const client = await context.cases.getCasesClient();
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await client.attachments.getAll({
caseID: request.params.case_id,
}),
});
} catch (error) {
logger.error(
`Failed to get all comments in route case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
});
} catch (error) {
throw createCaseError({
message: `Failed to get all comments in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -7,37 +7,34 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initGetCommentApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_COMMENT_DETAILS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
comment_id: schema.string(),
export const getCommentRoute = createCasesRoute({
method: 'get',
path: CASE_COMMENT_DETAILS_URL,
params: {
params: schema.object({
case_id: schema.string(),
comment_id: schema.string(),
}),
},
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.attachments.get({
attachmentID: request.params.comment_id,
caseID: request.params.case_id,
}),
},
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.attachments.get({
attachmentID: request.params.comment_id,
caseID: request.params.case_id,
}),
});
} catch (error) {
logger.error(
`Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}`
);
return response.customError(wrapError(error));
}
});
} catch (error) {
throw createCaseError({
message: `Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -11,43 +11,39 @@ import { identity } from 'fp-ts/lib/function';
import { schema } from '@kbn/config-schema';
import Boom from '@hapi/boom';
import { RouteDeps } from '../types';
import { escapeHatch, wrapError } from '../utils';
import { CommentPatchRequestRt, throwErrors } from '../../../../common/api';
import { CASE_COMMENTS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPatchCommentApi({ router, logger }: RouteDeps) {
router.patch(
{
path: CASE_COMMENTS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
export const patchCommentRoute = createCasesRoute({
method: 'patch',
path: CASE_COMMENTS_URL,
params: {
params: schema.object({
case_id: schema.string(),
}),
},
handler: async ({ context, request, response }) => {
try {
const query = pipe(
CommentPatchRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.attachments.update({
caseID: request.params.case_id,
updateRequest: query,
}),
body: escapeHatch,
},
},
async (context, request, response) => {
try {
const query = pipe(
CommentPatchRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.attachments.update({
caseID: request.params.case_id,
updateRequest: query,
}),
});
} catch (error) {
logger.error(
`Failed to patch comment in route case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
});
} catch (error) {
throw createCaseError({
message: `Failed to patch comment in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -6,41 +6,33 @@
*/
import { schema } from '@kbn/config-schema';
import { escapeHatch, wrapError } from '../utils';
import { RouteDeps } from '../types';
import { CASE_COMMENTS_URL } from '../../../../common/constants';
import { CommentRequest } from '../../../../common/api';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPostCommentApi({ router, logger }: RouteDeps) {
router.post(
{
path: CASE_COMMENTS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
}),
body: escapeHatch,
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
export const postCommentRoute = createCasesRoute({
method: 'post',
path: CASE_COMMENTS_URL,
params: {
params: schema.object({
case_id: schema.string(),
}),
},
handler: async ({ context, request, response }) => {
try {
const casesClient = await context.cases.getCasesClient();
const caseId = request.params.case_id;
const comment = request.body as CommentRequest;
const casesClient = await context.cases.getCasesClient();
const caseId = request.params.case_id;
const comment = request.body as CommentRequest;
return response.ok({
body: await casesClient.attachments.add({ caseId, comment }),
});
} catch (error) {
logger.error(
`Failed to post comment in route case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
return response.ok({
body: await casesClient.attachments.add({ caseId, comment }),
});
} catch (error) {
throw createCaseError({
message: `Failed to post comment in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -5,31 +5,27 @@
* 2.0.
*/
import { RouteDeps } from '../types';
import { escapeHatch, wrapError } from '../utils';
import { CASE_CONFIGURE_URL } from '../../../../common/constants';
import { GetConfigureFindRequest } from '../../../../common/api';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initGetCaseConfigure({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_CONFIGURE_URL,
validate: {
query: escapeHatch,
},
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
const options = request.query as GetConfigureFindRequest;
export const getCaseConfigureRoute = createCasesRoute({
method: 'get',
path: CASE_CONFIGURE_URL,
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
const options = request.query as GetConfigureFindRequest;
return response.ok({
body: await client.configure.get({ ...options }),
});
} catch (error) {
logger.error(`Failed to get case configure in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await client.configure.get({ ...options }),
});
} catch (error) {
throw createCaseError({
message: `Failed to get case configure in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -5,29 +5,26 @@
* 2.0.
*/
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
/*
* Be aware that this api will only return 20 connectors
*/
export function initCaseConfigureGetActionConnector({ router, logger }: RouteDeps) {
router.get(
{
path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`,
validate: false,
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
export const getConnectorsRoute = createCasesRoute({
method: 'get',
path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`,
handler: async ({ context, response }) => {
try {
const client = await context.cases.getCasesClient();
return response.ok({ body: await client.configure.getConnectors() });
} catch (error) {
logger.error(`Failed to get connectors in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({ body: await client.configure.getConnectors() });
} catch (error) {
throw createCaseError({
message: `Failed to get connectors in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -17,35 +17,30 @@ import {
excess,
} from '../../../../common/api';
import { CASE_CONFIGURE_DETAILS_URL } from '../../../../common/constants';
import { RouteDeps } from '../types';
import { wrapError, escapeHatch } from '../utils';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPatchCaseConfigure({ router, logger }: RouteDeps) {
router.patch(
{
path: CASE_CONFIGURE_DETAILS_URL,
validate: {
params: escapeHatch,
body: escapeHatch,
},
},
async (context, request, response) => {
try {
const params = pipe(
excess(CaseConfigureRequestParamsRt).decode(request.params),
fold(throwErrors(Boom.badRequest), identity)
);
export const patchCaseConfigureRoute = createCasesRoute({
method: 'patch',
path: CASE_CONFIGURE_DETAILS_URL,
handler: async ({ context, request, response }) => {
try {
const params = pipe(
excess(CaseConfigureRequestParamsRt).decode(request.params),
fold(throwErrors(Boom.badRequest), identity)
);
const client = await context.cases.getCasesClient();
const configuration = request.body as CasesConfigurePatch;
const client = await context.cases.getCasesClient();
const configuration = request.body as CasesConfigurePatch;
return response.ok({
body: await client.configure.update(params.configuration_id, configuration),
});
} catch (error) {
logger.error(`Failed to get patch configure in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await client.configure.update(params.configuration_id, configuration),
});
} catch (error) {
throw createCaseError({
message: `Failed to patch configure in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -12,33 +12,29 @@ import { identity } from 'fp-ts/lib/function';
import { CasesConfigureRequestRt, throwErrors } from '../../../../common/api';
import { CASE_CONFIGURE_URL } from '../../../../common/constants';
import { RouteDeps } from '../types';
import { wrapError, escapeHatch } from '../utils';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initPostCaseConfigure({ router, logger }: RouteDeps) {
router.post(
{
path: CASE_CONFIGURE_URL,
validate: {
body: escapeHatch,
},
},
async (context, request, response) => {
try {
const query = pipe(
CasesConfigureRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
export const postCaseConfigureRoute = createCasesRoute({
method: 'post',
path: CASE_CONFIGURE_URL,
handler: async ({ context, request, response }) => {
try {
const query = pipe(
CasesConfigureRequestRt.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
const client = await context.cases.getCasesClient();
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.configure.create(query),
});
} catch (error) {
logger.error(`Failed to post case configure in route: ${error}`);
return response.customError(wrapError(error));
}
return response.ok({
body: await client.configure.create(query),
});
} catch (error) {
throw createCaseError({
message: `Failed to post case configure in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -0,0 +1,10 @@
/*
* 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 { CaseRoute } from './types';
export const createCasesRoute = <P, Q, B>(route: CaseRoute<P, Q, B>) => route;

View file

@ -0,0 +1,61 @@
/*
* 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 { getCasesByAlertIdRoute } from './cases/alerts/get_cases';
import { deleteCaseRoute } from './cases/delete_cases';
import { findCaseRoute } from './cases/find_cases';
import { getCaseRoute, resolveCaseRoute } from './cases/get_case';
import { patchCaseRoute } from './cases/patch_cases';
import { postCaseRoute } from './cases/post_case';
import { pushCaseRoute } from './cases/push_case';
import { getReportersRoute } from './cases/reporters/get_reporters';
import { getStatusRoute } from './stats/get_status';
import { getUserActionsRoute } from './user_actions/get_all_user_actions';
import { CaseRoute } from './types';
import { getTagsRoute } from './cases/tags/get_tags';
import { deleteAllCommentsRoute } from './comments/delete_all_comments';
import { deleteCommentRoute } from './comments/delete_comment';
import { findCommentsRoute } from './comments/find_comments';
import { getCommentRoute } from './comments/get_comment';
import { getAllCommentsRoute } from './comments/get_all_comment';
import { patchCommentRoute } from './comments/patch_comment';
import { postCommentRoute } from './comments/post_comment';
import { getCaseConfigureRoute } from './configure/get_configure';
import { getConnectorsRoute } from './configure/get_connectors';
import { patchCaseConfigureRoute } from './configure/patch_configure';
import { postCaseConfigureRoute } from './configure/post_configure';
import { getAllAlertsAttachedToCaseRoute } from './comments/get_alerts';
import { getCaseMetricRoute } from './metrics/get_case_metrics';
export const getExternalRoutes = () =>
[
deleteCaseRoute,
findCaseRoute,
getCaseRoute,
resolveCaseRoute,
patchCaseRoute,
postCaseRoute,
pushCaseRoute,
getUserActionsRoute,
getStatusRoute,
getCasesByAlertIdRoute,
getReportersRoute,
getTagsRoute,
deleteCommentRoute,
deleteAllCommentsRoute,
findCommentsRoute,
getCommentRoute,
getAllCommentsRoute,
patchCommentRoute,
postCommentRoute,
getCaseConfigureRoute,
getConnectorsRoute,
patchCaseConfigureRoute,
postCaseConfigureRoute,
getAllAlertsAttachedToCaseRoute,
getCaseMetricRoute,
] as CaseRoute[];

View file

@ -5,76 +5,11 @@
* 2.0.
*/
import { initDeleteCasesApi } from './cases/delete_cases';
import { initFindCasesApi } from '././cases/find_cases';
import { initGetCaseApi } from './cases/get_case';
import { initPatchCasesApi } from './cases/patch_cases';
import { initPostCaseApi } from './cases/post_case';
import { initPushCaseApi } from './cases/push_case';
import { initGetReportersApi } from './cases/reporters/get_reporters';
import { initGetCasesStatusApi } from './stats/get_status';
import { initGetTagsApi } from './cases/tags/get_tags';
import { initGetAllCaseUserActionsApi } from './user_actions/get_all_user_actions';
import { initDeleteCommentApi } from './comments/delete_comment';
import { initDeleteAllCommentsApi } from './comments/delete_all_comments';
import { initFindCaseCommentsApi } from './comments/find_comments';
import { initGetAllCommentsApi } from './comments/get_all_comment';
import { initGetCommentApi } from './comments/get_comment';
import { initPatchCommentApi } from './comments/patch_comment';
import { initPostCommentApi } from './comments/post_comment';
import { initCaseConfigureGetActionConnector } from './configure/get_connectors';
import { initGetCaseConfigure } from './configure/get_configure';
import { initPatchCaseConfigure } from './configure/patch_configure';
import { initPostCaseConfigure } from './configure/post_configure';
import { RouteDeps } from './types';
import { initGetCasesByAlertIdApi } from './cases/alerts/get_cases';
import { initGetAllAlertsAttachToCaseApi } from './comments/get_alerts';
import { initGetCaseMetricsApi } from './metrics/get_case_metrics';
/**
* Default page number when interacting with the saved objects API.
*/
export const defaultPage = 1;
export const DEFAULT_PAGE = 1;
/**
* Default number of results when interacting with the saved objects API.
*/
export const defaultPerPage = 20;
export function initCaseApi(deps: RouteDeps) {
// Cases
initDeleteCasesApi(deps);
initFindCasesApi(deps);
initGetCaseApi(deps);
initPatchCasesApi(deps);
initPostCaseApi(deps);
initPushCaseApi(deps);
initGetAllCaseUserActionsApi(deps);
// Comments
initDeleteCommentApi(deps);
initDeleteAllCommentsApi(deps);
initFindCaseCommentsApi(deps);
initGetCommentApi(deps);
initGetAllCommentsApi(deps);
initPatchCommentApi(deps);
initPostCommentApi(deps);
// Cases Configure
initCaseConfigureGetActionConnector(deps);
initGetCaseConfigure(deps);
initPatchCaseConfigure(deps);
initPostCaseConfigure(deps);
// Reporters
initGetReportersApi(deps);
// Status
initGetCasesStatusApi(deps);
// Tags
initGetTagsApi(deps);
// Alerts
initGetCasesByAlertIdApi(deps);
initGetAllAlertsAttachToCaseApi(deps);
// Metrics
initGetCaseMetricsApi(deps);
}
export const DEFAULT_PER_PAGE = 20;

View file

@ -7,37 +7,35 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { CASE_METRICS_DETAILS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
export function initGetCaseMetricsApi({ router, logger }: RouteDeps) {
router.get(
{
path: CASE_METRICS_DETAILS_URL,
validate: {
params: schema.object({
case_id: schema.string({ minLength: 1 }),
export const getCaseMetricRoute = createCasesRoute({
method: 'get',
path: CASE_METRICS_DETAILS_URL,
params: {
params: schema.object({
case_id: schema.string({ minLength: 1 }),
}),
query: schema.object({
features: schema.arrayOf(schema.string({ minLength: 1 })),
}),
},
handler: async ({ context, request, response }) => {
try {
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.metrics.getCaseMetrics({
caseId: request.params.case_id,
features: request.query.features,
}),
query: schema.object({
features: schema.arrayOf(schema.string({ minLength: 1 })),
}),
},
},
async (context, request, response) => {
try {
const client = await context.cases.getCasesClient();
return response.ok({
body: await client.metrics.getCaseMetrics({
caseId: request.params.case_id,
features: request.query.features,
}),
});
} catch (error) {
logger.error(`Failed to get case metrics in route: ${error}`);
return response.customError(wrapError(error));
}
});
} catch (error) {
throw createCaseError({
message: `Failed to get case metrics in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -0,0 +1,280 @@
/*
* 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 {
httpServerMock,
httpServiceMock,
loggingSystemMock,
} from '../../../../../../src/core/server/mocks';
import { usageCollectionPluginMock } from '../../../../../../src/plugins/usage_collection/server/mocks';
import { CasesRouter } from '../../types';
import { createCasesRoute } from './create_cases_route';
import { registerRoutes } from './register_routes';
import { CaseRoute } from './types';
describe('registerRoutes', () => {
let router: jest.Mocked<CasesRouter>;
const logger = loggingSystemMock.createLogger();
const response = httpServerMock.createResponseFactory();
const telemetryUsageCounter = usageCollectionPluginMock
.createSetupContract()
.createUsageCounter('test');
const handler = jest.fn();
const customError = jest.fn();
const badRequest = jest.fn();
const routes = [
createCasesRoute({
method: 'get',
path: '/foo/{case_id}',
params: {
params: schema.object({
case_id: schema.string(),
}),
query: schema.object({
includeComments: schema.boolean(),
}),
},
handler,
}),
createCasesRoute({
method: 'post',
path: '/bar',
params: {
body: schema.object({
title: schema.string(),
}),
},
handler: async () => response.ok(),
}),
createCasesRoute({
method: 'put',
path: '/baz',
handler: async () => response.ok(),
}),
createCasesRoute({
method: 'patch',
path: '/qux',
handler: async () => response.ok(),
}),
createCasesRoute({
method: 'delete',
path: '/quux',
handler: async () => response.ok(),
}),
] as CaseRoute[];
const initApi = (casesRoutes: CaseRoute[]) => {
registerRoutes({
router,
logger,
routes: casesRoutes,
kibanaVersion: '8.2.0',
telemetryUsageCounter,
});
const simulateRequest = async ({
method,
path,
context = { cases: {} },
headers = {},
}: {
method: keyof Pick<CasesRouter, 'get' | 'post'>;
path: string;
context?: Record<string, unknown>;
headers?: Record<string, unknown>;
}) => {
const [, registeredRouteHandler] =
// @ts-ignore
router[method].mock.calls.find((call) => {
return call[0].path === path;
}) ?? [];
const result = await registeredRouteHandler(
context,
{ headers },
{ customError, badRequest }
);
return result;
};
return {
simulateRequest,
};
};
const initAndSimulateError = async () => {
const { simulateRequest } = initApi([
...routes,
createCasesRoute({
method: 'get',
path: '/error',
handler: async () => {
throw new Error('API error');
},
}),
]);
await simulateRequest({
method: 'get',
path: '/error',
});
};
beforeEach(() => {
jest.clearAllMocks();
router = httpServiceMock.createRouter();
});
describe('api registration', () => {
const endpoints: Array<[CaseRoute['method'], string]> = [
['get', '/foo/{case_id}'],
['post', '/bar'],
['put', '/baz'],
['patch', '/qux'],
['delete', '/quux'],
];
it('registers the endpoints correctly', () => {
initApi(routes);
for (const endpoint of endpoints) {
const [method, path] = endpoint;
expect(router[method]).toHaveBeenCalledTimes(1);
expect(router[method]).toBeCalledWith(
{ path, validate: expect.anything() },
expect.anything()
);
}
});
});
describe('api validation', () => {
const validation: Array<
['params' | 'query' | 'body', keyof CasesRouter, Record<string, unknown>]
> = [
['params', 'get', { case_id: '123' }],
['query', 'get', { includeComments: false }],
['body', 'post', { title: 'test' }],
];
describe.each(validation)('%s', (type, method, value) => {
it(`validates ${type} correctly`, () => {
initApi(routes);
// @ts-ignore
const params = router[method].mock.calls[0][0].validate[type];
expect(() => params.validate(value)).not.toThrow();
});
it(`throws if ${type} is wrong`, () => {
initApi(routes);
// @ts-ignore
const params = router[method].mock.calls[0][0].validate[type];
expect(() => params.validate({})).toThrow();
});
it(`skips path parameter validation if ${type} is not provided`, () => {
initApi(routes);
// @ts-ignore
const params = router.put.mock.calls[0][0].validate[type];
expect(() => params.validate({})).not.toThrow();
});
});
});
describe('handler execution', () => {
it('calls the handler correctly', async () => {
const { simulateRequest } = initApi(routes);
await simulateRequest({ method: 'get', path: '/foo/{case_id}' });
expect(handler).toHaveBeenCalled();
});
});
describe('telemetry', () => {
it('increases the counters correctly on a successful kibana request', async () => {
const { simulateRequest } = initApi(routes);
await simulateRequest({
method: 'get',
path: '/foo/{case_id}',
headers: { 'kbn-version': '8.2.0', referer: 'https://example.com' },
});
expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({
counterName: 'GET /foo/{case_id}',
counterType: 'success',
});
expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({
counterName: 'GET /foo/{case_id}',
counterType: 'kibanaRequest.yes',
});
});
it('increases the counters correctly on a successful non kibana request', async () => {
const { simulateRequest } = initApi(routes);
await simulateRequest({
method: 'get',
path: '/foo/{case_id}',
});
expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({
counterName: 'GET /foo/{case_id}',
counterType: 'success',
});
expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({
counterName: 'GET /foo/{case_id}',
counterType: 'kibanaRequest.no',
});
});
it('increases the counters correctly on an error', async () => {
await initAndSimulateError();
expect(telemetryUsageCounter.incrementCounter).toHaveBeenCalledWith({
counterName: 'GET /error',
counterType: 'error',
});
});
});
describe('errors', () => {
it('logs the error', async () => {
await initAndSimulateError();
expect(logger.error).toBeCalledWith('API error');
});
it('returns an error response', async () => {
await initAndSimulateError();
expect(customError).toBeCalledWith({
body: expect.anything(),
headers: {},
statusCode: 500,
});
});
it('returns an error response when the case context is not registered', async () => {
const { simulateRequest } = initApi(routes);
await simulateRequest({
method: 'get',
path: '/foo/{case_id}',
context: {},
});
expect(badRequest).toBeCalledWith({
body: 'RouteHandlerContext is not registered for cases',
});
});
});
});

View file

@ -0,0 +1,88 @@
/*
* 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 { RouteRegistrar } from 'kibana/server';
import { CasesRequestHandlerContext } from '../../types';
import { CaseRoute, RegisterRoutesDeps } from './types';
import { escapeHatch, getIsKibanaRequest, wrapError } from './utils';
const increaseTelemetryCounters = ({
telemetryUsageCounter,
method,
path,
isKibanaRequest,
isError = false,
}: {
telemetryUsageCounter: Exclude<RegisterRoutesDeps['telemetryUsageCounter'], undefined>;
method: string;
path: string;
isKibanaRequest: boolean;
isError?: boolean;
}) => {
const counterName = `${method.toUpperCase()} ${path}`;
telemetryUsageCounter.incrementCounter({
counterName,
counterType: isError ? 'error' : 'success',
});
telemetryUsageCounter.incrementCounter({
counterName,
counterType: `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
});
};
export const registerRoutes = (deps: RegisterRoutesDeps) => {
const { router, routes, logger, kibanaVersion, telemetryUsageCounter } = deps;
routes.forEach((route) => {
const { method, path, params, handler } = route as CaseRoute;
(router[method] as RouteRegistrar<typeof method, CasesRequestHandlerContext>)(
{
path,
validate: {
params: params?.params ?? escapeHatch,
query: params?.query ?? escapeHatch,
body: params?.body ?? schema.nullable(escapeHatch),
},
},
async (context, request, response) => {
const isKibanaRequest = getIsKibanaRequest(request.headers);
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
try {
const res = await handler({ logger, context, request, response, kibanaVersion });
if (telemetryUsageCounter) {
increaseTelemetryCounters({ telemetryUsageCounter, method, path, isKibanaRequest });
}
return res;
} catch (error) {
logger.error(error.message);
if (telemetryUsageCounter) {
increaseTelemetryCounters({
telemetryUsageCounter,
method,
path,
isError: true,
isKibanaRequest,
});
}
return response.customError(wrapError(error));
}
}
);
});
};

View file

@ -5,40 +5,40 @@
* 2.0.
*/
import { RouteDeps } from '../types';
import { escapeHatch, wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { CaseRoute } from '../types';
import { getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { CasesStatusRequest } from '../../../../common/api';
import { CASE_STATUS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
/**
* @deprecated since version 8.1.0
*/
export function initGetCasesStatusApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_STATUS_URL,
validate: { query: escapeHatch },
},
async (context, request, response) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get cases status API '${CASE_STATUS_URL}' is deprecated.`
);
export const getStatusRoute: CaseRoute = createCasesRoute({
method: 'get',
path: CASE_STATUS_URL,
handler: async ({ context, request, response, logger, kibanaVersion }) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get cases status API '${CASE_STATUS_URL}' is deprecated.`
);
const client = await context.cases.getCasesClient();
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await client.metrics.getStatusTotalsByType(request.query as CasesStatusRequest),
});
} catch (error) {
logger.error(`Failed to get status stats in route: ${error}`);
return response.customError(wrapError(error));
}
const client = await context.cases.getCasesClient();
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await client.metrics.getStatusTotalsByType(request.query as CasesStatusRequest),
});
} catch (error) {
throw createCaseError({
message: `Failed to get status stats in route: ${error}`,
error,
});
}
);
}
},
});

View file

@ -5,17 +5,44 @@
* 2.0.
*/
import type { Logger, PluginInitializerContext } from 'kibana/server';
import type {
Logger,
PluginInitializerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
RouteValidatorConfig,
} from 'kibana/server';
import type { CasesRouter } from '../../types';
import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/server';
import type { CasesRequestHandlerContext, CasesRouter } from '../../types';
export interface RouteDeps {
type TelemetryUsageCounter = ReturnType<UsageCollectionSetup['createUsageCounter']>;
export interface RegisterRoutesDeps {
router: CasesRouter;
routes: CaseRoute[];
logger: Logger;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
telemetryUsageCounter?: TelemetryUsageCounter;
}
export interface TotalCommentByCase {
caseId: string;
totalComments: number;
}
interface CaseRouteHandlerArguments<P, Q, B> {
request: KibanaRequest<P, Q, B>;
context: CasesRequestHandlerContext;
response: KibanaResponseFactory;
logger: Logger;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
}
export interface CaseRoute<P = unknown, Q = unknown, B = unknown> {
method: 'get' | 'post' | 'put' | 'delete' | 'patch';
path: string;
params?: RouteValidatorConfig<P, Q, B>;
handler: (args: CaseRouteHandlerArguments<P, Q, B>) => Promise<IKibanaResponse>;
}

View file

@ -7,50 +7,44 @@
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '../types';
import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils';
import { getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { CASE_USER_ACTIONS_URL } from '../../../../common/constants';
import { createCaseError } from '../../../common/error';
import { createCasesRoute } from '../create_cases_route';
/**
* @deprecated since version 8.1.0
*/
export function initGetAllCaseUserActionsApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_USER_ACTIONS_URL,
validate: {
params: schema.object({
case_id: schema.string(),
}),
},
},
async (context, request, response) => {
try {
if (!context.cases) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}
export const getUserActionsRoute = createCasesRoute({
method: 'get',
path: CASE_USER_ACTIONS_URL,
params: {
params: schema.object({
case_id: schema.string(),
}),
},
handler: async ({ context, request, response, logger, kibanaVersion }) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get all cases user actions API '${CASE_USER_ACTIONS_URL}' is deprecated.`
);
logDeprecatedEndpoint(
logger,
request.headers,
`The get all cases user actions API '${CASE_USER_ACTIONS_URL}' is deprecated.`
);
const casesClient = await context.cases.getCasesClient();
const caseId = request.params.case_id;
const casesClient = await context.cases.getCasesClient();
const caseId = request.params.case_id;
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await casesClient.userActions.getAll({ caseId }),
});
} catch (error) {
logger.error(
`Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}`
);
return response.customError(wrapError(error));
}
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await casesClient.userActions.getAll({ caseId }),
});
} catch (error) {
throw createCaseError({
message: `Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}`,
error,
});
}
);
}
},
});

View file

@ -52,10 +52,10 @@ export const getWarningHeader = (
* https://github.com/elastic/kibana/blob/ec30f2aeeb10fb64b507935e558832d3ef5abfaa/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts#L113-L118
*/
const getIsKibanaRequest = (headers?: Headers) => {
export const getIsKibanaRequest = (headers?: Headers): boolean => {
// The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client.
// We can't be 100% certain, but this is a reasonable attempt.
return headers && headers['kbn-version'] && headers.referer;
return !!(headers && headers['kbn-version'] && headers.referer);
};
export const logDeprecatedEndpoint = (logger: Logger, headers: Headers, msg: string) => {

View file

@ -39,7 +39,7 @@ import {
} from '../../../common/api';
import { SavedObjectFindOptionsKueryNode } from '../../common/types';
import { defaultSortField, flattenCaseSavedObject } from '../../common/utils';
import { defaultPage, defaultPerPage } from '../../routes/api';
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../routes/api';
import { combineFilters } from '../../client/utils';
import { includeFieldsRequiredForAuthentication } from '../../authorization/utils';
import {
@ -420,8 +420,8 @@ export class CasesService {
return {
saved_objects: [],
total: 0,
per_page: options?.perPage ?? defaultPerPage,
page: options?.page ?? defaultPage,
per_page: options?.perPage ?? DEFAULT_PER_PAGE,
page: options?.page ?? DEFAULT_PAGE,
};
}