mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Category fields endpoint (#138245)
* first commit * get auth index and try field caps * use esClient * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * wait for promise to finish * format field capabilities * add simplier browserFields mapper * update response * refactor * types and refactor * remove browser fields dependency * update fn name * update types * update imported type package * update mock object * error message for no o11y alert indices * add endpoint integration test * activate commented tests * add unit test * comment uncommented tests * fix tests * review by Xavier * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * update param names + right type Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
cc5ff75733
commit
f236196ad2
10 changed files with 308 additions and 0 deletions
|
@ -19,6 +19,7 @@ const createAlertsClientMock = () => {
|
|||
bulkUpdate: jest.fn(),
|
||||
find: jest.fn(),
|
||||
getFeatureIdsByRegistrationContexts: jest.fn(),
|
||||
getBrowserFields: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from '@kbn/alerting-plugin/server';
|
||||
import { Logger, ElasticsearchClient, EcsEventOutcome } from '@kbn/core/server';
|
||||
import { AuditLogger } from '@kbn/security-plugin/server';
|
||||
import { IndexPatternsFetcher } from '@kbn/data-plugin/server';
|
||||
import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events';
|
||||
import {
|
||||
ALERT_WORKFLOW_STATUS,
|
||||
|
@ -40,6 +41,8 @@ import {
|
|||
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
|
||||
import { Dataset, IRuleDataService } from '../rule_data_plugin_service';
|
||||
import { getAuthzFilter, getSpacesFilter } from '../lib';
|
||||
import { fieldDescriptorToBrowserFieldMapper } from './browser_fields';
|
||||
import { BrowserFields } from '../types';
|
||||
|
||||
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
|
||||
type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> & {
|
||||
|
@ -716,4 +719,23 @@ export class AlertsClient {
|
|||
throw Boom.failedDependency(errMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async getBrowserFields({
|
||||
indices,
|
||||
metaFields,
|
||||
allowNoIndex,
|
||||
}: {
|
||||
indices: string[];
|
||||
metaFields: string[];
|
||||
allowNoIndex: boolean;
|
||||
}): Promise<BrowserFields> {
|
||||
const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(this.esClient);
|
||||
const { fields } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({
|
||||
pattern: indices,
|
||||
metaFields,
|
||||
fieldCapsOptions: { allow_no_indices: allowNoIndex },
|
||||
});
|
||||
|
||||
return fieldDescriptorToBrowserFieldMapper(fields);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { FieldDescriptor } from '@kbn/data-views-plugin/server';
|
||||
import { BrowserField, BrowserFields } from '../../types';
|
||||
|
||||
const getFieldCategory = (fieldCapability: FieldDescriptor) => {
|
||||
const name = fieldCapability.name.split('.');
|
||||
|
||||
if (name.length === 1) {
|
||||
return 'base';
|
||||
}
|
||||
|
||||
return name[0];
|
||||
};
|
||||
|
||||
const browserFieldFactory = (
|
||||
fieldCapability: FieldDescriptor,
|
||||
category: string
|
||||
): { [fieldName in string]: BrowserField } => {
|
||||
return {
|
||||
[fieldCapability.name]: {
|
||||
...fieldCapability,
|
||||
category,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const fieldDescriptorToBrowserFieldMapper = (fields: FieldDescriptor[]): BrowserFields => {
|
||||
return fields.reduce((browserFields: BrowserFields, field: FieldDescriptor) => {
|
||||
const category = getFieldCategory(field);
|
||||
const browserField = browserFieldFactory(field, category);
|
||||
|
||||
if (browserFields[category]) {
|
||||
browserFields[category] = { fields: { ...browserFields[category].fields, ...browserField } };
|
||||
} else {
|
||||
browserFields[category] = { fields: browserField };
|
||||
}
|
||||
|
||||
return browserFields;
|
||||
}, {});
|
||||
};
|
|
@ -39,3 +39,10 @@ export const getReadFeatureIdsRequest = () =>
|
|||
path: `${BASE_RAC_ALERTS_API_PATH}/_feature_ids`,
|
||||
query: { registrationContext: ['security'] },
|
||||
});
|
||||
|
||||
export const getO11yBrowserFields = () =>
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path: `${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
|
||||
query: { featureIds: ['apm', 'logs'] },
|
||||
});
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id';
|
||||
import { requestContextMock } from './__mocks__/request_context';
|
||||
import { getO11yBrowserFields } from './__mocks__/request_responses';
|
||||
import { requestMock, serverMock } from './__mocks__/server';
|
||||
|
||||
describe('getBrowserFieldsByFeatureId', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
const path = `${BASE_RAC_ALERTS_API_PATH}/browser_fields`;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = serverMock.create();
|
||||
({ clients, context } = requestContextMock.createTools());
|
||||
});
|
||||
|
||||
describe('when racClient returns o11y indices', () => {
|
||||
beforeEach(() => {
|
||||
clients.rac.getAuthorizedAlertsIndices.mockResolvedValue([
|
||||
'.alerts-observability.logs.alerts-default',
|
||||
]);
|
||||
|
||||
getBrowserFieldsByFeatureId(server.router);
|
||||
});
|
||||
|
||||
test('route registered', async () => {
|
||||
const response = await server.inject(getO11yBrowserFields(), context);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
test('rejects invalid featureId type', async () => {
|
||||
await expect(
|
||||
server.inject(
|
||||
requestMock.create({
|
||||
method: 'get',
|
||||
path,
|
||||
query: { featureIds: undefined },
|
||||
}),
|
||||
context
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Request was rejected with message: 'Invalid value \\"undefined\\" supplied to \\"featureIds\\"'"`
|
||||
);
|
||||
});
|
||||
|
||||
test('returns error status if rac client "getAuthorizedAlertsIndices" fails', async () => {
|
||||
clients.rac.getAuthorizedAlertsIndices.mockRejectedValue(new Error('Unable to get index'));
|
||||
const response = await server.inject(getO11yBrowserFields(), context);
|
||||
|
||||
expect(response.status).toEqual(500);
|
||||
expect(response.body).toEqual({
|
||||
attributes: { success: false },
|
||||
message: 'Unable to get index',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { IRouter } from '@kbn/core/server';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { RacRequestHandlerContext } from '../types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
import { buildRouteValidation } from './utils/route_validation';
|
||||
|
||||
export const getBrowserFieldsByFeatureId = (router: IRouter<RacRequestHandlerContext>) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
|
||||
validate: {
|
||||
query: buildRouteValidation(
|
||||
t.exact(
|
||||
t.type({
|
||||
featureIds: t.union([t.string, t.array(t.string)]),
|
||||
})
|
||||
)
|
||||
),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:rac'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const racContext = await context.rac;
|
||||
const alertsClient = await racContext.getAlertsClient();
|
||||
const { featureIds = [] } = request.query;
|
||||
|
||||
const indices = await alertsClient.getAuthorizedAlertsIndices(
|
||||
Array.isArray(featureIds) ? featureIds : [featureIds]
|
||||
);
|
||||
const o11yIndices =
|
||||
indices?.filter((index) => index.startsWith('.alerts-observability')) ?? [];
|
||||
if (o11yIndices.length === 0) {
|
||||
return response.notFound({
|
||||
body: {
|
||||
message: `No alerts-observability indices found for featureIds [${featureIds}]`,
|
||||
attributes: { success: false },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const browserFields = await alertsClient.getBrowserFields({
|
||||
indices: o11yIndices,
|
||||
metaFields: ['_id', '_index'],
|
||||
allowNoIndex: true,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: browserFields,
|
||||
});
|
||||
} catch (error) {
|
||||
const formatedError = transformError(error);
|
||||
const contentType = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
const defaultedHeaders = {
|
||||
...contentType,
|
||||
};
|
||||
|
||||
return response.customError({
|
||||
headers: defaultedHeaders,
|
||||
statusCode: formatedError.statusCode,
|
||||
body: {
|
||||
message: formatedError.message,
|
||||
attributes: {
|
||||
success: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -13,6 +13,7 @@ import { getAlertsIndexRoute } from './get_alert_index';
|
|||
import { bulkUpdateAlertsRoute } from './bulk_update_alerts';
|
||||
import { findAlertsByQueryRoute } from './find';
|
||||
import { getFeatureIdsByRegistrationContexts } from './get_feature_ids_by_registration_contexts';
|
||||
import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id';
|
||||
|
||||
export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
|
||||
getAlertByIdRoute(router);
|
||||
|
@ -21,4 +22,5 @@ export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
|
|||
bulkUpdateAlertsRoute(router);
|
||||
findAlertsByQueryRoute(router);
|
||||
getFeatureIdsByRegistrationContexts(router);
|
||||
getBrowserFieldsByFeatureId(router);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
RuleTypeState,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { RuleExecutorOptions, RuleExecutorServices, RuleType } from '@kbn/alerting-plugin/server';
|
||||
import { FieldSpec } from '@kbn/data-plugin/common';
|
||||
import { AlertsClient } from './alert_data_client/alerts_client';
|
||||
|
||||
type SimpleAlertType<
|
||||
|
@ -71,3 +72,11 @@ export interface RacApiRequestHandlerContext {
|
|||
export type RacRequestHandlerContext = CustomRequestHandlerContext<{
|
||||
rac: RacApiRequestHandlerContext;
|
||||
}>;
|
||||
|
||||
export type BrowserField = FieldSpec & {
|
||||
category: string;
|
||||
};
|
||||
|
||||
export type BrowserFields = {
|
||||
[category in string]: { fields: { [fieldName in string]: BrowserField } };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { superUser, obsOnlySpacesAll, secOnlyRead } from '../../../common/lib/authentication/users';
|
||||
import type { User } from '../../../common/lib/authentication/types';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const SPACE1 = 'space1';
|
||||
const TEST_URL = '/internal/rac/alerts/browser_fields';
|
||||
|
||||
const getBrowserFieldsByFeatureId = async (
|
||||
user: User,
|
||||
featureIds: string[],
|
||||
expectedStatusCode: number = 200
|
||||
) => {
|
||||
const resp = await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`)
|
||||
.query({ featureIds })
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(expectedStatusCode);
|
||||
return resp.body;
|
||||
};
|
||||
|
||||
describe('Alert - Get browser fields by featureId', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
});
|
||||
|
||||
describe('Users:', () => {
|
||||
it(`${obsOnlySpacesAll.username} should be able to get browser fields for o11y featureIds`, async () => {
|
||||
const browserFields = await getBrowserFieldsByFeatureId(obsOnlySpacesAll, [
|
||||
'apm',
|
||||
'infrastructure',
|
||||
'logs',
|
||||
'uptime',
|
||||
]);
|
||||
expect(Object.keys(browserFields)).to.eql(['base']);
|
||||
});
|
||||
|
||||
it(`${superUser.username} should be able to get browser fields for o11y featureIds`, async () => {
|
||||
const browserFields = await getBrowserFieldsByFeatureId(superUser, [
|
||||
'apm',
|
||||
'infrastructure',
|
||||
'logs',
|
||||
'uptime',
|
||||
]);
|
||||
expect(Object.keys(browserFields)).to.eql(['base']);
|
||||
});
|
||||
|
||||
it(`${superUser.username} should NOT be able to get browser fields for siem featureId`, async () => {
|
||||
await getBrowserFieldsByFeatureId(superUser, ['siem'], 404);
|
||||
});
|
||||
|
||||
it(`${secOnlyRead.username} should NOT be able to get browser fields for siem featureId`, async () => {
|
||||
await getBrowserFieldsByFeatureId(secOnlyRead, ['siem'], 404);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -29,5 +29,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
|||
loadTestFile(require.resolve('./get_alerts_index'));
|
||||
loadTestFile(require.resolve('./find_alerts'));
|
||||
loadTestFile(require.resolve('./search_strategy'));
|
||||
loadTestFile(require.resolve('./get_browser_fields_by_feature_id'));
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue