Skip to content

Commit

Permalink
[RAM] HTTP Versioning Aggregate Rules Endpoint (elastic#164284)
Browse files Browse the repository at this point in the history
## Summary
Meta issue: elastic#157883

- Adds HTTP versioning to the aggregate rules endpoint
- Deletes legacy HTTP get aggregate method

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
jcger and kibanamachine authored Sep 18, 2023
1 parent d570545 commit 1093847
Show file tree
Hide file tree
Showing 42 changed files with 608 additions and 1,065 deletions.
1 change: 0 additions & 1 deletion x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export * from './parse_duration';
export * from './execution_log_types';
export * from './rule_snooze_type';
export * from './rrule_type';
export * from './default_rule_aggregation';
export * from './rule_tags_aggregation';
export * from './iso_weekdays';
export * from './saved_objects/rules/mappings';
Expand Down
21 changes: 21 additions & 0 deletions x-pack/plugins/alerting/common/routes/rule/apis/aggregate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.
*/
export {
aggregateRulesRequestBodySchema,
aggregateRulesResponseBodySchema,
} from './schemas/latest';
export type { AggregateRulesRequestBody, AggregateRulesResponseBody } from './types/latest';

export {
aggregateRulesRequestBodySchema as aggregateRulesRequestBodySchemaV1,
aggregateRulesResponseBodySchema as aggregateRulesResponseBodySchemaV1,
} from './schemas/v1';
export type {
AggregateRulesRequestBody as AggregateRulesRequestBodyV1,
AggregateRulesResponseBody as AggregateRulesResponseBodyV1,
AggregateRulesResponse as AggregateRulesResponseV1,
} from './types/v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { schema } from '@kbn/config-schema';

export const aggregateRulesRequestBodySchema = schema.object({
search: schema.maybe(schema.string()),
default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], {
defaultValue: 'OR',
}),
search_fields: schema.maybe(schema.arrayOf(schema.string())),
has_reference: schema.maybe(
// use nullable as maybe is currently broken
// in config-schema
schema.nullable(
schema.object({
type: schema.string(),
id: schema.string(),
})
)
),
filter: schema.maybe(schema.string()),
});

