mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cases] Hide cases in stack management UI (#163037)
## Summary
fixes https://github.com/elastic/kibana/issues/160337
This PR
- hides cases in the serverless Elasticsearch project, cases APIs throw
error
- throws 403 from API when `owner=cases` for security or observability
serverless mode
- verifies the behaviour in serverless functional as well as
api_integration tests
**How to test**
- Boot up `es` serverless solution and make sure that `cases` from the
navbar is hidden and cannot not be accessible through url as well
- Boot up `observability` or `security` serverless solutions and make
sure that `cases` is available in the navbar and works fine
- Boot up classic kibana and make sure that the left navbar has the same
menu entries it always had.
### Checklist
Delete any items that are not applicable to this PR.
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
20c1974e
-44f0-45b0-80aa-e644fec148ff
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f4856f7478
commit
dc949ee373
30 changed files with 1364 additions and 23 deletions
|
@ -90,3 +90,6 @@ vis_type_timeseries.readOnly: true
|
|||
vis_type_vislib.readOnly: true
|
||||
vis_type_xy.readOnly: true
|
||||
input_control_vis.readOnly: true
|
||||
|
||||
# Disable cases in stack management
|
||||
xpack.cases.stack.enabled: false
|
||||
|
|
|
@ -197,6 +197,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.cases.files.allowedMimeTypes (array)',
|
||||
'xpack.cases.files.maxSize (number)',
|
||||
'xpack.cases.markdownPlugins.lens (boolean)',
|
||||
'xpack.cases.stack.enabled (boolean)',
|
||||
'xpack.ccr.ui.enabled (boolean)',
|
||||
'xpack.cloud.base_url (string)',
|
||||
'xpack.cloud.cname (string)',
|
||||
|
|
|
@ -59,6 +59,9 @@ export interface CasesUiConfigType {
|
|||
maxSize?: number;
|
||||
allowedMimeTypes: string[];
|
||||
};
|
||||
stack: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const StatusAll = 'all' as const;
|
||||
|
|
154
x-pack/plugins/cases/public/plugin.test.ts
Normal file
154
x-pack/plugins/cases/public/plugin.test.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
import { featuresPluginMock } from '@kbn/features-plugin/public/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/public/mocks';
|
||||
import { managementPluginMock } from '@kbn/management-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
|
||||
import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
|
||||
import { mockStorage } from '@kbn/kibana-utils-plugin/public/storage/hashed_item_store/mock';
|
||||
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
|
||||
import type { CasesPluginSetup, CasesPluginStart } from './types';
|
||||
import { CasesUiPlugin } from './plugin';
|
||||
import { ALLOWED_MIME_TYPES } from '../common/constants/mime_types';
|
||||
|
||||
function getConfig(overrides = {}) {
|
||||
return {
|
||||
markdownPlugins: { lens: true },
|
||||
files: { maxSize: 1, allowedMimeTypes: ALLOWED_MIME_TYPES },
|
||||
stack: { enabled: true },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Cases Ui Plugin', () => {
|
||||
let context: PluginInitializerContext;
|
||||
let plugin: CasesUiPlugin;
|
||||
let coreSetup: ReturnType<typeof coreMock.createSetup>;
|
||||
let coreStart: ReturnType<typeof coreMock.createStart>;
|
||||
let pluginsSetup: jest.Mocked<CasesPluginSetup>;
|
||||
let pluginsStart: jest.Mocked<CasesPluginStart>;
|
||||
|
||||
beforeEach(() => {
|
||||
context = coreMock.createPluginInitializerContext(getConfig());
|
||||
plugin = new CasesUiPlugin(context);
|
||||
coreSetup = coreMock.createSetup();
|
||||
coreStart = coreMock.createStart();
|
||||
|
||||
pluginsSetup = {
|
||||
files: {
|
||||
filesClientFactory: { asScoped: jest.fn(), asUnscoped: jest.fn() },
|
||||
registerFileKind: jest.fn(),
|
||||
},
|
||||
security: securityMock.createSetup(),
|
||||
management: managementPluginMock.createSetupContract(),
|
||||
};
|
||||
|
||||
pluginsStart = {
|
||||
licensing: licensingMock.createStart(),
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
files: {
|
||||
filesClientFactory: { asScoped: jest.fn(), asUnscoped: jest.fn() },
|
||||
getAllFindKindDefinitions: jest.fn(),
|
||||
getFileKindDefinition: jest.fn(),
|
||||
},
|
||||
features: featuresPluginMock.createStart(),
|
||||
security: securityMock.createStart(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
embeddable: embeddablePluginMock.createStartContract(),
|
||||
lens: lensPluginMock.createStartContract(),
|
||||
contentManagement: contentManagementMock.createStartContract(),
|
||||
storage: {
|
||||
store: {
|
||||
getItem: mockStorage.getItem,
|
||||
setItem: mockStorage.setItem,
|
||||
removeItem: mockStorage.removeItem,
|
||||
clear: mockStorage.clear,
|
||||
},
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
triggersActionsUi: triggersActionsUiMock.createStart(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('setup()', () => {
|
||||
it('should start setup cases plugin correctly', async () => {
|
||||
const setup = plugin.setup(coreSetup, pluginsSetup);
|
||||
|
||||
expect(setup).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attachmentFramework": Object {
|
||||
"registerExternalReference": [Function],
|
||||
"registerPersistableState": [Function],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should register kibana feature when stack is enabled', async () => {
|
||||
plugin.setup(coreSetup, pluginsSetup);
|
||||
|
||||
expect(
|
||||
pluginsSetup.management.sections.section.insightsAndAlerting.registerApp
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not register kibana feature when stack is disabled', async () => {
|
||||
context = coreMock.createPluginInitializerContext(getConfig({ stack: { enabled: false } }));
|
||||
const pluginWithStackDisabled = new CasesUiPlugin(context);
|
||||
|
||||
pluginWithStackDisabled.setup(coreSetup, pluginsSetup);
|
||||
|
||||
expect(
|
||||
pluginsSetup.management.sections.section.insightsAndAlerting.registerApp
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should start cases plugin correctly', async () => {
|
||||
const pluginStart = plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(pluginStart).toStrictEqual({
|
||||
api: {
|
||||
cases: {
|
||||
bulkGet: expect.any(Function),
|
||||
find: expect.any(Function),
|
||||
getCasesMetrics: expect.any(Function),
|
||||
getCasesStatus: expect.any(Function),
|
||||
},
|
||||
getRelatedCases: expect.any(Function),
|
||||
},
|
||||
helpers: {
|
||||
canUseCases: expect.any(Function),
|
||||
getRuleIdFromEvent: expect.any(Function),
|
||||
getUICapabilities: expect.any(Function),
|
||||
groupAlertsByRule: expect.any(Function),
|
||||
},
|
||||
hooks: {
|
||||
useCasesAddToExistingCaseModal: expect.any(Function),
|
||||
useCasesAddToNewCaseFlyout: expect.any(Function),
|
||||
},
|
||||
ui: {
|
||||
getAllCasesSelectorModal: expect.any(Function),
|
||||
getCases: expect.any(Function),
|
||||
getCasesContext: expect.any(Function),
|
||||
getRecentCases: expect.any(Function),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -75,30 +75,32 @@ export class CasesUiPlugin
|
|||
});
|
||||
}
|
||||
|
||||
plugins.management.sections.section.insightsAndAlerting.registerApp({
|
||||
id: APP_ID,
|
||||
title: APP_TITLE,
|
||||
order: 1,
|
||||
async mount(params: ManagementAppMountParams) {
|
||||
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
|
||||
CoreStart,
|
||||
CasesPluginStart,
|
||||
unknown
|
||||
];
|
||||
if (config.stack.enabled) {
|
||||
plugins.management.sections.section.insightsAndAlerting.registerApp({
|
||||
id: APP_ID,
|
||||
title: APP_TITLE,
|
||||
order: 1,
|
||||
async mount(params: ManagementAppMountParams) {
|
||||
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
|
||||
CoreStart,
|
||||
CasesPluginStart,
|
||||
unknown
|
||||
];
|
||||
|
||||
const { renderApp } = await import('./application');
|
||||
const { renderApp } = await import('./application');
|
||||
|
||||
return renderApp({
|
||||
mountParams: params,
|
||||
coreStart,
|
||||
pluginsStart,
|
||||
storage,
|
||||
kibanaVersion,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
});
|
||||
},
|
||||
});
|
||||
return renderApp({
|
||||
mountParams: params,
|
||||
coreStart,
|
||||
pluginsStart,
|
||||
storage,
|
||||
kibanaVersion,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
attachmentFramework: {
|
||||
|
|
|
@ -106,6 +106,9 @@ describe('config validation', () => {
|
|||
"markdownPlugins": Object {
|
||||
"lens": true,
|
||||
},
|
||||
"stack": Object {
|
||||
"enabled": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -20,6 +20,9 @@ export const ConfigSchema = schema.object({
|
|||
// intentionally not setting a default here so that we can determine if the user set it
|
||||
maxSize: schema.maybe(schema.number({ min: 0 })),
|
||||
}),
|
||||
stack: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
export type ConfigType = TypeOf<typeof ConfigSchema>;
|
||||
|
|
|
@ -16,6 +16,7 @@ export const config: PluginConfigDescriptor<ConfigType> = {
|
|||
exposeToBrowser: {
|
||||
markdownPlugins: true,
|
||||
files: { maxSize: true, allowedMimeTypes: true },
|
||||
stack: { enabled: true },
|
||||
},
|
||||
deprecations: ({ renameFromRoot }) => [
|
||||
renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled', { level: 'critical' }),
|
||||
|
|
123
x-pack/plugins/cases/server/plugin.test.ts
Normal file
123
x-pack/plugins/cases/server/plugin.test.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext } from '@kbn/core/server';
|
||||
import {} from '@kbn/core/server';
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
|
||||
import { createFilesSetupMock } from '@kbn/files-plugin/server/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
import { makeLensEmbeddableFactory } from '@kbn/lens-plugin/server/embeddable/make_lens_embeddable_factory';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { notificationsMock } from '@kbn/notifications-plugin/server/mocks';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import type { PluginsSetup, PluginsStart } from './plugin';
|
||||
import { CasePlugin } from './plugin';
|
||||
import type { ConfigType } from './config';
|
||||
import { ALLOWED_MIME_TYPES } from '../common/constants/mime_types';
|
||||
|
||||
function getConfig(overrides = {}) {
|
||||
return {
|
||||
markdownPlugins: { lens: true },
|
||||
files: { maxSize: 1, allowedMimeTypes: ALLOWED_MIME_TYPES },
|
||||
stack: { enabled: true },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Cases Plugin', () => {
|
||||
let context: PluginInitializerContext;
|
||||
let plugin: CasePlugin;
|
||||
let coreSetup: ReturnType<typeof coreMock.createSetup>;
|
||||
let coreStart: ReturnType<typeof coreMock.createStart>;
|
||||
let pluginsSetup: jest.Mocked<PluginsSetup>;
|
||||
let pluginsStart: jest.Mocked<PluginsStart>;
|
||||
|
||||
beforeEach(() => {
|
||||
context = coreMock.createPluginInitializerContext<ConfigType>(getConfig());
|
||||
|
||||
plugin = new CasePlugin(context);
|
||||
coreSetup = coreMock.createSetup();
|
||||
coreStart = coreMock.createStart();
|
||||
|
||||
pluginsSetup = {
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
actions: actionsMock.createSetup(),
|
||||
files: createFilesSetupMock(),
|
||||
lens: {
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(
|
||||
() => ({}),
|
||||
() => ({}),
|
||||
{}
|
||||
),
|
||||
registerVisualizationMigration: jest.fn(),
|
||||
},
|
||||
security: securityMock.createSetup(),
|
||||
licensing: licensingMock.createSetup(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
features: featuresPluginMock.createSetup(),
|
||||
};
|
||||
|
||||
pluginsStart = {
|
||||
licensing: licensingMock.createStart(),
|
||||
actions: actionsMock.createStart(),
|
||||
files: { fileServiceFactory: { asScoped: jest.fn(), asInternal: jest.fn() } },
|
||||
features: featuresPluginMock.createStart(),
|
||||
security: securityMock.createStart(),
|
||||
notifications: notificationsMock.createStart(),
|
||||
ruleRegistry: { getRacClientWithRequest: jest.fn(), alerting: alertsMock.createStart() },
|
||||
};
|
||||
});
|
||||
|
||||
describe('setup()', () => {
|
||||
it('should start setup cases plugin correctly', async () => {
|
||||
plugin.setup(coreSetup, pluginsSetup);
|
||||
|
||||
expect(context.logger.get().debug).toHaveBeenCalledWith(
|
||||
`Setting up Case Workflow with core contract [${Object.keys(
|
||||
coreSetup
|
||||
)}] and plugins [${Object.keys(pluginsSetup)}]`
|
||||
);
|
||||
});
|
||||
|
||||
it('should register kibana feature when stack is enabled', async () => {
|
||||
plugin.setup(coreSetup, pluginsSetup);
|
||||
|
||||
expect(pluginsSetup.features.registerKibanaFeature).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not register kibana feature when stack is disabled', async () => {
|
||||
context = coreMock.createPluginInitializerContext<ConfigType>(
|
||||
getConfig({ stack: { enabled: false } })
|
||||
);
|
||||
const pluginWithStackDisabled = new CasePlugin(context);
|
||||
|
||||
pluginWithStackDisabled.setup(coreSetup, pluginsSetup);
|
||||
|
||||
expect(pluginsSetup.features.registerKibanaFeature).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should start cases plugin correctly', async () => {
|
||||
const pluginStart = plugin.start(coreStart, pluginsStart);
|
||||
|
||||
expect(context.logger.get().debug).toHaveBeenCalledWith(`Starting Case Workflow`);
|
||||
|
||||
expect(pluginStart).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"getCasesClientWithRequest": [Function],
|
||||
"getExternalReferenceAttachmentTypeRegistry": [Function],
|
||||
"getPersistableStateAttachmentTypeRegistry": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -121,7 +121,9 @@ export class CasePlugin {
|
|||
this.securityPluginSetup = plugins.security;
|
||||
this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory;
|
||||
|
||||
plugins.features.registerKibanaFeature(getCasesKibanaFeature());
|
||||
if (this.caseConfig.stack.enabled) {
|
||||
plugins.features.registerKibanaFeature(getCasesKibanaFeature());
|
||||
}
|
||||
|
||||
core.savedObjects.registerType(
|
||||
createCaseCommentSavedObjectType({
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
"@kbn/core-theme-browser",
|
||||
"@kbn/serverless",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/alerting-plugin",
|
||||
"@kbn/content-management-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import {
|
||||
findCases,
|
||||
createCase,
|
||||
deleteAllCaseItems,
|
||||
postCaseReq,
|
||||
findCasesResp,
|
||||
} from './helpers/api';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('find_cases', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('should return empty response', async () => {
|
||||
const cases = await findCases({ supertest });
|
||||
expect(cases).to.eql(findCasesResp);
|
||||
});
|
||||
|
||||
it('should return cases', async () => {
|
||||
const a = await createCase(supertest, postCaseReq);
|
||||
const b = await createCase(supertest, postCaseReq);
|
||||
const c = await createCase(supertest, postCaseReq);
|
||||
|
||||
const cases = await findCases({ supertest });
|
||||
|
||||
expect(cases).to.eql({
|
||||
...findCasesResp,
|
||||
total: 3,
|
||||
cases: [a, b, c],
|
||||
count_open_cases: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty response when trying to find cases with owner as cases', async () => {
|
||||
const cases = await findCases({ supertest, query: { owner: 'cases' } });
|
||||
expect(cases).to.eql(findCasesResp);
|
||||
});
|
||||
|
||||
it('returns empty response when trying to find cases with owner as securitySolution', async () => {
|
||||
const cases = await findCases({ supertest, query: { owner: 'securitySolution' } });
|
||||
expect(cases).to.eql(findCasesResp);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import {
|
||||
getCase,
|
||||
createCase,
|
||||
deleteCasesByESQuery,
|
||||
getPostCaseRequest,
|
||||
postCaseResp,
|
||||
} from './helpers/api';
|
||||
import { removeServerGeneratedPropertiesFromCase } from './helpers/omit';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('get_case', () => {
|
||||
afterEach(async () => {
|
||||
await deleteCasesByESQuery(es);
|
||||
});
|
||||
|
||||
it('should return a case', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true });
|
||||
|
||||
const data = removeServerGeneratedPropertiesFromCase(theCase);
|
||||
expect(data).to.eql(postCaseResp());
|
||||
expect(data.comments?.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type SuperTest from 'supertest';
|
||||
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server/src/saved_objects_index_pattern';
|
||||
import { CASES_URL } from '@kbn/cases-plugin/common';
|
||||
import { Case, CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain';
|
||||
import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api';
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
|
||||
import { CasesFindResponse } from '@kbn/cases-plugin/common/types/api';
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
description?: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export const superUser: User = {
|
||||
username: 'superuser',
|
||||
password: 'superuser',
|
||||
roles: ['superuser'],
|
||||
};
|
||||
|
||||
export const setupAuth = ({
|
||||
apiCall,
|
||||
headers,
|
||||
auth,
|
||||
}: {
|
||||
apiCall: SuperTest.Test;
|
||||
headers: Record<string, unknown>;
|
||||
auth?: { user: User; space: string | null } | null;
|
||||
}): SuperTest.Test => {
|
||||
if (!Object.hasOwn(headers, 'Cookie') && auth != null) {
|
||||
return apiCall.auth(auth.user.username, auth.user.password);
|
||||
}
|
||||
|
||||
return apiCall;
|
||||
};
|
||||
|
||||
export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => {
|
||||
return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``;
|
||||
};
|
||||
|
||||
export const deleteAllCaseItems = async (es: Client) => {
|
||||
await Promise.all([
|
||||
deleteCasesByESQuery(es),
|
||||
deleteCasesUserActions(es),
|
||||
deleteComments(es),
|
||||
deleteConfiguration(es),
|
||||
deleteMappings(es),
|
||||
]);
|
||||
};
|
||||
|
||||
export const deleteCasesUserActions = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-user-actions',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCasesByESQuery = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteComments = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-comments',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteConfiguration = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-configure',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteMappings = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-connector-mappings',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultUser = { email: null, full_name: null, username: 'elastic' };
|
||||
/**
|
||||
* A null filled user will occur when the security plugin is disabled
|
||||
*/
|
||||
export const nullUser = { email: null, full_name: null, username: null };
|
||||
|
||||
export const postCaseReq: CasePostRequest = {
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Observability Issue',
|
||||
tags: ['defacement'],
|
||||
severity: CaseSeverity.LOW,
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: 'observability',
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a request for creating a case.
|
||||
*/
|
||||
export const getPostCaseRequest = (req?: Partial<CasePostRequest>): CasePostRequest => ({
|
||||
...postCaseReq,
|
||||
...req,
|
||||
});
|
||||
|
||||
export const postCaseResp = (
|
||||
id?: string | null,
|
||||
req: CasePostRequest = postCaseReq
|
||||
): Partial<Case> => ({
|
||||
...req,
|
||||
...(id != null ? { id } : {}),
|
||||
comments: [],
|
||||
duration: null,
|
||||
severity: req.severity ?? CaseSeverity.LOW,
|
||||
totalAlerts: 0,
|
||||
totalComment: 0,
|
||||
closed_by: null,
|
||||
created_by: defaultUser,
|
||||
external_service: null,
|
||||
status: CaseStatuses.open,
|
||||
updated_by: null,
|
||||
category: null,
|
||||
});
|
||||
|
||||
const findCommon = {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
count_open_cases: 0,
|
||||
count_closed_cases: 0,
|
||||
count_in_progress_cases: 0,
|
||||
};
|
||||
|
||||
export const findCasesResp: CasesFindResponse = {
|
||||
...findCommon,
|
||||
cases: [],
|
||||
};
|
||||
|
||||
export const createCase = async (
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>,
|
||||
params: CasePostRequest,
|
||||
expectedHttpCode: number = 200,
|
||||
auth: { user: User; space: string | null } | null = { user: superUser, space: null },
|
||||
headers: Record<string, unknown> = {}
|
||||
): Promise<Case> => {
|
||||
const apiCall = supertest.post(`${CASES_URL}`);
|
||||
|
||||
setupAuth({ apiCall, headers, auth });
|
||||
|
||||
const response = await apiCall
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.set(headers)
|
||||
.send(params)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return response.body;
|
||||
};
|
||||
|
||||
export const findCases = async ({
|
||||
supertest,
|
||||
query = {},
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
query?: Record<string, unknown>;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<CasesFindResponse> => {
|
||||
const { body: res } = await supertest
|
||||
.get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/_find`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.query({ sortOrder: 'asc', ...query })
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.send()
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getCase = async ({
|
||||
supertest,
|
||||
caseId,
|
||||
includeComments = false,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseId: string;
|
||||
includeComments?: boolean;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<Case> => {
|
||||
const { body: theCase } = await supertest
|
||||
.get(
|
||||
`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}?includeComments=${includeComments}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return theCase;
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Case, Attachment } from '@kbn/cases-plugin/common/types/domain';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
interface CommonSavedObjectAttributes {
|
||||
id?: string | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
version?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id'];
|
||||
|
||||
export const removeServerGeneratedPropertiesFromObject = <T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
keys: K[]
|
||||
): Omit<T, K> => {
|
||||
return omit<T, K>(object, keys);
|
||||
};
|
||||
export const removeServerGeneratedPropertiesFromSavedObject = <
|
||||
T extends CommonSavedObjectAttributes
|
||||
>(
|
||||
attributes: T,
|
||||
keys: Array<keyof T> = []
|
||||
): Omit<T, typeof savedObjectCommonAttributes[number] | typeof keys[number]> => {
|
||||
return removeServerGeneratedPropertiesFromObject(attributes, [
|
||||
...savedObjectCommonAttributes,
|
||||
...keys,
|
||||
]);
|
||||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromCase = (theCase: Case): Partial<Case> => {
|
||||
return removeServerGeneratedPropertiesFromSavedObject<Case>(theCase, ['closed_at']);
|
||||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromComments = (
|
||||
comments: Attachment[] | undefined
|
||||
): Array<Partial<Attachment>> | undefined => {
|
||||
return comments?.map((comment) => {
|
||||
return removeServerGeneratedPropertiesFromSavedObject<Attachment>(comment, []);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { deleteCasesByESQuery, createCase, getPostCaseRequest } from './helpers/api';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('post_case', () => {
|
||||
afterEach(async () => {
|
||||
await deleteCasesByESQuery(es);
|
||||
});
|
||||
|
||||
it('should create a case', async () => {
|
||||
expect(
|
||||
await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
}),
|
||||
200
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw 403 when create a case with securitySolution as owner', async () => {
|
||||
expect(
|
||||
await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
owner: 'securitySolution',
|
||||
}),
|
||||
403
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw 403 when create a case with cases as owner', async () => {
|
||||
expect(
|
||||
await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
owner: 'cases',
|
||||
}),
|
||||
403
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -17,5 +17,8 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./threshold_rule/documents_count_fired'));
|
||||
loadTestFile(require.resolve('./threshold_rule/custom_eq_avg_bytes_fired'));
|
||||
loadTestFile(require.resolve('./threshold_rule/group_by_fired'));
|
||||
loadTestFile(require.resolve('./cases/post_case'));
|
||||
loadTestFile(require.resolve('./cases/find_cases'));
|
||||
loadTestFile(require.resolve('./cases/get_case'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { CASES_URL } from '@kbn/cases-plugin/common/constants';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('find_cases', () => {
|
||||
it('403 when calling find cases API', async () => {
|
||||
await supertest
|
||||
.get(`${CASES_URL}/_find`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { CASES_URL } from '@kbn/cases-plugin/common/constants';
|
||||
import { CaseSeverity } from '@kbn/cases-plugin/common/types/domain';
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('post_case', () => {
|
||||
it('403 when trying to create case', async () => {
|
||||
await supertest
|
||||
.post(CASES_URL)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.send({
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Observability Issue',
|
||||
tags: ['defacement'],
|
||||
severity: CaseSeverity.LOW,
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: 'cases',
|
||||
assignees: [],
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -10,5 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('serverless search API', function () {
|
||||
loadTestFile(require.resolve('./snapshot_telemetry'));
|
||||
loadTestFile(require.resolve('./cases/post_case'));
|
||||
loadTestFile(require.resolve('./cases/find_cases'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
import {
|
||||
findCases,
|
||||
createCase,
|
||||
deleteAllCaseItems,
|
||||
findCasesResp,
|
||||
postCaseReq,
|
||||
} from './helpers/api';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('find_cases', () => {
|
||||
describe('basic tests', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllCaseItems(es);
|
||||
});
|
||||
|
||||
it('should return empty response', async () => {
|
||||
const cases = await findCases({ supertest });
|
||||
expect(cases).to.eql(findCasesResp);
|
||||
});
|
||||
|
||||
it('should return cases', async () => {
|
||||
const a = await createCase(supertest, postCaseReq);
|
||||
const b = await createCase(supertest, postCaseReq);
|
||||
const c = await createCase(supertest, postCaseReq);
|
||||
|
||||
const cases = await findCases({ supertest });
|
||||
|
||||
expect(cases).to.eql({
|
||||
...findCasesResp,
|
||||
total: 3,
|
||||
cases: [a, b, c],
|
||||
count_open_cases: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty response when trying to find cases with owner as cases', async () => {
|
||||
const cases = await findCases({ supertest, query: { owner: 'cases' } });
|
||||
expect(cases).to.eql(findCasesResp);
|
||||
});
|
||||
|
||||
it('returns empty response when trying to find cases with owner as observability', async () => {
|
||||
const cases = await findCases({ supertest, query: { owner: 'observability' } });
|
||||
expect(cases).to.eql(findCasesResp);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
import {
|
||||
getCase,
|
||||
createCase,
|
||||
deleteCasesByESQuery,
|
||||
getPostCaseRequest,
|
||||
postCaseResp,
|
||||
} from './helpers/api';
|
||||
import { removeServerGeneratedPropertiesFromCase } from './helpers/omit';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('get_case', () => {
|
||||
afterEach(async () => {
|
||||
await deleteCasesByESQuery(es);
|
||||
});
|
||||
|
||||
it('should return a case', async () => {
|
||||
const postedCase = await createCase(supertest, getPostCaseRequest());
|
||||
const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true });
|
||||
|
||||
const data = removeServerGeneratedPropertiesFromCase(theCase);
|
||||
expect(data).to.eql(postCaseResp());
|
||||
expect(data.comments?.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type SuperTest from 'supertest';
|
||||
import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server/src/saved_objects_index_pattern';
|
||||
import { CASES_URL } from '@kbn/cases-plugin/common';
|
||||
import { Case, CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain';
|
||||
import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api';
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
|
||||
import { CasesFindResponse } from '@kbn/cases-plugin/common/types/api';
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
description?: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export const superUser: User = {
|
||||
username: 'superuser',
|
||||
password: 'superuser',
|
||||
roles: ['superuser'],
|
||||
};
|
||||
|
||||
export const setupAuth = ({
|
||||
apiCall,
|
||||
headers,
|
||||
auth,
|
||||
}: {
|
||||
apiCall: SuperTest.Test;
|
||||
headers: Record<string, unknown>;
|
||||
auth?: { user: User; space: string | null } | null;
|
||||
}): SuperTest.Test => {
|
||||
if (!Object.hasOwn(headers, 'Cookie') && auth != null) {
|
||||
return apiCall.auth(auth.user.username, auth.user.password);
|
||||
}
|
||||
|
||||
return apiCall;
|
||||
};
|
||||
|
||||
export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => {
|
||||
return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``;
|
||||
};
|
||||
|
||||
export const deleteAllCaseItems = async (es: Client) => {
|
||||
await Promise.all([
|
||||
deleteCasesByESQuery(es),
|
||||
deleteCasesUserActions(es),
|
||||
deleteComments(es),
|
||||
deleteConfiguration(es),
|
||||
deleteMappings(es),
|
||||
]);
|
||||
};
|
||||
|
||||
export const deleteCasesUserActions = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-user-actions',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCasesByESQuery = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteComments = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-comments',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteConfiguration = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-configure',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteMappings = async (es: Client): Promise<void> => {
|
||||
await es.deleteByQuery({
|
||||
index: ALERTING_CASES_SAVED_OBJECT_INDEX,
|
||||
q: 'type:cases-connector-mappings',
|
||||
wait_for_completion: true,
|
||||
refresh: true,
|
||||
body: {},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
};
|
||||
|
||||
export const defaultUser = { email: null, full_name: null, username: 'elastic' };
|
||||
/**
|
||||
* A null filled user will occur when the security plugin is disabled
|
||||
*/
|
||||
export const nullUser = { email: null, full_name: null, username: null };
|
||||
|
||||
export const postCaseReq: CasePostRequest = {
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Observability Issue',
|
||||
tags: ['defacement'],
|
||||
severity: CaseSeverity.LOW,
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: 'securitySolution',
|
||||
assignees: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a request for creating a case.
|
||||
*/
|
||||
export const getPostCaseRequest = (req?: Partial<CasePostRequest>): CasePostRequest => ({
|
||||
...postCaseReq,
|
||||
...req,
|
||||
});
|
||||
|
||||
export const postCaseResp = (
|
||||
id?: string | null,
|
||||
req: CasePostRequest = postCaseReq
|
||||
): Partial<Case> => ({
|
||||
...req,
|
||||
...(id != null ? { id } : {}),
|
||||
comments: [],
|
||||
duration: null,
|
||||
severity: req.severity ?? CaseSeverity.LOW,
|
||||
totalAlerts: 0,
|
||||
totalComment: 0,
|
||||
closed_by: null,
|
||||
created_by: defaultUser,
|
||||
external_service: null,
|
||||
status: CaseStatuses.open,
|
||||
updated_by: null,
|
||||
category: null,
|
||||
});
|
||||
|
||||
const findCommon = {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 0,
|
||||
count_open_cases: 0,
|
||||
count_closed_cases: 0,
|
||||
count_in_progress_cases: 0,
|
||||
};
|
||||
|
||||
export const findCasesResp: CasesFindResponse = {
|
||||
...findCommon,
|
||||
cases: [],
|
||||
};
|
||||
|
||||
export const createCase = async (
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>,
|
||||
params: CasePostRequest,
|
||||
expectedHttpCode: number = 200,
|
||||
auth: { user: User; space: string | null } | null = { user: superUser, space: null },
|
||||
headers: Record<string, unknown> = {}
|
||||
): Promise<Case> => {
|
||||
const apiCall = supertest.post(`${CASES_URL}`);
|
||||
|
||||
setupAuth({ apiCall, headers, auth });
|
||||
|
||||
const { body: theCase } = await apiCall
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.set(headers)
|
||||
.send(params)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return theCase;
|
||||
};
|
||||
|
||||
export const findCases = async ({
|
||||
supertest,
|
||||
query = {},
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
query?: Record<string, unknown>;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<CasesFindResponse> => {
|
||||
const { body: res } = await supertest
|
||||
.get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/_find`)
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.query({ sortOrder: 'asc', ...query })
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.send()
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getCase = async ({
|
||||
supertest,
|
||||
caseId,
|
||||
includeComments = false,
|
||||
expectedHttpCode = 200,
|
||||
auth = { user: superUser, space: null },
|
||||
}: {
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>;
|
||||
caseId: string;
|
||||
includeComments?: boolean;
|
||||
expectedHttpCode?: number;
|
||||
auth?: { user: User; space: string | null };
|
||||
}): Promise<Case> => {
|
||||
const { body: theCase } = await supertest
|
||||
.get(
|
||||
`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/${caseId}?includeComments=${includeComments}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo')
|
||||
.auth(auth.user.username, auth.user.password)
|
||||
.expect(expectedHttpCode);
|
||||
|
||||
return theCase;
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Case, Attachment } from '@kbn/cases-plugin/common/types/domain';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
interface CommonSavedObjectAttributes {
|
||||
id?: string | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
version?: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id'];
|
||||
|
||||
export const removeServerGeneratedPropertiesFromObject = <T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
keys: K[]
|
||||
): Omit<T, K> => {
|
||||
return omit<T, K>(object, keys);
|
||||
};
|
||||
export const removeServerGeneratedPropertiesFromSavedObject = <
|
||||
T extends CommonSavedObjectAttributes
|
||||
>(
|
||||
attributes: T,
|
||||
keys: Array<keyof T> = []
|
||||
): Omit<T, typeof savedObjectCommonAttributes[number] | typeof keys[number]> => {
|
||||
return removeServerGeneratedPropertiesFromObject(attributes, [
|
||||
...savedObjectCommonAttributes,
|
||||
...keys,
|
||||
]);
|
||||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromCase = (theCase: Case): Partial<Case> => {
|
||||
return removeServerGeneratedPropertiesFromSavedObject<Case>(theCase, ['closed_at']);
|
||||
};
|
||||
|
||||
export const removeServerGeneratedPropertiesFromComments = (
|
||||
comments: Attachment[] | undefined
|
||||
): Array<Partial<Attachment>> | undefined => {
|
||||
return comments?.map((comment) => {
|
||||
return removeServerGeneratedPropertiesFromSavedObject<Attachment>(comment, []);
|
||||
});
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { deleteCasesByESQuery, createCase, getPostCaseRequest, postCaseResp } from './helpers/api';
|
||||
import { removeServerGeneratedPropertiesFromCase } from './helpers/omit';
|
||||
|
||||
export default ({ getService }: FtrProviderContext): void => {
|
||||
const es = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('post_case', () => {
|
||||
afterEach(async () => {
|
||||
await deleteCasesByESQuery(es);
|
||||
});
|
||||
|
||||
it('should create a case', async () => {
|
||||
const postedCase = await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
})
|
||||
);
|
||||
const data = removeServerGeneratedPropertiesFromCase(postedCase);
|
||||
|
||||
expect(data).to.eql(
|
||||
postCaseResp(
|
||||
null,
|
||||
getPostCaseRequest({
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw 403 when trying to create a case with observability as owner', async () => {
|
||||
expect(
|
||||
await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
owner: 'observability',
|
||||
}),
|
||||
403
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw 403 when trying to create a case with cases as owner', async () => {
|
||||
expect(
|
||||
await createCase(
|
||||
supertest,
|
||||
getPostCaseRequest({
|
||||
owner: 'cases',
|
||||
}),
|
||||
403
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -11,5 +11,8 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('serverless security API', function () {
|
||||
loadTestFile(require.resolve('./fleet'));
|
||||
loadTestFile(require.resolve('./snapshot_telemetry'));
|
||||
loadTestFile(require.resolve('./cases/post_case'));
|
||||
loadTestFile(require.resolve('./cases/find_cases'));
|
||||
loadTestFile(require.resolve('./cases/get_case'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -86,5 +86,22 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
|||
|
||||
await expect(await browser.getCurrentUrl()).contain('/app/discover#/p/log-explorer');
|
||||
});
|
||||
|
||||
it('shows cases in sidebar navigation', async () => {
|
||||
await svlCommonNavigation.expectExists();
|
||||
|
||||
await svlCommonNavigation.sidenav.expectLinkExists({
|
||||
deepLinkId: 'observability-overview:cases',
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to cases app', async () => {
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-overview:cases' });
|
||||
|
||||
await svlCommonNavigation.sidenav.expectLinkActive({
|
||||
deepLinkId: 'observability-overview:cases',
|
||||
});
|
||||
expect(await browser.getCurrentUrl()).contain('/app/observability/cases');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -74,5 +74,18 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
|||
|
||||
await expect(await browser.getCurrentUrl()).contain('/app/discover');
|
||||
});
|
||||
|
||||
it('does not show cases in sidebar navigation', async () => {
|
||||
await svlSearchLandingPage.assertSvlSearchSideNavExists();
|
||||
|
||||
expect(await testSubjects.missingOrFail('cases'));
|
||||
});
|
||||
|
||||
it('does not navigate to cases app', async () => {
|
||||
await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'discover' });
|
||||
|
||||
expect(await browser.getCurrentUrl()).not.contain('/app/management/cases');
|
||||
await testSubjects.missingOrFail('cases-all-title');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,5 +43,19 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
|
|||
|
||||
await expect(await browser.getCurrentUrl()).contain('app/security/dashboards');
|
||||
});
|
||||
|
||||
it('shows cases in sidebar navigation', async () => {
|
||||
await svlSecLandingPage.assertSvlSecSideNavExists();
|
||||
await svlCommonNavigation.expectExists();
|
||||
|
||||
expect(await testSubjects.existOrFail('solutionSideNavItemLink-cases'));
|
||||
});
|
||||
|
||||
it('navigates to cases app', async () => {
|
||||
await testSubjects.click('solutionSideNavItemLink-cases');
|
||||
|
||||
expect(await browser.getCurrentUrl()).contain('/app/security/cases');
|
||||
await testSubjects.existOrFail('cases-all-title');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,5 +46,6 @@
|
|||
"@kbn/test-subj-selector",
|
||||
"@kbn/core-http-common",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/core-saved-objects-server",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue