[SIEM] [Case] Case workflow api schema (#51535)

This commit is contained in:
Steph Milovic 2020-01-08 14:28:29 -07:00 committed by GitHub
parent 26ce6104a9
commit 303e4842ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1909 additions and 0 deletions

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
/* eslint-disable @typescript-eslint/camelcase */
import {
NewCaseFormatted,
NewCommentFormatted,
} from '../../../../../../../x-pack/plugins/case/server';
import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings';
// Temporary file to write mappings for case
// while Saved Object Mappings API is programmed for the NP
// See: https://github.com/elastic/kibana/issues/50309
export const caseSavedObjectType = 'case-workflow';
export const caseCommentSavedObjectType = 'case-workflow-comment';
export const caseSavedObjectMappings: {
[caseSavedObjectType]: ElasticsearchMappingOf<NewCaseFormatted>;
} = {
[caseSavedObjectType]: {
properties: {
assignees: {
properties: {
username: {
type: 'keyword',
},
full_name: {
type: 'keyword',
},
},
},
created_at: {
type: 'date',
},
description: {
type: 'text',
},
title: {
type: 'keyword',
},
created_by: {
properties: {
username: {
type: 'keyword',
},
full_name: {
type: 'keyword',
},
},
},
state: {
type: 'keyword',
},
tags: {
type: 'keyword',
},
case_type: {
type: 'keyword',
},
},
},
};
export const caseCommentSavedObjectMappings: {
[caseCommentSavedObjectType]: ElasticsearchMappingOf<NewCommentFormatted>;
} = {
[caseCommentSavedObjectType]: {
properties: {
comment: {
type: 'text',
},
created_at: {
type: 'date',
},
created_by: {
properties: {
full_name: {
type: 'keyword',
},
username: {
type: 'keyword',
},
},
},
},
},
};

View file

@ -0,0 +1,9 @@
# Case Workflow
*Experimental Feature*
Elastic is developing a Case Management Workflow. Follow our progress:
- [Case API Documentation](https://documenter.getpostman.com/view/172706/SW7c2SuF?version=latest)
- [Github Meta](https://github.com/elastic/kibana/issues/50103)

View file

@ -0,0 +1,9 @@
{
"configPath": ["xpack", "case"],
"id": "case",
"kibanaVersion": "kibana",
"requiredPlugins": ["security"],
"server": true,
"ui": false,
"version": "8.0.0"
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema, TypeOf } from '@kbn/config-schema';
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
indexPattern: schema.string({ defaultValue: '.case-test-2' }),
secret: schema.string({ defaultValue: 'Cool secret huh?' }),
});
export type ConfigType = TypeOf<typeof ConfigSchema>;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const CASE_SAVED_OBJECT = 'case-workflow';
export const CASE_COMMENT_SAVED_OBJECT = 'case-workflow-comment';

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from '../../../../src/core/server';
import { ConfigSchema } from './config';
import { CasePlugin } from './plugin';
export { NewCaseFormatted, NewCommentFormatted } from './routes/api/types';
export const config = { schema: ConfigSchema };
export const plugin = (initializerContext: PluginInitializerContext) =>
new CasePlugin(initializerContext);

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { first, map } from 'rxjs/operators';
import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server';
import { ConfigType } from './config';
import { initCaseApi } from './routes/api';
import { CaseService } from './services';
import { PluginSetupContract as SecurityPluginSetup } from '../../security/server';
function createConfig$(context: PluginInitializerContext) {
return context.config.create<ConfigType>().pipe(map(config => config));
}
export interface PluginsSetup {
security: SecurityPluginSetup;
}
export class CasePlugin {
private readonly log: Logger;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.log = this.initializerContext.logger.get();
}
public async setup(core: CoreSetup, plugins: PluginsSetup) {
const config = await createConfig$(this.initializerContext)
.pipe(first())
.toPromise();
if (!config.enabled) {
return;
}
const service = new CaseService(this.log);
this.log.debug(
`Setting up Case Workflow with core contract [${Object.keys(
core
)}] and plugins [${Object.keys(plugins)}]`
);
const caseService = await service.setup({
authentication: plugins.security.authc,
});
const router = core.http.createRouter();
initCaseApi({
caseService,
router,
});
}
public start() {
this.log.debug(`Starting Case Workflow`);
}
public stop() {
this.log.debug(`Stopping Case Workflow`);
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Authentication } from '../../../../../security/server';
const getCurrentUser = jest.fn().mockReturnValue({
username: 'awesome',
full_name: 'Awesome D00d',
});
const getCurrentUserThrow = jest.fn().mockImplementation(() => {
throw new Error('Bad User - the user is not authenticated');
});
export const authenticationMock = {
create: (): jest.Mocked<Authentication> => ({
login: jest.fn(),
createAPIKey: jest.fn(),
getCurrentUser,
invalidateAPIKey: jest.fn(),
isAuthenticated: jest.fn(),
logout: jest.fn(),
getSessionInfo: jest.fn(),
}),
createInvalid: (): jest.Mocked<Authentication> => ({
login: jest.fn(),
createAPIKey: jest.fn(),
getCurrentUser: getCurrentUserThrow,
invalidateAPIKey: jest.fn(),
isAuthenticated: jest.fn(),
logout: jest.fn(),
getSessionInfo: jest.fn(),
}),
};

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from 'src/core/server';
import { CASE_COMMENT_SAVED_OBJECT } from '../../../constants';
export const createMockSavedObjectsRepository = (savedObject: any[] = []) => {
const mockSavedObjectsClientContract = ({
get: jest.fn((type, id) => {
const result = savedObject.filter(s => s.id === id);
if (!result.length) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return result[0];
}),
find: jest.fn(findArgs => {
if (findArgs.hasReference && findArgs.hasReference.id === 'bad-guy') {
throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing');
}
return {
total: savedObject.length,
saved_objects: savedObject,
};
}),
create: jest.fn((type, attributes, references) => {
if (attributes.description === 'Throw an error' || attributes.comment === 'Throw an error') {
throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing');
}
if (type === CASE_COMMENT_SAVED_OBJECT) {
return {
type,
id: 'mock-comment',
attributes,
...references,
updated_at: '2019-12-02T22:48:08.327Z',
version: 'WzksMV0=',
};
}
return {
type,
id: 'mock-it',
attributes,
references: [],
updated_at: '2019-12-02T22:48:08.327Z',
version: 'WzksMV0=',
};
}),
update: jest.fn((type, id, attributes) => {
if (!savedObject.find(s => s.id === id)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
return {
id,
type,
updated_at: '2019-11-22T22:50:55.191Z',
version: 'WzE3LDFd',
attributes,
};
}),
delete: jest.fn((type: string, id: string) => {
const result = savedObject.filter(s => s.id === id);
if (!result.length) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
if (type === 'case-workflow-comment' && id === 'bad-guy') {
throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing');
}
return {};
}),
deleteByNamespace: jest.fn(),
} as unknown) as jest.Mocked<SavedObjectsClientContract>;
return mockSavedObjectsClientContract;
};

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { mockCases, mockCasesErrorTriggerData, mockCaseComments } from './mock_saved_objects';
export { createMockSavedObjectsRepository } from './create_mock_so_repository';
export { createRouteContext } from './route_contexts';
export { authenticationMock } from './authc_mock';
export { createRoute } from './mock_router';

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter } from 'kibana/server';
import { loggingServiceMock, httpServiceMock } from '../../../../../../../src/core/server/mocks';
import { CaseService } from '../../../services';
import { authenticationMock } from '../__fixtures__';
import { RouteDeps } from '../index';
export const createRoute = async (
api: (deps: RouteDeps) => void,
method: 'get' | 'post' | 'delete',
badAuth = false
) => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter('') as jest.Mocked<IRouter>;
const log = loggingServiceMock.create().get('case');
const service = new CaseService(log);
const caseService = await service.setup({
authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(),
});
api({
router,
caseService,
});
return router[method].mock.calls[0][1];
};

