mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
4213f77cd3
commit
0152724d47
23 changed files with 268 additions and 0 deletions
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -312,6 +312,7 @@ export interface RuleType<
|
|||
*/
|
||||
autoRecoverAlerts?: boolean;
|
||||
getViewInAppRelativeUrl?: GetViewInAppRelativeUrlFn<Params>;
|
||||
fieldsForAAD?: string[];
|
||||
}
|
||||
export type UntypedRuleType = RuleType<
|
||||
RuleTypeParams,
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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(
|
||||
{
|
||||
|
|
|
@ -25,6 +25,7 @@ const createAlertsClientMock = () => {
|
|||
ensureAllAlertsAuthorizedRead: jest.fn(),
|
||||
removeCaseIdFromAlerts: jest.fn(),
|
||||
removeCaseIdsFromAllAlerts: jest.fn(),
|
||||
getAADFields: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('bulkUpdateCases', () => {
|
|||
esClient: esClientMock,
|
||||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
getRuleType: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -163,6 +163,7 @@ export class RuleRegistryPlugin
|
|||
},
|
||||
securityPluginSetup: security,
|
||||
ruleDataService,
|
||||
getRuleType: plugins.alerting.getType,
|
||||
});
|
||||
|
||||
const getRacClientWithRequest = (request: KibanaRequest) => {
|
||||
|
|
|
@ -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' },
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
ruleDataService: ruleDataServiceMock.create(),
|
||||
esClient: esClientMock,
|
||||
getRuleType: jest.fn(),
|
||||
};
|
||||
|
||||
export function getAlertsClientMockInstance(esClient?: ElasticsearchClient) {
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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'));
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue