From dfee7e927b78d8e548ba9da98c5b610ce31db758 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau Date: Thu, 12 Jan 2023 18:43:43 -0500 Subject: [PATCH] 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 --- .../alerting/server/routes/aggregate_rules.ts | 25 + .../alerting/server/routes/find_rules.ts | 45 ++ .../lib/rule_api/aggregate.test.ts | 95 +-- .../application/lib/rule_api/aggregate.ts | 6 +- .../lib/rule_api/aggregate_kuery_filter.ts | 6 +- .../lib/rule_api/rules_kuery_filter.ts | 7 +- .../group1/tests/alerting/find_with_post.ts | 576 ++++++++++++++++++ .../group1/tests/alerting/index.ts | 1 + .../tests/alerting/aggregate_post.ts | 223 +++++++ .../spaces_only/tests/alerting/index.ts | 1 + 10 files changed, 906 insertions(+), 79 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts index acab5ca75d2e0..b927464b67e38 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -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), + }); + }) + ) + ); }; diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts index d653f50834763..50bd9ad387d7d 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -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 = ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index 89df179d55eea..8d14720443ae3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -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\\"}", }, ] `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 980571b920036..605aa96552583 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -40,15 +40,15 @@ export async function loadRuleAggregations({ ruleStatusesFilter, tagsFilter, }); - const res = await http.get>( + const res = await http.post>( `${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); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts index 28adf597af7ac..958ccd79e95ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate_kuery_filter.ts @@ -28,13 +28,13 @@ export async function loadRuleAggregationsWithKueryFilter({ searchText, }); - const res = await http.get>( + const res = await http.post>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { - query: { + body: JSON.stringify({ ...(filtersKueryNode ? { filter: JSON.stringify(filtersKueryNode) } : {}), default_search_operator: 'AND', - }, + }), } ); return rewriteBodyRes(res); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts index 46d2c32ccdbdd..d7c56388b4e9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules_kuery_filter.ts @@ -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>; }> >(`${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, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts new file mode 100644 index 0000000000000..e3ee01c3271ec --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_with_post.ts @@ -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, + 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); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts index 86b249dc6bc79..24679b228c379 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts @@ -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')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts new file mode 100644 index 0000000000000..05d7701a23e54 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate_post.ts @@ -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, + waitMillis: number = 10000 + ): Promise> { + 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 { + 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; + } +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index c887a10aa14aa..9f3faf1d3a5fa 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -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'));