export const aggregateRulesResponseBodySchema = schema.object({
rule_execution_status: schema.recordOf(schema.string(), schema.number()),
rule_last_run_outcome: schema.recordOf(schema.string(), schema.number()),
rule_enabled_status: schema.object({
enabled: schema.number(),
disabled: schema.number(),
}),
rule_muted_status: schema.object({
muted: schema.number(),
unmuted: schema.number(),
}),
rule_snoozed_status: schema.object({
snoozed: schema.number(),
}),
rule_tags: schema.arrayOf(schema.string()),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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.
*/

export * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { aggregateRulesRequestBodySchemaV1, aggregateRulesResponseBodySchemaV1 } from '..';

export type AggregateRulesRequestBody = TypeOf<typeof aggregateRulesRequestBodySchemaV1>;
export type AggregateRulesResponseBody = TypeOf<typeof aggregateRulesResponseBodySchemaV1>;

export interface AggregateRulesResponse {
body: AggregateRulesResponseBody;
}
24 changes: 1 addition & 23 deletions x-pack/plugins/alerting/common/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
SavedObjectAttributes,
SavedObjectsResolveResponse,
} from '@kbn/core/server';
import type { Filter, KueryNode } from '@kbn/es-query';
import type { Filter } from '@kbn/es-query';
import { IsoWeekday } from './iso_weekdays';
import { RuleNotifyWhenType } from './rule_notify_when_type';
import { RuleSnooze } from './rule_snooze_type';
Expand Down Expand Up @@ -118,28 +118,6 @@ export interface RuleAction {
alertsFilter?: AlertsFilter;
}

export interface AggregateOptions {
search?: string;
defaultSearchOperator?: 'AND' | 'OR';
searchFields?: string[];
hasReference?: {
type: string;
id: string;
};
filter?: string | KueryNode;
page?: number;
perPage?: number;
}

export interface RuleAggregationFormattedResult {
ruleExecutionStatus: { [status: string]: number };
ruleLastRunOutcome: { [status: string]: number };
ruleEnabledStatus: { enabled: number; disabled: number };
ruleMutedStatus: { muted: number; unmuted: number };
ruleSnoozedStatus: { snoozed: number };
ruleTags: string[];
}

export interface RuleLastRun {
outcome: RuleLastRunOutcomes;
outcomeOrder?: number;
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/common/rule_tags_aggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
AggregationsCompositeAggregation,
AggregationsAggregateOrder,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { AggregateOptions } from './rule';
import type { AggregateOptions } from '../server/application/rule/methods/aggregate/types';

export type RuleTagsAggregationOptions = Pick<AggregateOptions, 'filter' | 'search'> & {
after?: AggregationsCompositeAggregation['after'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,43 @@
* 2.0.
*/

import { RulesClient, ConstructorOptions } from '../rules_client';
import { RulesClient, ConstructorOptions } from '../../../../rules_client';
import {
savedObjectsClientMock,
loggingSystemMock,
savedObjectsRepositoryMock,
} from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import {
RecoveredActionGroup,
getDefaultRuleAggregation,
DefaultRuleAggregationResult,
} from '../../../common';
import { RegistryRuleType } from '../../rule_type_registry';
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';

import { RegistryRuleType } from '../../../../rule_type_registry';
import { fromKueryExpression, nodeTypes } from '@kbn/es-query';
import { RecoveredActionGroup } from '../../../../../common';
import { DefaultRuleAggregationResult } from '../../../../routes/rule/apis/aggregate/types';
import { defaultRuleAggregationFactory } from '.';

const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();

const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditLoggerMock.create();
const internalSavedObjectsRepository = savedObjectsRepositoryMock.create();

const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
maxScheduledPerMinute: 10000,
minimumScheduleInterval: { value: '1m', enforce: false },
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
Expand All @@ -52,13 +50,14 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
internalSavedObjectsRepository,
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
maxScheduledPerMinute: 1000,
internalSavedObjectsRepository,
};

beforeEach(() => {
Expand Down Expand Up @@ -176,7 +175,7 @@ describe('aggregate()', () => {
const rulesClient = new RulesClient(rulesClientParams);
const result = await rulesClient.aggregate<DefaultRuleAggregationResult>({
options: {},
aggs: getDefaultRuleAggregation(),
aggs: defaultRuleAggregationFactory(),
});

expect(result).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -332,7 +331,7 @@ describe('aggregate()', () => {
const rulesClient = new RulesClient(rulesClientParams);
await rulesClient.aggregate({
options: { filter: 'foo: someTerm' },
aggs: getDefaultRuleAggregation(),
aggs: defaultRuleAggregationFactory(),
});

expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -385,7 +384,9 @@ describe('aggregate()', () => {
const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger });
authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized'));

await expect(rulesClient.aggregate({ aggs: getDefaultRuleAggregation() })).rejects.toThrow();
await expect(
rulesClient.aggregate({ aggs: defaultRuleAggregationFactory() })
).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
Expand All @@ -404,7 +405,7 @@ describe('aggregate()', () => {
test('sets to default (50) if it is not provided', async () => {
const rulesClient = new RulesClient(rulesClientParams);

await rulesClient.aggregate({ aggs: getDefaultRuleAggregation() });
await rulesClient.aggregate({ aggs: defaultRuleAggregationFactory() });

expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([
{
Expand All @@ -421,7 +422,7 @@ describe('aggregate()', () => {
const rulesClient = new RulesClient(rulesClientParams);

await rulesClient.aggregate({
aggs: getDefaultRuleAggregation({ maxTags: 1000 }),
aggs: defaultRuleAggregationFactory({ maxTags: 1000 }),
});

expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 { KueryNode, nodeBuilder } from '@kbn/es-query';
import { findRulesSo } from '../../../../data/rule';
import { AlertingAuthorizationEntity } from '../../../../authorization';
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
import { buildKueryNodeFilter } from '../../../../rules_client/common';
import { alertingAuthorizationFilterOpts } from '../../../../rules_client/common/constants';
import { RulesClientContext } from '../../../../rules_client/types';
import { aggregateOptionsSchema } from './schemas';
import type { AggregateParams } from './types';
import { validateRuleAggregationFields } from './validation';

export async function aggregateRules<T = Record<string, unknown>>(
context: RulesClientContext,
params: AggregateParams<T>
): Promise<T> {
const { options = {}, aggs } = params;
const { filter, page = 1, perPage = 0, ...restOptions } = options;

let authorizationTuple;
try {
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
AlertingAuthorizationEntity.Rule,
alertingAuthorizationFilterOpts
);
validateRuleAggregationFields(aggs);
aggregateOptionsSchema.validate(options);
} catch (error) {
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.AGGREGATE,
error,
})
);
throw error;
}

const { filter: authorizationFilter } = authorizationTuple;
const filterKueryNode = buildKueryNodeFilter(filter);

const { aggregations } = await findRulesSo<T>({
savedObjectsClient: context.unsecuredSavedObjectsClient,
savedObjectsFindOptions: {
...restOptions,
filter:
authorizationFilter && filterKueryNode
? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
: authorizationFilter,
page,
perPage,
aggs,
},
});

return aggregations!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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.
*/
export { defaultRuleAggregationFactory } from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { defaultRuleAggregationFactory } from './v1';

describe('getDefaultRuleAggregation', () => {
it('should return aggregation with default maxTags', () => {
const result = defaultRuleAggregationFactory();
expect(result.tags).toEqual({
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 },
});
});

it('should return aggregation with custom maxTags', () => {
const result = defaultRuleAggregationFactory({ maxTags: 100 });
expect(result.tags).toEqual({
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 100 },
});
});
});
Loading

0 comments on commit 1093847

Please sign in to comment.