View file

@ -0,0 +1,143 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const mockCases = [
{
type: 'case-workflow',
id: 'mock-id-1',
attributes: {
created_at: 1574718888885,
created_by: {
full_name: null,
username: 'elastic',
},
description: 'This is a brand new case of a bad meanie defacing data',
title: 'Super Bad Security Issue',
state: 'open',
tags: ['defacement'],
case_type: 'security',
assignees: [],
},
references: [],
updated_at: '2019-11-25T21:54:48.952Z',
version: 'WzAsMV0=',
},
{
type: 'case-workflow',
id: 'mock-id-2',
attributes: {
created_at: 1574721120834,
created_by: {
full_name: null,
username: 'elastic',
},
description: 'Oh no, a bad meanie destroying data!',
title: 'Damaging Data Destruction Detected',
state: 'open',
tags: ['Data Destruction'],
case_type: 'security',
assignees: [],
},
references: [],
updated_at: '2019-11-25T22:32:00.900Z',
version: 'WzQsMV0=',
},
{
type: 'case-workflow',
id: 'mock-id-3',
attributes: {
created_at: 1574721137881,
created_by: {
full_name: null,
username: 'elastic',
},
description: 'Oh no, a bad meanie going LOLBins all over the place!',
title: 'Another bad one',
state: 'open',
tags: ['LOLBins'],
case_type: 'security',
assignees: [],
},
references: [],
updated_at: '2019-11-25T22:32:17.947Z',
version: 'WzUsMV0=',
},
];
export const mockCasesErrorTriggerData = [
{
id: 'valid-id',
},
{
id: 'bad-guy',
},
];
export const mockCaseComments = [
{
type: 'case-workflow-comment',
id: 'mock-comment-1',
attributes: {
comment: 'Wow, good luck catching that bad meanie!',
created_at: 1574718900112,
created_by: {
full_name: null,
username: 'elastic',
},
},
references: [
{
type: 'case-workflow',
name: 'associated-case-workflow',
id: 'mock-id-1',
},
],
updated_at: '2019-11-25T21:55:00.177Z',
version: 'WzEsMV0=',
},
{
type: 'case-workflow-comment',
id: 'mock-comment-2',
attributes: {
comment: 'Well I decided to update my comment. So what? Deal with it.',
created_at: 1574718902724,
created_by: {
full_name: null,
username: 'elastic',
},
},
references: [
{
type: 'case-workflow',
name: 'associated-case-workflow',
id: 'mock-id-1',
},
],
updated_at: '2019-11-25T21:55:14.633Z',
version: 'WzMsMV0=',
},
{
type: 'case-workflow-comment',
id: 'mock-comment-3',
attributes: {
comment: 'Wow, good luck catching that bad meanie!',
created_at: 1574721150542,
created_by: {
full_name: null,
username: 'elastic',
},
},
references: [
{
type: 'case-workflow',
name: 'associated-case-workflow',
id: 'mock-id-3',
},
],
updated_at: '2019-11-25T22:32:30.608Z',
version: 'WzYsMV0=',
},
];

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandlerContext } from 'src/core/server';
export const createRouteContext = (client: any) => {
return ({
core: {
savedObjects: {
client,
},
},
} as unknown) as RequestHandlerContext;
};

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
mockCasesErrorTriggerData,
} from '../__fixtures__';
import { initDeleteCaseApi } from '../delete_case';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('DELETE case', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initDeleteCaseApi, 'delete');
});
it(`deletes the case. responds with 204`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
method: 'delete',
params: {
id: 'mock-id-1',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(204);
});
it(`returns an error when thrown from deleteCase service`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
method: 'delete',
params: {
id: 'not-real',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
it(`returns an error when thrown from getAllCaseComments service`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
method: 'delete',
params: {
id: 'bad-guy',
},
});
const theContext = createRouteContext(
createMockSavedObjectsRepository(mockCasesErrorTriggerData)
);
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
it(`returns an error when thrown from deleteComment service`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
method: 'delete',
params: {
id: 'valid-id',
},
});
const theContext = createRouteContext(
createMockSavedObjectsRepository(mockCasesErrorTriggerData)
);
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
});

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
mockCasesErrorTriggerData,
} from '../__fixtures__';
import { initDeleteCommentApi } from '../delete_comment';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('DELETE comment', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initDeleteCommentApi, 'delete');
});
it(`deletes the comment. responds with 204`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/comments/{comment_id}',
method: 'delete',
params: {
comment_id: 'mock-id-1',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(204);
});
it(`returns an error when thrown from deleteComment service`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/comments/{comment_id}',
method: 'delete',
params: {
comment_id: 'bad-guy',
},
});
const theContext = createRouteContext(
createMockSavedObjectsRepository(mockCasesErrorTriggerData)
);
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
} from '../__fixtures__';
import { initGetAllCasesApi } from '../get_all_cases';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('GET all cases', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initGetAllCasesApi, 'get');
});
it(`returns the case without case comments when includeComments is false`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases',
method: 'get',
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.saved_objects).toHaveLength(3);
});
});

View file

@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
mockCasesErrorTriggerData,
} from '../__fixtures__';
import { initGetCaseApi } from '../get_case';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('GET case', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initGetCaseApi, 'get');
});
it(`returns the case without case comments when includeComments is false`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
params: {
id: 'mock-id-1',
},
method: 'get',
query: {
includeComments: false,
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload).toEqual(mockCases.find(s => s.id === 'mock-id-1'));
expect(response.payload.comments).toBeUndefined();
});
it(`returns an error when thrown from getCase`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
params: {
id: 'abcdefg',
},
method: 'get',
query: {
includeComments: false,
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
it(`returns the case with case comments when includeComments is true`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
params: {
id: 'mock-id-1',
},
method: 'get',
query: {
includeComments: true,
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments.saved_objects).toHaveLength(3);
});
it(`returns an error when thrown from getAllCaseComments`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
params: {
id: 'bad-guy',
},
method: 'get',
query: {
includeComments: true,
},
});
const theContext = createRouteContext(
createMockSavedObjectsRepository(mockCasesErrorTriggerData)
);
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCaseComments,
} from '../__fixtures__';
import { initGetCommentApi } from '../get_comment';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('GET comment', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initGetCommentApi, 'get');
});
it(`returns the comment`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/comments/{id}',
method: 'get',
params: {
id: 'mock-comment-1',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload).toEqual(mockCaseComments.find(s => s.id === 'mock-comment-1'));
});
it(`returns an error when getComment throws`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/comments/{id}',
method: 'get',
params: {
id: 'not-real',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
});

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
} from '../__fixtures__';
import { initPostCaseApi } from '../post_case';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('POST cases', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initPostCaseApi, 'post');
});
it(`Posts a new case`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases',
method: 'post',
body: {
description: 'This is a brand new case of a bad meanie defacing data',
title: 'Super Bad Security Issue',
state: 'open',
tags: ['defacement'],
case_type: 'security',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.id).toEqual('mock-it');
expect(response.payload.attributes.created_by.username).toEqual('awesome');
});
it(`Returns an error if postNewCase throws`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases',
method: 'post',
body: {
description: 'Throw an error',
title: 'Super Bad Security Issue',
state: 'open',
tags: ['error'],
case_type: 'security',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
});
it(`Returns an error if user authentication throws`, async () => {
routeHandler = await createRoute(initPostCaseApi, 'post', true);
const request = httpServerMock.createKibanaRequest({
path: '/api/cases',
method: 'post',
body: {
description: 'This is a brand new case of a bad meanie defacing data',
title: 'Super Bad Security Issue',
state: 'open',
tags: ['defacement'],
case_type: 'security',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(500);
expect(response.payload.isBoom).toEqual(true);
});
});

View file

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
} from '../__fixtures__';
import { initPostCommentApi } from '../post_comment';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('POST comment', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initPostCommentApi, 'post');
});
it(`Posts a new comment`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}/comment',
method: 'post',
params: {
id: 'mock-id-1',
},
body: {
comment: 'Wow, good luck catching that bad meanie!',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.id).toEqual('mock-comment');
expect(response.payload.references[0].id).toEqual('mock-id-1');
});
it(`Returns an error if the case does not exist`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}/comment',
method: 'post',
params: {
id: 'this-is-not-real',
},
body: {
comment: 'Wow, good luck catching that bad meanie!',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
it(`Returns an error if postNewCase throws`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}/comment',
method: 'post',
params: {
id: 'mock-id-1',
},
body: {
comment: 'Throw an error',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(400);
expect(response.payload.isBoom).toEqual(true);
});
it(`Returns an error if user authentication throws`, async () => {
routeHandler = await createRoute(initPostCommentApi, 'post', true);
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}/comment',
method: 'post',
params: {
id: 'mock-id-1',
},
body: {
comment: 'Wow, good luck catching that bad meanie!',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(500);
expect(response.payload.isBoom).toEqual(true);
});
});

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
} from '../__fixtures__';
import { initUpdateCaseApi } from '../update_case';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('UPDATE case', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initUpdateCaseApi, 'post');
});
it(`Updates a case`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
method: 'post',
params: {
id: 'mock-id-1',
},
body: {
state: 'closed',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.id).toEqual('mock-id-1');
expect(response.payload.attributes.state).toEqual('closed');
});
it(`Returns an error if updateCase throws`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/{id}',
method: 'post',
params: {
id: 'mock-id-does-not-exist',
},
body: {
state: 'closed',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCases));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
});

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCaseComments,
} from '../__fixtures__';
import { initUpdateCommentApi } from '../update_comment';
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
describe('UPDATE comment', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeAll(async () => {
routeHandler = await createRoute(initUpdateCommentApi, 'post');
});
it(`Updates a comment`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/comment/{id}',
method: 'post',
params: {
id: 'mock-comment-1',
},
body: {
comment: 'Update my comment',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.id).toEqual('mock-comment-1');
expect(response.payload.attributes.comment).toEqual('Update my comment');
});
it(`Returns an error if updateComment throws`, async () => {
const request = httpServerMock.createKibanaRequest({
path: '/api/cases/comment/{id}',
method: 'post',
params: {
id: 'mock-comment-does-not-exist',
},
body: {
comment: 'Update my comment',
},
});
const theContext = createRouteContext(createMockSavedObjectsRepository(mockCaseComments));
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
expect(response.payload.isBoom).toEqual(true);
});
});

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '.';
import { wrapError } from './utils';
export function initDeleteCaseApi({ caseService, router }: RouteDeps) {
router.delete(
{
path: '/api/cases/{id}',
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response) => {
let allCaseComments;
try {
await caseService.deleteCase({
client: context.core.savedObjects.client,
caseId: request.params.id,
});
} catch (error) {
return response.customError(wrapError(error));
}
try {
allCaseComments = await caseService.getAllCaseComments({
client: context.core.savedObjects.client,
caseId: request.params.id,
});
} catch (error) {
return response.customError(wrapError(error));
}
try {
if (allCaseComments.saved_objects.length > 0) {
await Promise.all(
allCaseComments.saved_objects.map(({ id }) =>
caseService.deleteComment({
client: context.core.savedObjects.client,
commentId: id,
})
)
);
}
return response.noContent();
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '.';
import { wrapError } from './utils';
export function initDeleteCommentApi({ caseService, router }: RouteDeps) {
router.delete(
{
path: '/api/cases/comments/{comment_id}',
validate: {
params: schema.object({
comment_id: schema.string(),
}),
},
},
async (context, request, response) => {
const client = context.core.savedObjects.client;
try {
await caseService.deleteComment({
client,
commentId: request.params.comment_id,
});
return response.noContent();
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '.';
import { wrapError } from './utils';
export function initGetAllCaseCommentsApi({ caseService, router }: RouteDeps) {
router.get(
{
path: '/api/cases/{id}/comments',
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response) => {
try {
const theComments = await caseService.getAllCaseComments({
client: context.core.savedObjects.client,
caseId: request.params.id,
});
return response.ok({ body: theComments });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RouteDeps } from '.';
import { wrapError } from './utils';
export function initGetAllCasesApi({ caseService, router }: RouteDeps) {
router.get(
{
path: '/api/cases',
validate: false,
},
async (context, request, response) => {
try {
const cases = await caseService.getAllCases({
client: context.core.savedObjects.client,
});
return response.ok({ body: cases });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '.';
import { wrapError } from './utils';
export function initGetCaseApi({ caseService, router }: RouteDeps) {
router.get(
{
path: '/api/cases/{id}',
validate: {
params: schema.object({
id: schema.string(),
}),
query: schema.object({
includeComments: schema.string({ defaultValue: 'true' }),
}),
},
},
async (context, request, response) => {
let theCase;
const includeComments = JSON.parse(request.query.includeComments);
try {
theCase = await caseService.getCase({
client: context.core.savedObjects.client,
caseId: request.params.id,
});
} catch (error) {
return response.customError(wrapError(error));
}
if (!includeComments) {
return response.ok({ body: theCase });
}
try {
const theComments = await caseService.getAllCaseComments({
client: context.core.savedObjects.client,
caseId: request.params.id,
});
return response.ok({ body: { ...theCase, comments: theComments } });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RouteDeps } from '.';
import { wrapError } from './utils';
export function initGetCommentApi({ caseService, router }: RouteDeps) {
router.get(
{
path: '/api/cases/comments/{id}',
validate: {
params: schema.object({
id: schema.string(),
}),
},
},
async (context, request, response) => {
try {
const theComment = await caseService.getComment({
client: context.core.savedObjects.client,
commentId: request.params.id,
});
return response.ok({ body: theComment });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter } from 'src/core/server';
import { initDeleteCommentApi } from './delete_comment';
import { initDeleteCaseApi } from './delete_case';
import { initGetAllCaseCommentsApi } from './get_all_case_comments';
import { initGetAllCasesApi } from './get_all_cases';
import { initGetCaseApi } from './get_case';
import { initGetCommentApi } from './get_comment';
import { initPostCaseApi } from './post_case';
import { initPostCommentApi } from './post_comment';
import { initUpdateCaseApi } from './update_case';
import { initUpdateCommentApi } from './update_comment';
import { CaseServiceSetup } from '../../services';
export interface RouteDeps {
caseService: CaseServiceSetup;
router: IRouter;
}
export function initCaseApi(deps: RouteDeps) {
initGetAllCaseCommentsApi(deps);
initGetAllCasesApi(deps);
initGetCaseApi(deps);
initGetCommentApi(deps);
initDeleteCaseApi(deps);
initDeleteCommentApi(deps);
initPostCaseApi(deps);
initPostCommentApi(deps);
initUpdateCaseApi(deps);
initUpdateCommentApi(deps);
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { formatNewCase, wrapError } from './utils';
import { NewCaseSchema } from './schema';
import { RouteDeps } from '.';
export function initPostCaseApi({ caseService, router }: RouteDeps) {
router.post(
{
path: '/api/cases',
validate: {
body: NewCaseSchema,
},
},
async (context, request, response) => {
let createdBy;
try {
createdBy = await caseService.getUser({ request, response });
} catch (error) {
return response.customError(wrapError(error));
}
try {
const newCase = await caseService.postNewCase({
client: context.core.savedObjects.client,
attributes: formatNewCase(request.body, {
...createdBy,
}),
});
return response.ok({ body: newCase });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { formatNewComment, wrapError } from './utils';
import { NewCommentSchema } from './schema';
import { RouteDeps } from '.';
import { CASE_SAVED_OBJECT } from '../../constants';
export function initPostCommentApi({ caseService, router }: RouteDeps) {
router.post(
{
path: '/api/cases/{id}/comment',
validate: {
params: schema.object({
id: schema.string(),
}),
body: NewCommentSchema,
},
},
async (context, request, response) => {
let createdBy;
let newComment;
try {
await caseService.getCase({
client: context.core.savedObjects.client,
caseId: request.params.id,
});
} catch (error) {
return response.customError(wrapError(error));
}
try {
createdBy = await caseService.getUser({ request, response });
} catch (error) {
return response.customError(wrapError(error));
}
try {
newComment = await caseService.postNewComment({
client: context.core.savedObjects.client,
attributes: formatNewComment({
newComment: request.body,
...createdBy,
}),
references: [
{
type: CASE_SAVED_OBJECT,
name: `associated-${CASE_SAVED_OBJECT}`,
id: request.params.id,
},
],
});
return response.ok({ body: newComment });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
export const UserSchema = schema.object({
username: schema.string(),
full_name: schema.maybe(schema.string()),
});
export const NewCommentSchema = schema.object({
comment: schema.string(),
});
export const CommentSchema = schema.object({
comment: schema.string(),
created_at: schema.number(),
created_by: UserSchema,
});
export const UpdatedCommentSchema = schema.object({
comment: schema.string(),
});
export const NewCaseSchema = schema.object({
assignees: schema.arrayOf(UserSchema, { defaultValue: [] }),
description: schema.string(),
title: schema.string(),
state: schema.oneOf([schema.literal('open'), schema.literal('closed')], { defaultValue: 'open' }),
tags: schema.arrayOf(schema.string(), { defaultValue: [] }),
case_type: schema.string(),
});
export const UpdatedCaseSchema = schema.object({
assignees: schema.maybe(schema.arrayOf(UserSchema)),
description: schema.maybe(schema.string()),
title: schema.maybe(schema.string()),
state: schema.maybe(schema.oneOf([schema.literal('open'), schema.literal('closed')])),
tags: schema.maybe(schema.arrayOf(schema.string())),
case_type: schema.maybe(schema.string()),
});

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TypeOf } from '@kbn/config-schema';
import {
CommentSchema,
NewCaseSchema,
NewCommentSchema,
UpdatedCaseSchema,
UpdatedCommentSchema,
UserSchema,
} from './schema';
export type NewCaseType = TypeOf<typeof NewCaseSchema>;
export type NewCommentFormatted = TypeOf<typeof CommentSchema>;
export type NewCommentType = TypeOf<typeof NewCommentSchema>;
export type UpdatedCaseTyped = TypeOf<typeof UpdatedCaseSchema>;
export type UpdatedCommentType = TypeOf<typeof UpdatedCommentSchema>;
export type UserType = TypeOf<typeof UserSchema>;
export interface NewCaseFormatted extends NewCaseType {
created_at: number;
created_by: UserType;
}
export interface UpdatedCaseType {
assignees?: UpdatedCaseTyped['assignees'];
description?: UpdatedCaseTyped['description'];
title?: UpdatedCaseTyped['title'];
state?: UpdatedCaseTyped['state'];
tags?: UpdatedCaseTyped['tags'];
case_type?: UpdatedCaseTyped['case_type'];
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { wrapError } from './utils';
import { RouteDeps } from '.';
import { UpdatedCaseSchema } from './schema';
export function initUpdateCaseApi({ caseService, router }: RouteDeps) {
router.post(
{
path: '/api/cases/{id}',
validate: {
params: schema.object({
id: schema.string(),
}),
body: UpdatedCaseSchema,
},
},
async (context, request, response) => {
try {
const updatedCase = await caseService.updateCase({
client: context.core.savedObjects.client,
caseId: request.params.id,
updatedAttributes: request.body,
});
return response.ok({ body: updatedCase });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { wrapError } from './utils';
import { NewCommentSchema } from './schema';
import { RouteDeps } from '.';
export function initUpdateCommentApi({ caseService, router }: RouteDeps) {
router.post(
{
path: '/api/cases/comment/{id}',
validate: {
params: schema.object({
id: schema.string(),
}),
body: NewCommentSchema,
},
},
async (context, request, response) => {
try {
const updatedComment = await caseService.updateComment({
client: context.core.savedObjects.client,
commentId: request.params.id,
updatedAttributes: request.body,
});
return response.ok({ body: updatedComment });
} catch (error) {
return response.customError(wrapError(error));
}
}
);
}

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { boomify, isBoom } from 'boom';
import { CustomHttpResponseOptions, ResponseError } from 'kibana/server';
import {
NewCaseType,
NewCaseFormatted,
NewCommentType,
NewCommentFormatted,
UserType,
} from './types';
export const formatNewCase = (
newCase: NewCaseType,
{ full_name, username }: { full_name?: string; username: string }
): NewCaseFormatted => ({
created_at: new Date().valueOf(),
created_by: { full_name, username },
...newCase,
});
interface NewCommentArgs {
newComment: NewCommentType;
full_name?: UserType['full_name'];
username: UserType['username'];
}
export const formatNewComment = ({
newComment,
full_name,
username,
}: NewCommentArgs): NewCommentFormatted => ({
...newComment,
created_at: new Date().valueOf(),
created_by: { full_name, username },
});
export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> {
const boom = isBoom(error) ? error : boomify(error);
return {
body: boom,
headers: boom.output.headers,
statusCode: boom.output.statusCode,
};
}

View file

@ -0,0 +1,192 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
KibanaRequest,
KibanaResponseFactory,
Logger,
SavedObject,
SavedObjectsClientContract,
SavedObjectsFindResponse,
SavedObjectsUpdateResponse,
SavedObjectReference,
} from 'kibana/server';
import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../constants';
import {
NewCaseFormatted,
NewCommentFormatted,
UpdatedCaseType,
UpdatedCommentType,
} from '../routes/api/types';
import {
AuthenticatedUser,
PluginSetupContract as SecurityPluginSetup,
} from '../../../security/server';
interface ClientArgs {
client: SavedObjectsClientContract;
}
interface GetCaseArgs extends ClientArgs {
caseId: string;
}
interface GetCommentArgs extends ClientArgs {
commentId: string;
}
interface PostCaseArgs extends ClientArgs {
attributes: NewCaseFormatted;
}
interface PostCommentArgs extends ClientArgs {
attributes: NewCommentFormatted;
references: SavedObjectReference[];
}
interface UpdateCaseArgs extends ClientArgs {
caseId: string;
updatedAttributes: UpdatedCaseType;
}
interface UpdateCommentArgs extends ClientArgs {
commentId: string;
updatedAttributes: UpdatedCommentType;
}
interface GetUserArgs {
request: KibanaRequest;
response: KibanaResponseFactory;
}
interface CaseServiceDeps {
authentication: SecurityPluginSetup['authc'];
}
export interface CaseServiceSetup {
deleteCase(args: GetCaseArgs): Promise<{}>;
deleteComment(args: GetCommentArgs): Promise<{}>;
getAllCases(args: ClientArgs): Promise<SavedObjectsFindResponse>;
getAllCaseComments(args: GetCaseArgs): Promise<SavedObjectsFindResponse>;
getCase(args: GetCaseArgs): Promise<SavedObject>;
getComment(args: GetCommentArgs): Promise<SavedObject>;
getUser(args: GetUserArgs): Promise<AuthenticatedUser>;
postNewCase(args: PostCaseArgs): Promise<SavedObject>;
postNewComment(args: PostCommentArgs): Promise<SavedObject>;
updateCase(args: UpdateCaseArgs): Promise<SavedObjectsUpdateResponse>;
updateComment(args: UpdateCommentArgs): Promise<SavedObjectsUpdateResponse>;
}
export class CaseService {
constructor(private readonly log: Logger) {}
public setup = async ({ authentication }: CaseServiceDeps): Promise<CaseServiceSetup> => ({
deleteCase: async ({ client, caseId }: GetCaseArgs) => {
try {
this.log.debug(`Attempting to GET case ${caseId}`);
return await client.delete(CASE_SAVED_OBJECT, caseId);
} catch (error) {
this.log.debug(`Error on GET case ${caseId}: ${error}`);
throw error;
}
},
deleteComment: async ({ client, commentId }: GetCommentArgs) => {
try {
this.log.debug(`Attempting to GET comment ${commentId}`);
return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId);
} catch (error) {
this.log.debug(`Error on GET comment ${commentId}: ${error}`);
throw error;
}
},
getCase: async ({ client, caseId }: GetCaseArgs) => {
try {
this.log.debug(`Attempting to GET case ${caseId}`);
return await client.get(CASE_SAVED_OBJECT, caseId);
} catch (error) {
this.log.debug(`Error on GET case ${caseId}: ${error}`);
throw error;
}
},
getComment: async ({ client, commentId }: GetCommentArgs) => {
try {
this.log.debug(`Attempting to GET comment ${commentId}`);
return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId);
} catch (error) {
this.log.debug(`Error on GET comment ${commentId}: ${error}`);
throw error;
}
},
getAllCases: async ({ client }: ClientArgs) => {
try {
this.log.debug(`Attempting to GET all cases`);
return await client.find({ type: CASE_SAVED_OBJECT });
} catch (error) {
this.log.debug(`Error on GET cases: ${error}`);
throw error;
}
},
getAllCaseComments: async ({ client, caseId }: GetCaseArgs) => {
try {
this.log.debug(`Attempting to GET all comments for case ${caseId}`);
return await client.find({
type: CASE_COMMENT_SAVED_OBJECT,
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
});
} catch (error) {
this.log.debug(`Error on GET all comments for case ${caseId}: ${error}`);
throw error;
}
},
getUser: async ({ request, response }: GetUserArgs) => {
let user;
try {
this.log.debug(`Attempting to authenticate a user`);
user = await authentication!.getCurrentUser(request);
} catch (error) {
this.log.debug(`Error on GET user: ${error}`);
throw error;
}
if (!user) {
this.log.debug(`Error on GET user: Bad User`);
throw new Error('Bad User - the user is not authenticated');
}
return user;
},
postNewCase: async ({ client, attributes }: PostCaseArgs) => {
try {
this.log.debug(`Attempting to POST a new case`);
return await client.create(CASE_SAVED_OBJECT, { ...attributes });
} catch (error) {
this.log.debug(`Error on POST a new case: ${error}`);
throw error;
}
},
postNewComment: async ({ client, attributes, references }: PostCommentArgs) => {
try {
this.log.debug(`Attempting to POST a new comment`);
return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references });
} catch (error) {
this.log.debug(`Error on POST a new comment: ${error}`);
throw error;
}
},
updateCase: async ({ client, caseId, updatedAttributes }: UpdateCaseArgs) => {
try {
this.log.debug(`Attempting to UPDATE case ${caseId}`);
return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes });
} catch (error) {
this.log.debug(`Error on UPDATE case ${caseId}: ${error}`);
throw error;
}
},
updateComment: async ({ client, commentId, updatedAttributes }: UpdateCommentArgs) => {
try {
this.log.debug(`Attempting to UPDATE comment ${commentId}`);
return await client.update(CASE_COMMENT_SAVED_OBJECT, commentId, {
...updatedAttributes,
});
} catch (error) {
this.log.debug(`Error on UPDATE comment ${commentId}: ${error}`);
throw error;
}
},
});
}

View file

@ -17,6 +17,7 @@ import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin';
// These exports are part of public Security plugin contract, any change in signature of exported
// functions or removal of exports should be considered as a breaking change.
export {
Authentication,
AuthenticationResult,
DeauthenticationResult,
CreateAPIKeyResult,
@ -24,6 +25,7 @@ export {
InvalidateAPIKeyResult,
} from './authentication';
export { PluginSetupContract };
export { AuthenticatedUser } from '../common/model';
export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
schema: ConfigSchema,