Rule find post (#148836)

## Summary

FIX -> https://github.com/elastic/kibana/issues/148287


### 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
This commit is contained in:
Xavier Mouligneau 2023-01-12 18:43:43 -05:00 committed by GitHub
parent e3c8b3cbe4
commit f2342fc3a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 906 additions and 79 deletions

View file

@ -93,4 +93,29 @@ export const aggregateRulesRoute = (
})
)
);
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
validate: {
body: querySchema,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const options = rewriteQueryReq({
...req.body,
has_reference: req.body.has_reference || undefined,
});
trackLegacyTerminology(
[req.body.search, req.body.search_fields].filter(Boolean) as string[],
usageCounter
);
const aggregateResult = await rulesClient.aggregate({ options });
return res.ok({
body: rewriteBodyRes(aggregateResult),
});
})
)
);
};

View file

@ -136,6 +136,51 @@ const buildFindRulesRoute = ({
})
)
);
if (path === `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_find`) {
router.post(
{
path,
validate: {
body: querySchema,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
trackLegacyTerminology(
[req.body.search, req.body.search_fields, req.body.sort_field].filter(
Boolean
) as string[],
usageCounter
);
const options = rewriteQueryReq({
...req.body,
has_reference: req.body.has_reference || undefined,
search_fields: searchFieldsAsArray(req.body.search_fields),
});
if (req.body.fields) {
usageCounter?.incrementCounter({
counterName: `alertingFieldsUsage`,
counterType: 'alertingFieldsUsage',
incrementBy: 1,
});
}
const findResult = await rulesClient.find({
options,
excludeFromPublicApi,
includeSnoozeData: true,
});
return res.ok({
body: rewriteBodyRes(findResult),
});
})
)
);
}
};
export const findRulesRoute = (

View file

@ -23,7 +23,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValueOnce(resolvedValue);
http.post.mockResolvedValueOnce(resolvedValue);
const result = await loadRuleAggregations({ http });
expect(result).toEqual({
@ -35,16 +35,11 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": undefined,
"search": undefined,
"search_fields": undefined,
},
"body": "{\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -60,7 +55,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValueOnce(resolvedValue);
http.post.mockResolvedValueOnce(resolvedValue);
const result = await loadRuleAggregations({ http, searchText: 'apples' });
expect(result).toEqual({
@ -72,16 +67,11 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": undefined,
"search": "apples",
"search_fields": "[\\"name\\",\\"tags\\"]",
},
"body": "{\\"search_fields\\":\\"[\\\\\\"name\\\\\\",\\\\\\"tags\\\\\\"]\\",\\"search\\":\\"apples\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -97,7 +87,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValueOnce(resolvedValue);
http.post.mockResolvedValueOnce(resolvedValue);
const result = await loadRuleAggregations({
http,
@ -113,16 +103,11 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })",
"search": "foo",
"search_fields": "[\\"name\\",\\"tags\\"]",
},
"body": "{\\"search_fields\\":\\"[\\\\\\"name\\\\\\",\\\\\\"tags\\\\\\"]\\",\\"search\\":\\"foo\\",\\"filter\\":\\"(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -138,7 +123,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValueOnce(resolvedValue);
http.post.mockResolvedValueOnce(resolvedValue);
const result = await loadRuleAggregations({
http,
@ -153,16 +138,11 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.alertTypeId:(foo or bar)",
"search": undefined,
"search_fields": undefined,
},
"body": "{\\"filter\\":\\"alert.attributes.alertTypeId:(foo or bar)\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -178,7 +158,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValueOnce(resolvedValue);
http.post.mockResolvedValueOnce(resolvedValue);
const result = await loadRuleAggregations({
http,
@ -195,16 +175,11 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })",
"search": "baz",
"search_fields": "[\\"name\\",\\"tags\\"]",
},
"body": "{\\"search_fields\\":\\"[\\\\\\"name\\\\\\",\\\\\\"tags\\\\\\"]\\",\\"search\\":\\"baz\\",\\"filter\\":\\"alert.attributes.alertTypeId:(foo or bar) and (alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:type })\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -220,7 +195,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValue(resolvedValue);
http.post.mockResolvedValue(resolvedValue);
let result = await loadRuleAggregations({
http,
@ -237,16 +212,11 @@ describe('loadRuleAggregations', () => {
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled: true",
"search": undefined,
"search_fields": undefined,
},
"body": "{\\"filter\\":\\"alert.attributes.enabled: true\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -256,16 +226,11 @@ describe('loadRuleAggregations', () => {
ruleStatusesFilter: ['enabled', 'snoozed'],
});
expect(http.get.mock.calls[1]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[1]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })",
"search": undefined,
"search_fields": undefined,
},
"body": "{\\"filter\\":\\"alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -275,16 +240,11 @@ describe('loadRuleAggregations', () => {
ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'],
});
expect(http.get.mock.calls[1]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[1]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })",
"search": undefined,
"search_fields": undefined,
},
"body": "{\\"filter\\":\\"alert.attributes.enabled: true and (alert.attributes.muteAll:true OR alert.attributes.snoozeSchedule: { duration > 0 })\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);
@ -300,7 +260,7 @@ describe('loadRuleAggregations', () => {
unknown: 0,
},
};
http.get.mockResolvedValueOnce(resolvedValue);
http.post.mockResolvedValueOnce(resolvedValue);
const result = await loadRuleAggregations({
http,
@ -318,16 +278,11 @@ describe('loadRuleAggregations', () => {
},
});
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.tags:(a or b or c)",
"search": "baz",
"search_fields": "[\\"name\\",\\"tags\\"]",
},
"body": "{\\"search_fields\\":\\"[\\\\\\"name\\\\\\",\\\\\\"tags\\\\\\"]\\",\\"search\\":\\"baz\\",\\"filter\\":\\"alert.attributes.tags:(a or b or c)\\",\\"default_search_operator\\":\\"AND\\"}",
},
]
`);

View file

@ -40,15 +40,15 @@ export async function loadRuleAggregations({
ruleStatusesFilter,
tagsFilter,
});
const res = await http.get<AsApiContract<RuleAggregations>>(
const res = await http.post<AsApiContract<RuleAggregations>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
{
query: {
body: JSON.stringify({
search_fields: searchText ? JSON.stringify(['name', 'tags']) : undefined,
search: searchText,
filter: filters.length ? filters.join(' and ') : undefined,
default_search_operator: 'AND',
},
}),
}
);
return rewriteBodyRes(res);

View file

@ -28,13 +28,13 @@ export async function loadRuleAggregationsWithKueryFilter({
searchText,
});
const res = await http.get<AsApiContract<RuleAggregations>>(
const res = await http.post<AsApiContract<RuleAggregations>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
{
query: {
body: JSON.stringify({
...(filtersKueryNode ? { filter: JSON.stringify(filtersKueryNode) } : {}),
default_search_operator: 'AND',
},
}),
}
);
return rewriteBodyRes(res);

View file

@ -38,7 +38,7 @@ export async function loadRulesWithKueryFilter({
searchText,
});
const res = await http.get<
const res = await http.post<
AsApiContract<{
page: number;
perPage: number;
@ -46,14 +46,15 @@ export async function loadRulesWithKueryFilter({
data: Array<AsApiContract<Rule>>;
}>
>(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_find`, {
query: {
body: JSON.stringify({
page: page.index + 1,
per_page: page.size,
...(filtersKueryNode ? { filter: JSON.stringify(filtersKueryNode) } : {}),
sort_field: sort.field,
sort_order: sort.direction,
},
}),
});
return {
page: res.page,
perPage: res.per_page,

View file

@ -0,0 +1,576 @@
/*
* 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 { SuperTest, Test } from 'supertest';
import { chunk, omit } from 'lodash';
import uuid from 'uuid';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
const findTestUtils = (
describeType: 'internal' | 'public',
objectRemover: ObjectRemover,
supertest: SuperTest<Test>,
supertestWithoutAuth: any
) => {
// FLAKY: https://github.com/elastic/kibana/issues/148660
describe.skip(describeType, () => {
afterEach(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle find alert request appropriately', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData())
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
search: 'test.noop',
search_fields: 'alertTypeId',
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: `Unauthorized to find rules for any rule types`,
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const match = response.body.data.find((obj: any) => obj.id === createdAlert.id);
const activeSnoozes = match.active_snoozes;
const hasActiveSnoozes = !!(activeSnoozes || []).filter((obj: any) => obj).length;
expect(match).to.eql({
id: createdAlert.id,
name: 'abc',
tags: ['foo'],
rule_type_id: 'test.noop',
running: false,
consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: true,
actions: [],
params: {},
created_by: 'elastic',
scheduled_task_id: match.scheduled_task_id,
created_at: match.created_at,
updated_at: match.updated_at,
throttle: '1m',
notify_when: 'onThrottleInterval',
updated_by: 'elastic',
api_key_owner: 'elastic',
mute_all: false,
muted_alert_ids: [],
execution_status: match.execution_status,
...(match.next_run ? { next_run: match.next_run } : {}),
...(match.last_run ? { last_run: match.last_run } : {}),
...(describeType === 'internal'
? {
monitoring: match.monitoring,
snooze_schedule: match.snooze_schedule,
...(hasActiveSnoozes && { active_snoozes: activeSnoozes }),
}
: {}),
});
expect(Date.parse(match.created_at)).to.be.greaterThan(0);
expect(Date.parse(match.updated_at)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should filter out types that the user is not authorized to `get` retaining pagination', async () => {
async function createNoOpAlert(overrides = {}) {
const alert = getTestRuleData(overrides);
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(alert)
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
return {
id: createdAlert.id,
rule_type_id: alert.rule_type_id,
};
}
function createRestrictedNoOpAlert() {
return createNoOpAlert({
rule_type_id: 'test.restricted-noop',
consumer: 'alertsRestrictedFixture',
});
}
function createUnrestrictedNoOpAlert() {
return createNoOpAlert({
rule_type_id: 'test.unrestricted-noop',
consumer: 'alertsFixture',
});
}
const allAlerts = [];
allAlerts.push(await createNoOpAlert());
allAlerts.push(await createNoOpAlert());
allAlerts.push(await createRestrictedNoOpAlert());
allAlerts.push(await createUnrestrictedNoOpAlert());
allAlerts.push(await createUnrestrictedNoOpAlert());
allAlerts.push(await createRestrictedNoOpAlert());
allAlerts.push(await createNoOpAlert());
allAlerts.push(await createNoOpAlert());
const perPage = 4;
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
per_page: perPage,
sort_field: 'createdAt',
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: `Unauthorized to find rules for any rule types`,
statusCode: 403,
});
break;
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.per_page).to.be.equal(perPage);
expect(response.body.total).to.be.equal(6);
{
const [firstPage] = chunk(
allAlerts
.filter((alert) => alert.rule_type_id !== 'test.restricted-noop')
.map((alert) => alert.id),
perPage
);
expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage);
}
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.per_page).to.be.equal(perPage);
expect(response.body.total).to.be.equal(8);
{
const [firstPage, secondPage] = chunk(
allAlerts.map((alert) => alert.id),
perPage
);
expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage);
const secondResponse = await supertestWithoutAuth
.get(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find?per_page=${perPage}&sort_field=createdAt&page=2`
)
.auth(user.username, user.password);
expect(secondResponse.body.data.map((alert: any) => alert.id)).to.eql(secondPage);
}
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle find alert request with filter appropriately', async () => {
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: 'test.noop',
config: {},
secrets: {},
})
.expect(200);
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
actions: [
{
id: createdAction.id,
group: 'default',
params: {},
},
],
})
)
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
filter: 'alert.attributes.actions:{ actionTypeId: "test.noop" }',
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: `Unauthorized to find rules for any rule types`,
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const match = response.body.data.find((obj: any) => obj.id === createdAlert.id);
const activeSnoozes = match.active_snoozes;
const hasActiveSnoozes = !!(activeSnoozes || []).filter((obj: any) => obj).length;
expect(match).to.eql({
id: createdAlert.id,
name: 'abc',
tags: ['foo'],
rule_type_id: 'test.noop',
running: false,
consumer: 'alertsFixture',
schedule: { interval: '1m' },
enabled: false,
actions: [
{
id: createdAction.id,
group: 'default',
connector_type_id: 'test.noop',
params: {},
},
],
params: {},
created_by: 'elastic',
throttle: '1m',
updated_by: 'elastic',
api_key_owner: null,
mute_all: false,
muted_alert_ids: [],
notify_when: 'onThrottleInterval',
created_at: match.created_at,
updated_at: match.updated_at,
execution_status: match.execution_status,
...(match.next_run ? { next_run: match.next_run } : {}),
...(match.last_run ? { last_run: match.last_run } : {}),
...(describeType === 'internal'
? {
monitoring: match.monitoring,
snooze_schedule: match.snooze_schedule,
...(hasActiveSnoozes && { active_snoozes: activeSnoozes }),
}
: {}),
});
expect(Date.parse(match.created_at)).to.be.greaterThan(0);
expect(Date.parse(match.updated_at)).to.be.greaterThan(0);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle find alert request with fields appropriately', async () => {
const myTag = uuid.v4();
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
tags: [myTag],
rule_type_id: 'test.restricted-noop',
consumer: 'alertsRestrictedFixture',
})
)
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
// create another type with same tag
const { body: createdSecondAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
tags: [myTag],
rule_type_id: 'test.restricted-noop',
consumer: 'alertsRestrictedFixture',
})
)
.expect(200);
objectRemover.add(space.id, createdSecondAlert.id, 'rule', 'alerting');
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
filter: 'alert.attributes.alertTypeId: "test.restricted - noop"',
fields: ['tags'],
sort_field: 'createdAt',
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: `Unauthorized to find rules for any rule types`,
statusCode: 403,
});
break;
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.data).to.eql([]);
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const [matchFirst, matchSecond] = response.body.data;
expect(omit(matchFirst, 'updatedAt')).to.eql({
id: createdAlert.id,
actions: [],
tags: [myTag],
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
expect(omit(matchSecond, 'updatedAt')).to.eql({
id: createdSecondAlert.id,
actions: [],
tags: [myTag],
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle find alert request with executionStatus field appropriately', async () => {
const myTag = uuid.v4();
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
enabled: false,
tags: [myTag],
rule_type_id: 'test.restricted-noop',
consumer: 'alertsRestrictedFixture',
})
)
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
// create another type with same tag
const { body: createdSecondAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
tags: [myTag],
rule_type_id: 'test.restricted-noop',
consumer: 'alertsRestrictedFixture',
})
)
.expect(200);
objectRemover.add(space.id, createdSecondAlert.id, 'rule', 'alerting');
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
filter: 'alert.attributes.alertTypeId: "test.restricted - noop"',
fields: ['tags', 'executionStatus'],
sort_field: 'createdAt',
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: `Unauthorized to find rules for any rule types`,
statusCode: 403,
});
break;
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.data).to.eql([]);
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(200);
expect(response.body.page).to.equal(1);
expect(response.body.per_page).to.be.greaterThan(0);
expect(response.body.total).to.be.greaterThan(0);
const [matchFirst, matchSecond] = response.body.data;
expect(omit(matchFirst, 'updatedAt')).to.eql({
id: createdAlert.id,
actions: [],
tags: [myTag],
execution_status: matchFirst.execution_status,
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
expect(omit(matchSecond, 'updatedAt')).to.eql({
id: createdSecondAlert.id,
actions: [],
tags: [myTag],
execution_status: matchSecond.execution_status,
...(describeType === 'internal' && {
snooze_schedule: [],
}),
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it(`shouldn't find alert from another space`, async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData())
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/${
describeType === 'public' ? 'api' : 'internal'
}/alerting/rules/_find`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({
search: 'test.noop',
search_fields: 'alertTypeId',
});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: `Unauthorized to find rules for any rule types`,
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
expect(response.statusCode).to.eql(200);
expect(response.body).to.eql({
page: 1,
per_page: 10,
total: 0,
data: [],
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});
};
// eslint-disable-next-line import/no-default-export
export default function createFindTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('find with post', () => {
const objectRemover = new ObjectRemover(supertest);
afterEach(() => objectRemover.removeAll());
findTestUtils('internal', objectRemover, supertest, supertestWithoutAuth);
});
}

View file

@ -21,6 +21,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
});
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./find_with_post'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./disable'));

View file

@ -0,0 +1,223 @@
/*
* 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 { Spaces } from '../../scenarios';
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function createAggregateTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('aggregate post', () => {
const objectRemover = new ObjectRemover(supertest);
afterEach(() => objectRemover.removeAll());
it('should aggregate when there are no alerts', async () => {
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate`)
.set('kbn-xsrf', 'foo')
.send({});
expect(response.status).to.eql(200);
expect(response.body).to.eql({
rule_enabled_status: {
disabled: 0,
enabled: 0,
},
rule_execution_status: {
ok: 0,
active: 0,
error: 0,
pending: 0,
unknown: 0,
warning: 0,
},
rule_last_run_outcome: {
succeeded: 0,
warning: 0,
failed: 0,
},
rule_muted_status: {
muted: 0,
unmuted: 0,
},
rule_snoozed_status: {
snoozed: 0,
},
rule_tags: [],
});
});
it('should aggregate alert status totals', async () => {
const NumOkAlerts = 4;
const NumActiveAlerts = 1;
const NumErrorAlerts = 2;
await Promise.all(
[...Array(NumOkAlerts)].map(async () => {
const okAlertId = await createTestAlert(
{
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
},
'ok'
);
objectRemover.add(Spaces.space1.id, okAlertId, 'rule', 'alerting');
})
);
await Promise.all(
[...Array(NumActiveAlerts)].map(async () => {
const activeAlertId = await createTestAlert(
{
rule_type_id: 'test.patternFiring',
schedule: { interval: '1s' },
params: {
pattern: { instance: new Array(100).fill(true) },
},
},
'active'
);
objectRemover.add(Spaces.space1.id, activeAlertId, 'rule', 'alerting');
})
);
await Promise.all(
[...Array(NumErrorAlerts)].map(async () => {
const activeAlertId = await createTestAlert(
{
rule_type_id: 'test.throw',
schedule: { interval: '1s' },
},
'error'
);
objectRemover.add(Spaces.space1.id, activeAlertId, 'rule', 'alerting');
})
);
// Adding delay to allow ES refresh cycle to run. Even when the waitForStatus
// calls are successful, the call to aggregate may return stale totals if called
// too early.
await delay(1000);
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate`)
.set('kbn-xsrf', 'foo')
.send({});
expect(response.status).to.eql(200);
expect(response.body).to.eql({
rule_enabled_status: {
disabled: 0,
enabled: 7,
},
rule_execution_status: {
ok: NumOkAlerts,
active: NumActiveAlerts,
error: NumErrorAlerts,
pending: 0,
unknown: 0,
warning: 0,
},
rule_last_run_outcome: {
succeeded: 5,
warning: 0,
failed: 2,
},
rule_muted_status: {
muted: 0,
unmuted: 7,
},
rule_snoozed_status: {
snoozed: 0,
},
rule_tags: ['foo'],
});
});
describe('tags limit', () => {
it('should be 50 be default', async () => {
const numOfAlerts = 3;
const numOfTagsPerAlert = 30;
await Promise.all(
[...Array(numOfAlerts)].map(async (_, alertIndex) => {
const okAlertId = await createTestAlert(
{
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
tags: [...Array(numOfTagsPerAlert)].map(
(__, i) => `tag-${i + numOfTagsPerAlert * alertIndex}`
),
},
'ok'
);
objectRemover.add(Spaces.space1.id, okAlertId, 'rule', 'alerting');
})
);
const response = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_aggregate`)
.set('kbn-xsrf', 'foo')
.send({});
expect(response.body.rule_tags.length).to.eql(50);
});
});
});
const WaitForStatusIncrement = 500;
async function waitForStatus(
id: string,
statuses: Set<string>,
waitMillis: number = 10000
): Promise<Record<string, any>> {
if (waitMillis < 0) {
expect().fail(`waiting for alert ${id} statuses ${Array.from(statuses)} timed out`);
}
const response = await supertest.get(
`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule/${id}`
);
expect(response.status).to.eql(200);
const { execution_status: executionStatus } = response.body || {};
const { status } = executionStatus || {};
const message = `waitForStatus(${Array.from(statuses)}): got ${JSON.stringify(
executionStatus
)}`;
if (statuses.has(status)) {
return executionStatus;
}
// eslint-disable-next-line no-console
console.log(`${message}, retrying`);
await delay(WaitForStatusIncrement);
return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement);
}
async function delay(millis: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, millis));
}
async function createTestAlert(testAlertOverrides = {}, status: string) {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(getTestRuleData(testAlertOverrides))
.expect(200);
await waitForStatus(createdAlert.id, new Set([status]));
return createdAlert.id;
}
}

View file

@ -15,6 +15,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
after(async () => await tearDown(getService));
loadTestFile(require.resolve('./aggregate'));
loadTestFile(require.resolve('./aggregate_post'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./disable'));