Skip to content

Commit

Permalink
[RAM] Category fields endpoint (#138245)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Xavier Mouligneau <[email protected]>
  • Loading branch information
3 people authored Sep 12, 2022
1 parent cc5ff75 commit f236196
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const createAlertsClientMock = () => {
bulkUpdate: jest.fn(),
find: jest.fn(),
getFeatureIdsByRegistrationContexts: jest.fn(),
getBrowserFields: jest.fn(),
};
return mocked;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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> & {
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}, {});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
});
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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,
},
},
});
}
}
);
};
2 changes: 2 additions & 0 deletions x-pack/plugins/rule_registry/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -21,4 +22,5 @@ export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
bulkUpdateAlertsRoute(router);
findAlertsByQueryRoute(router);
getFeatureIdsByRegistrationContexts(router);
getBrowserFieldsByFeatureId(router);
}
9 changes: 9 additions & 0 deletions x-pack/plugins/rule_registry/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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 } };
};
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
};

0 comments on commit f236196

Please sign in to comment.