[RAM] Add AAD Fields API (#158516)

## Summary

Closes #158324

Adds API to retrieve the `fieldsForAAD` from a given rule type. This is
a list of fields used for Alerts As Data; at the moment we need these
fields to provide an accurate autocomplete list to the Conditional
Actions UI.

### Checklist
- [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

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
Zacqary Adam Xeper 2023-06-13 07:31:33 -05:00 committed by GitHub
parent 4213f77cd3
commit 0152724d47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 268 additions and 0 deletions

View file

@ -57,6 +57,7 @@ const createShareStartMock = () => {
const createStartMock = () => {
const mock: jest.Mocked<PluginStartContract> = {
listTypes: jest.fn(),
getType: jest.fn(),
getAllTypes: jest.fn(),
getAlertingAuthorizationWithRequest: jest.fn(),
getRulesClientWithRequest: jest.fn().mockResolvedValue(rulesClientMock.create()),

View file

@ -143,6 +143,7 @@ export interface PluginStartContract {
listTypes: RuleTypeRegistry['list'];
getAllTypes: RuleTypeRegistry['getAllTypes'];
getType: RuleTypeRegistry['get'];
getRulesClientWithRequest(request: KibanaRequest): RulesClientApi;
@ -550,6 +551,7 @@ export class AlertingPlugin {
return {
listTypes: ruleTypeRegistry!.list.bind(this.ruleTypeRegistry!),
getType: ruleTypeRegistry!.get.bind(this.ruleTypeRegistry),
getAllTypes: ruleTypeRegistry!.getAllTypes.bind(this.ruleTypeRegistry!),
getAlertingAuthorizationWithRequest,
getRulesClientWithRequest,

View file

@ -312,6 +312,7 @@ export interface RuleType<
*/
autoRecoverAlerts?: boolean;
getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn<Params>;
fieldsForAAD?: string[];
}
export type UntypedRuleType = RuleType<
RuleTypeParams,

View file

@ -24,3 +24,12 @@ export const POD_FIELD = 'kubernetes.pod.uid';
export const DISCOVER_APP_TARGET = 'discover';
export const LOGS_APP_TARGET = 'logs-ui';
export const O11Y_AAD_FIELDS = [
'cloud.*',
'host.*',
'orchestrator.*',
'container.*',
'labels.*',
'tags',
];

View file

@ -39,6 +39,7 @@ import {
NO_DATA_ACTIONS,
} from './metric_threshold_executor';
import { MetricsRulesTypeAlertDefinition } from '../register_rule_types';
import { O11Y_AAD_FIELDS } from '../../../../common/constants';
type MetricThresholdAllowedActionGroups = ActionGroupIdsOf<
typeof FIRED_ACTIONS | typeof WARNING_ACTIONS | typeof NO_DATA_ACTIONS
@ -115,6 +116,7 @@ export async function registerMetricThresholdRuleType(
name: i18n.translate('xpack.infra.metrics.alertName', {
defaultMessage: 'Metric threshold',
}),
fieldsForAAD: O11Y_AAD_FIELDS,
validate: {
params: schema.object(
{

View file

@ -25,6 +25,7 @@ const createAlertsClientMock = () => {
ensureAllAlertsAuthorizedRead: jest.fn(),
removeCaseIdFromAlerts: jest.fn(),
removeCaseIdsFromAllAlerts: jest.fn(),
getAADFields: jest.fn(),
};
return mocked;
};

View file

@ -40,6 +40,7 @@ import { Logger, ElasticsearchClient, EcsEvent } from '@kbn/core/server';
import { AuditLogger } from '@kbn/security-plugin/server';
import { IndexPatternsFetcher } from '@kbn/data-plugin/server';
import { isEmpty } from 'lodash';
import { RuleTypeRegistry } from '@kbn/alerting-plugin/server/types';
import { BrowserFields } from '../../common';
import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events';
import {
@ -79,6 +80,7 @@ export interface ConstructorOptions {
auditLogger?: AuditLogger;
esClient: ElasticsearchClient;
ruleDataService: IRuleDataService;
getRuleType: RuleTypeRegistry['get'];
}
export interface UpdateOptions<Params extends RuleTypeParams> {
@ -149,6 +151,7 @@ export class AlertsClient {
private readonly esClient: ElasticsearchClient;
private readonly spaceId: string | undefined;
private readonly ruleDataService: IRuleDataService;
private readonly getRuleType: RuleTypeRegistry['get'];
constructor(options: ConstructorOptions) {
this.logger = options.logger;
@ -159,6 +162,7 @@ export class AlertsClient {
// Otherwise, if space is enabled and not specified, it is "default"
this.spaceId = this.authorization.getSpaceId();
this.ruleDataService = options.ruleDataService;
this.getRuleType = options.getRuleType;
}
private getOutcome(
@ -1091,4 +1095,19 @@ export class AlertsClient {
return fieldDescriptorToBrowserFieldMapper(fields);
}
public async getAADFields({ ruleTypeId }: { ruleTypeId: string }) {
const { producer, fieldsForAAD = [] } = this.getRuleType(ruleTypeId);
const indices = await this.getAuthorizedAlertsIndices([producer]);
const o11yIndices = indices?.filter((index) => index.startsWith('.alerts-observability')) ?? [];
const indexPatternsFetcherAsInternalUser = new IndexPatternsFetcher(this.esClient);
const { fields } = await indexPatternsFetcherAsInternalUser.getFieldsForWildcard({
pattern: o11yIndices,
metaFields: ['_id', '_index'],
fieldCapsOptions: { allow_no_indices: true },
fields: [...fieldsForAAD, 'kibana.*'],
});
return fields;
}
}

View file

@ -26,6 +26,7 @@ const alertsClientFactoryParams: AlertsClientFactoryProps = {
securityPluginSetup,
esClient: {} as ElasticsearchClient,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
const fakeRequest = {
@ -65,6 +66,7 @@ describe('AlertsClientFactory', () => {
auditLogger,
esClient: {},
ruleDataService: alertsClientFactoryParams.ruleDataService,
getRuleType: alertsClientFactoryParams.getRuleType,
});
});

View file

@ -7,6 +7,7 @@
import { PublicMethodsOf } from '@kbn/utility-types';
import { ElasticsearchClient, KibanaRequest, Logger } from '@kbn/core/server';
import type { RuleTypeRegistry } from '@kbn/alerting-plugin/server/types';
import { AlertingAuthorization } from '@kbn/alerting-plugin/server';
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
import { IRuleDataService } from '../rule_data_plugin_service';
@ -18,6 +19,7 @@ export interface AlertsClientFactoryProps {
getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf<AlertingAuthorization>;
securityPluginSetup: SecurityPluginSetup | undefined;
ruleDataService: IRuleDataService | null;
getRuleType: RuleTypeRegistry['get'];
}
export class AlertsClientFactory {
@ -29,6 +31,7 @@ export class AlertsClientFactory {
) => PublicMethodsOf<AlertingAuthorization>;
private securityPluginSetup!: SecurityPluginSetup | undefined;
private ruleDataService!: IRuleDataService | null;
private getRuleType!: RuleTypeRegistry['get'];
public initialize(options: AlertsClientFactoryProps) {
/**
@ -44,6 +47,7 @@ export class AlertsClientFactory {
this.esClient = options.esClient;
this.securityPluginSetup = options.securityPluginSetup;
this.ruleDataService = options.ruleDataService;
this.getRuleType = options.getRuleType;
}
public async create(request: KibanaRequest): Promise<AlertsClient> {
@ -55,6 +59,7 @@ export class AlertsClientFactory {
auditLogger: securityPluginSetup?.audit.asScoped(request),
esClient: this.esClient,
ruleDataService: this.ruleDataService!,
getRuleType: this.getRuleType,
});
}
}

View file

@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
const DEFAULT_SPACE = 'test_default_space_id';

View file

@ -36,6 +36,7 @@ describe('bulkUpdateCases', () => {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
beforeEach(() => {

View file

@ -29,6 +29,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
const DEFAULT_SPACE = 'test_default_space_id';

View file

@ -30,6 +30,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
const DEFAULT_SPACE = 'test_default_space_id';

View file

@ -31,6 +31,7 @@ describe('remove cases from alerts', () => {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
beforeEach(() => {
@ -87,6 +88,7 @@ describe('remove cases from alerts', () => {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
beforeEach(() => {

View file

@ -29,6 +29,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
esClient: esClientMock,
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
getRuleType: jest.fn(),
};
const DEFAULT_SPACE = 'test_default_space_id';

View file

@ -163,6 +163,7 @@ export class RuleRegistryPlugin
},
securityPluginSetup: security,
ruleDataService,
getRuleType: plugins.alerting.getType,
});
const getRacClientWithRequest = (request: KibanaRequest) => {

View file

@ -46,3 +46,10 @@ export const getO11yBrowserFields = () =>
path: `${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
query: { featureIds: ['apm', 'logs'] },
});
export const getMetricThresholdAADFields = () =>
requestMock.create({
method: 'get',
path: `${BASE_RAC_ALERTS_API_PATH}/aad_fields`,
query: { ruleTypeId: 'metrics.alert.threshold' },
});

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getAADFieldsByRuleType } from './get_aad_fields_by_rule_type';
import { requestContextMock } from './__mocks__/request_context';
import { getMetricThresholdAADFields } from './__mocks__/request_responses';
import { serverMock } from './__mocks__/server';
describe('getAADFieldsByRuleType', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
beforeEach(async () => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
});
describe('when racClient returns o11y indices', () => {
beforeEach(() => {
clients.rac.getAADFields.mockResolvedValue([
{
name: '_id',
type: 'string',
searchable: false,
aggregatable: false,
readFromDocValues: false,
metadata_field: true,
esTypes: [],
},
]);
getAADFieldsByRuleType(server.router);
});
test('route registered', async () => {
const response = await server.inject(getMetricThresholdAADFields(), context);
expect(response.status).toEqual(200);
});
test('returns error status if rac client "getAADFields" fails', async () => {
clients.rac.getAADFields.mockRejectedValue(new Error('Rule type not registered'));
const response = await server.inject(getMetricThresholdAADFields(), context);
expect(response.status).toEqual(500);
expect(response.body).toEqual({
attributes: { success: false },
message: 'Rule type not registered',
});
});
});
});

View file

@ -0,0 +1,66 @@
/*
* 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 getAADFieldsByRuleType = (router: IRouter<RacRequestHandlerContext>) => {
router.get(
{
path: `${BASE_RAC_ALERTS_API_PATH}/aad_fields`,
validate: {
query: buildRouteValidation(
t.exact(
t.type({
ruleTypeId: t.string,
})
)
),
},
options: {
tags: ['access:rac'],
},
},
async (context, request, response) => {
try {
const racContext = await context.rac;
const alertsClient = await racContext.getAlertsClient();
const { ruleTypeId } = request.query;
const aadFields = await alertsClient.getAADFields({ ruleTypeId });
return response.ok({
body: aadFields,
});
} 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,
},
},
});
}
}
);
};

View file

@ -15,6 +15,7 @@ import { findAlertsByQueryRoute } from './find';
import { getFeatureIdsByRegistrationContexts } from './get_feature_ids_by_registration_contexts';
import { getBrowserFieldsByFeatureId } from './get_browser_fields_by_feature_id';
import { getAlertSummaryRoute } from './get_alert_summary';
import { getAADFieldsByRuleType } from './get_aad_fields_by_rule_type';
export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
getAlertByIdRoute(router);
@ -25,4 +26,5 @@ export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
getFeatureIdsByRegistrationContexts(router);
getBrowserFieldsByFeatureId(router);
getAlertSummaryRoute(router);
getAADFieldsByRuleType(router);
}

View file

@ -63,6 +63,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
ruleDataService: ruleDataServiceMock.create(),
esClient: esClientMock,
getRuleType: jest.fn(),
};
export function getAlertsClientMockInstance(esClient?: ElasticsearchClient) {

View file

@ -0,0 +1,85 @@
/*
* 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 { obsOnlySpacesAll } 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 retry = getService('retry');
const SPACE1 = 'space1';
const TEST_URL = '/internal/rac/alerts/aad_fields';
const getAADFieldsByRuleType = async (
user: User,
ruleTypeId: string,
expectedStatusCode: number = 200
) => {
const resp = await supertestWithoutAuth
.get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}`)
.query({ ruleTypeId })
.auth(user.username, user.password)
.set('kbn-xsrf', 'true')
.expect(expectedStatusCode);
return resp.body;
};
describe('Alert - Get AAD fields by ruleType', () => {
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 () => {
await retry.try(async () => {
const aadFields = await getAADFieldsByRuleType(
obsOnlySpacesAll,
'metrics.alert.threshold'
);
expect(aadFields.slice(0, 2)).to.eql(expectedResult);
expect(aadFields.length > 2).to.be(true);
for (const field of aadFields) {
expectToBeFieldDescriptor(field);
}
});
});
});
});
};
const expectedResult = [
{
name: '_id',
type: 'string',
searchable: false,
aggregatable: false,
readFromDocValues: false,
metadata_field: true,
},
{
name: '_index',
type: 'string',
searchable: false,
aggregatable: false,
readFromDocValues: false,
metadata_field: true,
},
];
const expectToBeFieldDescriptor = (field: Record<string, unknown>) => {
expect('name' in field).to.be(true);
expect('type' in field).to.be(true);
expect('searchable' in field).to.be(true);
expect('aggregatable' in field).to.be(true);
expect('readFromDocValues' in field).to.be(true);
expect('metadata_field' in field).to.be(true);
};

View file

@ -31,5 +31,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
loadTestFile(require.resolve('./search_strategy'));
loadTestFile(require.resolve('./get_browser_fields_by_feature_id'));
loadTestFile(require.resolve('./get_alert_summary'));
loadTestFile(require.resolve('./get_aad_fields_by_rule_type'));
});
};