From 196cabad9bbd0511219fc7833a62cb8a0bb61514 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 23 Oct 2024 00:33:31 -0400 Subject: [PATCH] [ResponseOps][Rules] Remove unintended internal Find routes API with public access (#193757) ## Summary Fixes #192957 Removes the `internal/_find` route from public access by moving the hard-coded `options` into the route builder functions. --------- Co-authored-by: Elastic Machine --- .../plugins/alerting/server/routes/index.ts | 3 +- .../find/find_internal_rules_route.test.ts | 36 + .../apis/find/find_internal_rules_route.ts | 87 ++ .../rule/apis/find/find_rules_route.test.ts | 12 + .../routes/rule/apis/find/find_rules_route.ts | 116 +-- .../rule_management/api/api.test.ts | 17 +- .../rule_management/api/api.ts | 6 +- .../use_fetch_rules_snooze_settings_query.ts | 2 +- .../group1/tests/alerting/find.ts | 230 ++---- .../group1/tests/alerting/find_internal.ts | 762 ++++++++++++++++++ .../group1/tests/alerting/find_with_post.ts | 14 +- .../group1/tests/alerting/index.ts | 1 + .../tests/alerting/group1/create.ts | 13 +- .../spaces_only/tests/alerting/group1/find.ts | 160 ++-- .../tests/alerting/group1/find_internal.ts | 355 ++++++++ .../tests/alerting/group2/update.ts | 9 +- .../pages/alerts/custom_threshold.ts | 5 +- .../apps/observability/pages/rules_page.ts | 7 +- .../rule_actions/snoozing/rule_snoozing.cy.ts | 2 +- 19 files changed, 1450 insertions(+), 387 deletions(-) create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.test.ts create mode 100644 x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_internal.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index 97fdf8c90f8d6..1a274692cefe4 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -20,7 +20,8 @@ import { deleteRuleRoute } from './rule/apis/delete/delete_rule_route'; import { aggregateRulesRoute } from './rule/apis/aggregate/aggregate_rules_route'; import { disableRuleRoute } from './rule/apis/disable/disable_rule_route'; import { enableRuleRoute } from './rule/apis/enable/enable_rule_route'; -import { findRulesRoute, findInternalRulesRoute } from './rule/apis/find/find_rules_route'; +import { findRulesRoute } from './rule/apis/find/find_rules_route'; +import { findInternalRulesRoute } from './rule/apis/find/find_internal_rules_route'; import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; import { getRuleExecutionLogRoute } from './get_rule_execution_log'; import { getGlobalExecutionLogRoute } from './get_global_execution_logs'; diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.test.ts new file mode 100644 index 0000000000000..46ff2e8e96e12 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/server/mocks'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { findInternalRulesRoute } from './find_internal_rules_route'; + +jest.mock('../../../../lib/license_api_access', () => ({ + verifyApiAccess: jest.fn(), +})); + +jest.mock('../../../lib/track_legacy_terminology', () => ({ + trackLegacyTerminology: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('findInternalRulesRoute', () => { + it('registers the route without public access', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + findInternalRulesRoute(router, licenseState); + expect(router.post).toHaveBeenCalledWith( + expect.not.objectContaining({ + options: expect.objectContaining({ access: 'public' }), + }), + expect.any(Function) + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.ts new file mode 100644 index 0000000000000..8ff8ce59192cf --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_internal_rules_route.ts @@ -0,0 +1,87 @@ +/* + * 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 { UsageCounter } from '@kbn/usage-collection-plugin/server'; +import type { + FindRulesRequestQueryV1, + FindRulesResponseV1, +} from '../../../../../common/routes/rule/apis/find'; +import { findRulesRequestQuerySchemaV1 } from '../../../../../common/routes/rule/apis/find'; +import { RuleParamsV1 } from '../../../../../common/routes/rule/response'; +import { ILicenseState } from '../../../../lib'; +import { + AlertingRequestHandlerContext, + INTERNAL_ALERTING_API_FIND_RULES_PATH, +} from '../../../../types'; +import { verifyAccessAndContext } from '../../../lib'; +import { trackLegacyTerminology } from '../../../lib/track_legacy_terminology'; +import { transformFindRulesBodyV1, transformFindRulesResponseV1 } from './transforms'; + +export const findInternalRulesRoute = ( + router: IRouter, + licenseState: ILicenseState, + usageCounter?: UsageCounter +) => { + router.post( + { + path: INTERNAL_ALERTING_API_FIND_RULES_PATH, + options: { access: 'internal' }, + validate: { + body: findRulesRequestQuerySchemaV1, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = (await context.alerting).getRulesClient(); + + const body: FindRulesRequestQueryV1 = req.body; + + trackLegacyTerminology( + [req.body.search, req.body.search_fields, req.body.sort_field].filter( + Boolean + ) as string[], + usageCounter + ); + + const options = transformFindRulesBodyV1({ + ...body, + has_reference: body.has_reference || undefined, + search_fields: searchFieldsAsArray(body.search_fields), + }); + + if (req.body.fields) { + usageCounter?.incrementCounter({ + counterName: `alertingFieldsUsage`, + counterType: 'alertingFieldsUsage', + incrementBy: 1, + }); + } + + const findResult = await rulesClient.find({ + options, + excludeFromPublicApi: false, + includeSnoozeData: true, + }); + + const responseBody: FindRulesResponseV1['body'] = + transformFindRulesResponseV1(findResult, options.fields); + + return res.ok({ + body: responseBody, + }); + }) + ) + ); +}; + +function searchFieldsAsArray(searchFields: string | string[] | undefined): string[] | undefined { + if (!searchFields) { + return; + } + return Array.isArray(searchFields) ? searchFields : [searchFields]; +} diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts index 4e3c9b635ec3a..0e1f07a5ce543 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.test.ts @@ -30,6 +30,18 @@ beforeEach(() => { }); describe('findRulesRoute', () => { + it('registers the route with public access', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + findRulesRoute(router, licenseState); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + options: expect.objectContaining({ access: 'public' }), + }), + expect.any(Function) + ); + }); it('finds rules with proper parameters', async () => { const licenseState = licenseStateMock.create(); const router = httpServiceMock.createRouter(); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts index b4384e9d8f4ef..90afde8f20813 100644 --- a/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/find/find_rules_route.ts @@ -7,40 +7,26 @@ import { IRouter } from '@kbn/core/server'; import { UsageCounter } from '@kbn/usage-collection-plugin/server'; -import { ILicenseState } from '../../../../lib'; -import { verifyAccessAndContext } from '../../../lib'; -import { findRulesRequestQuerySchemaV1 } from '../../../../../common/routes/rule/apis/find'; import type { FindRulesRequestQueryV1, FindRulesResponseV1, } from '../../../../../common/routes/rule/apis/find'; +import { findRulesRequestQuerySchemaV1 } from '../../../../../common/routes/rule/apis/find'; import { RuleParamsV1, ruleResponseSchemaV1 } from '../../../../../common/routes/rule/response'; -import { - AlertingRequestHandlerContext, - BASE_ALERTING_API_PATH, - INTERNAL_ALERTING_API_FIND_RULES_PATH, -} from '../../../../types'; +import { ILicenseState } from '../../../../lib'; +import { AlertingRequestHandlerContext, BASE_ALERTING_API_PATH } from '../../../../types'; +import { verifyAccessAndContext } from '../../../lib'; import { trackLegacyTerminology } from '../../../lib/track_legacy_terminology'; import { transformFindRulesBodyV1, transformFindRulesResponseV1 } from './transforms'; -interface BuildFindRulesRouteParams { - licenseState: ILicenseState; - path: string; - router: IRouter; - excludeFromPublicApi?: boolean; - usageCounter?: UsageCounter; -} - -const buildFindRulesRoute = ({ - licenseState, - path, - router, - excludeFromPublicApi = false, - usageCounter, -}: BuildFindRulesRouteParams) => { +export const findRulesRoute = ( + router: IRouter, + licenseState: ILicenseState, + usageCounter?: UsageCounter +) => { router.get( { - path, + path: `${BASE_ALERTING_API_PATH}/rules/_find`, options: { access: 'public', summary: 'Get information about rules', @@ -91,7 +77,7 @@ const buildFindRulesRoute = ({ const findResult = await rulesClient.find({ options, - excludeFromPublicApi, + excludeFromPublicApi: true, includeSnoozeData: true, }); @@ -104,86 +90,6 @@ const buildFindRulesRoute = ({ }) ) ); - if (path === INTERNAL_ALERTING_API_FIND_RULES_PATH) { - router.post( - { - path, - options: { access: 'internal' }, - validate: { - body: findRulesRequestQuerySchemaV1, - }, - }, - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async function (context, req, res) { - const rulesClient = (await context.alerting).getRulesClient(); - - const body: FindRulesRequestQueryV1 = req.body; - - trackLegacyTerminology( - [req.body.search, req.body.search_fields, req.body.sort_field].filter( - Boolean - ) as string[], - usageCounter - ); - - const options = transformFindRulesBodyV1({ - ...body, - has_reference: body.has_reference || undefined, - search_fields: searchFieldsAsArray(body.search_fields), - }); - - if (req.body.fields) { - usageCounter?.incrementCounter({ - counterName: `alertingFieldsUsage`, - counterType: 'alertingFieldsUsage', - incrementBy: 1, - }); - } - - const findResult = await rulesClient.find({ - options, - excludeFromPublicApi, - includeSnoozeData: true, - }); - - const responseBody: FindRulesResponseV1['body'] = - transformFindRulesResponseV1(findResult, options.fields); - - return res.ok({ - body: responseBody, - }); - }) - ) - ); - } -}; - -export const findRulesRoute = ( - router: IRouter, - licenseState: ILicenseState, - usageCounter?: UsageCounter -) => { - buildFindRulesRoute({ - excludeFromPublicApi: true, - licenseState, - path: `${BASE_ALERTING_API_PATH}/rules/_find`, - router, - usageCounter, - }); -}; - -export const findInternalRulesRoute = ( - router: IRouter, - licenseState: ILicenseState, - usageCounter?: UsageCounter -) => { - buildFindRulesRoute({ - excludeFromPublicApi: false, - licenseState, - path: INTERNAL_ALERTING_API_FIND_RULES_PATH, - router, - usageCounter, - }); }; function searchFieldsAsArray(searchFields: string | string[] | undefined): string[] | undefined { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index d10bb4bb03e08..bc7c288906e8e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -854,9 +854,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - query: expect.objectContaining({ - filter: 'alert.id:"alert:id1" or alert.id:"alert:id2"', - }), + body: '{"filter":"alert.id:\\"alert:id1\\" or alert.id:\\"alert:id2\\"","fields":"[\\"muteAll\\",\\"activeSnoozes\\",\\"isSnoozedUntil\\",\\"snoozeSchedule\\"]","per_page":2}', }) ); }); @@ -867,9 +865,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - query: expect.objectContaining({ - per_page: 2, - }), + body: '{"filter":"alert.id:\\"alert:id1\\" or alert.id:\\"alert:id2\\"","fields":"[\\"muteAll\\",\\"activeSnoozes\\",\\"isSnoozedUntil\\",\\"snoozeSchedule\\"]","per_page":2}', }) ); }); @@ -880,14 +876,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - query: expect.objectContaining({ - fields: JSON.stringify([ - 'muteAll', - 'activeSnoozes', - 'isSnoozedUntil', - 'snoozeSchedule', - ]), - }), + body: '{"filter":"alert.id:\\"alert:id1\\" or alert.id:\\"alert:id2\\"","fields":"[\\"muteAll\\",\\"activeSnoozes\\",\\"isSnoozedUntil\\",\\"snoozeSchedule\\"]","per_page":2}', }) ); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index c86606d0d8137..1e2ee1be7a47a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -241,12 +241,12 @@ export const fetchRulesSnoozeSettings = async ({ const response = await KibanaServices.get().http.fetch( INTERNAL_ALERTING_API_FIND_RULES_PATH, { - method: 'GET', - query: { + method: 'POST', + body: JSON.stringify({ filter: ids.map((x) => `alert.id:"alert:${x}"`).join(' or '), fields: JSON.stringify(['muteAll', 'activeSnoozes', 'isSnoozedUntil', 'snoozeSchedule']), per_page: ids.length, - }, + }), signal, } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts index 8d2ca14d79f9b..a0eedb35d4fa1 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/hooks/use_fetch_rules_snooze_settings_query.ts @@ -13,7 +13,7 @@ import type { RulesSnoozeSettingsMap } from '../../logic'; import { fetchRulesSnoozeSettings } from '../api'; import { DEFAULT_QUERY_OPTIONS } from './constants'; -const FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY = ['GET', INTERNAL_ALERTING_API_FIND_RULES_PATH]; +const FETCH_RULE_SNOOZE_SETTINGS_QUERY_KEY = ['POST', INTERNAL_ALERTING_API_FIND_RULES_PATH]; /** * A wrapper around useQuery provides default values to the underlying query, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts index 37d42ceeccb3a..bf5595e5756c1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find.ts @@ -6,10 +6,8 @@ */ import expect from '@kbn/expect'; -import { Agent as SuperTestAgent } from 'supertest'; import { chunk, omit } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { SupertestWithoutAuthProviderType } from '@kbn/ftr-common-functional-services'; import { ES_QUERY_ID, ML_ANOMALY_DETECTION_RULE_TYPE_ID, @@ -19,13 +17,14 @@ import { SuperuserAtSpace1, UserAtSpaceScenarios, StackAlertsOnly } from '../../ import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -const findTestUtils = ( - describeType: 'internal' | 'public', - objectRemover: ObjectRemover, - supertest: SuperTestAgent, - supertestWithoutAuth: SupertestWithoutAuthProviderType -) => { - describe(describeType, () => { +// eslint-disable-next-line import/no-default-export +export default function createFindTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('find public API', () => { + const objectRemover = new ObjectRemover(supertest); + afterEach(async () => { await objectRemover.removeAll(); }); @@ -71,9 +70,9 @@ const findTestUtils = ( const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix( + space.id + )}/api/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` ) .auth(user.username, user.password); @@ -97,8 +96,6 @@ const findTestUtils = ( 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', @@ -138,14 +135,6 @@ const findTestUtils = ( 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 }), - is_snoozed_until: null, - } - : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); @@ -195,9 +184,9 @@ const findTestUtils = ( const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?per_page=${perPage}&sort_field=createdAt` + `${getUrlPrefix( + space.id + )}/api/alerting/rules/_find?per_page=${perPage}&sort_field=createdAt` ) .auth(user.username, user.password); @@ -244,9 +233,9 @@ const findTestUtils = ( const secondResponse = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?per_page=${perPage}&sort_field=createdAt&page=2` + `${getUrlPrefix( + space.id + )}/api/alerting/rules/_find?per_page=${perPage}&sort_field=createdAt&page=2` ) .auth(user.username, user.password); @@ -291,9 +280,9 @@ const findTestUtils = ( const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` + `${getUrlPrefix( + space.id + )}/api/alerting/rules/_find?filter=alert.attributes.actions:{ actionTypeId: test.noop }` ) .auth(user.username, user.password); @@ -317,8 +306,6 @@ const findTestUtils = ( 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', @@ -352,14 +339,6 @@ const findTestUtils = ( revision: 0, ...(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 }), - is_snoozed_until: null, - } - : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); @@ -401,9 +380,9 @@ const findTestUtils = ( const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags"]&sort_field=createdAt` + `${getUrlPrefix( + space.id + )}/api/alerting/rules/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags"]&sort_field=createdAt` ) .auth(user.username, user.password); @@ -434,19 +413,11 @@ const findTestUtils = ( id: createdAlert.id, actions: [], tags: [myTag], - ...(describeType === 'internal' && { - snooze_schedule: [], - is_snoozed_until: null, - }), }); expect(omit(matchSecond, 'updatedAt')).to.eql({ id: createdSecondAlert.id, actions: [], tags: [myTag], - ...(describeType === 'internal' && { - snooze_schedule: [], - is_snoozed_until: null, - }), }); break; default: @@ -486,9 +457,9 @@ const findTestUtils = ( const response = await supertestWithoutAuth .get( - `${getUrlPrefix(space.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags","executionStatus"]&sort_field=createdAt` + `${getUrlPrefix( + space.id + )}/api/alerting/rules/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags","executionStatus"]&sort_field=createdAt` ) .auth(user.username, user.password); @@ -520,20 +491,12 @@ const findTestUtils = ( actions: [], tags: [myTag], execution_status: matchFirst.execution_status, - ...(describeType === 'internal' && { - snooze_schedule: [], - is_snoozed_until: null, - }), }); expect(omit(matchSecond, 'updatedAt')).to.eql({ id: createdSecondAlert.id, actions: [], tags: [myTag], execution_status: matchSecond.execution_status, - ...(describeType === 'internal' && { - snooze_schedule: [], - is_snoozed_until: null, - }), }); break; default: @@ -551,9 +514,9 @@ const findTestUtils = ( const response = await supertestWithoutAuth .get( - `${getUrlPrefix('other')}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix( + 'other' + )}/api/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` ) .auth(user.username, user.password); @@ -586,88 +549,71 @@ const findTestUtils = ( }); }); } - }); - describe('Actions', () => { - const { user, space } = SuperuserAtSpace1; - - it('should return the actions correctly', 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: createdRule1 } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - enabled: true, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - { - id: 'system-connector-test.system-action', - params: {}, - }, - ], + describe('Actions', () => { + const { user, space } = SuperuserAtSpace1; + + it('should return the actions correctly', 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); - - objectRemover.add(space.id, createdRule1.id, 'rule', 'alerting'); - - const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alerting/rules/_find`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); - - const action = response.body.data[0].actions[0]; - const systemAction = response.body.data[0].actions[1]; - const { uuid, ...restAction } = action; - const { uuid: systemActionUuid, ...restSystemAction } = systemAction; - - expect([restAction, restSystemAction]).to.eql([ - { - id: createdAction.id, - connector_type_id: 'test.noop', - group: 'default', - params: {}, - }, - { - id: 'system-connector-test.system-action', - connector_type_id: 'test.system-action', - params: {}, - }, - , - ]); - }); - }); -}; + .expect(200); -// eslint-disable-next-line import/no-default-export -export default function createFindTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); + const { body: createdRule1 } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: true, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + { + id: 'system-connector-test.system-action', + params: {}, + }, + ], + }) + ) + .expect(200); - describe('find', () => { - const objectRemover = new ObjectRemover(supertest); + objectRemover.add(space.id, createdRule1.id, 'rule', 'alerting'); - afterEach(async () => { - await objectRemover.removeAll(); - }); + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + const action = response.body.data[0].actions[0]; + const systemAction = response.body.data[0].actions[1]; + const { uuid, ...restAction } = action; + const { uuid: systemActionUuid, ...restSystemAction } = systemAction; - findTestUtils('public', objectRemover, supertest, supertestWithoutAuth); - findTestUtils('internal', objectRemover, supertest, supertestWithoutAuth); + expect([restAction, restSystemAction]).to.eql([ + { + id: createdAction.id, + connector_type_id: 'test.noop', + group: 'default', + params: {}, + }, + { + id: 'system-connector-test.system-action', + connector_type_id: 'test.system-action', + params: {}, + }, + , + ]); + }); + }); describe('stack alerts', () => { const ruleTypes = [ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_internal.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_internal.ts new file mode 100644 index 0000000000000..ee8d49c662ada --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/find_internal.ts @@ -0,0 +1,762 @@ +/* + * 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 { chunk, omit } from 'lodash'; +import { v4 as uuidv4 } from 'uuid'; +import { + ES_QUERY_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; +import { SuperuserAtSpace1, UserAtSpaceScenarios, StackAlertsOnly } 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 createFindTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('find internal API', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(async () => { + await objectRemover.removeAll(); + }); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + describe(scenario.id, () => { + it('should handle find alert request 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({ + actions: [ + { + group: 'default', + id: createdAction.id, + params: {}, + frequency: { + summary: false, + notify_when: 'onThrottleInterval', + throttle: '1m', + }, + }, + ], + notify_when: undefined, + throttle: undefined, + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting'); + + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/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: match.running ?? false, + consumer: 'alertsFixture', + schedule: { interval: '1m' }, + enabled: true, + actions: [ + { + group: 'default', + id: createdAction.id, + connector_type_id: 'test.noop', + params: {}, + uuid: match.actions[0].uuid, + frequency: { + summary: false, + notify_when: 'onThrottleInterval', + throttle: '1m', + }, + }, + ], + params: {}, + created_by: 'elastic', + scheduled_task_id: match.scheduled_task_id, + created_at: match.created_at, + updated_at: match.updated_at, + throttle: null, + notify_when: null, + updated_by: 'elastic', + api_key_owner: 'elastic', + api_key_created_by_user: false, + mute_all: false, + muted_alert_ids: [], + revision: 0, + execution_status: match.execution_status, + ...(match.next_run ? { next_run: match.next_run } : {}), + ...(match.last_run ? { last_run: match.last_run } : {}), + + monitoring: match.monitoring, + snooze_schedule: match.snooze_schedule, + ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, + }); + 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)}/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 + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send({ + per_page: perPage, + sort_field: 'createdAt', + page: 2, + }); + + 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)}/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: match.running ?? false, + consumer: 'alertsFixture', + schedule: { interval: '1m' }, + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + connector_type_id: 'test.noop', + params: {}, + uuid: createdAlert.actions[0].uuid, + }, + ], + params: {}, + created_by: 'elastic', + throttle: '1m', + api_key_created_by_user: null, + 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, + revision: 0, + ...(match.next_run ? { next_run: match.next_run } : {}), + ...(match.last_run ? { last_run: match.last_run } : {}), + monitoring: match.monitoring, + snooze_schedule: match.snooze_schedule, + ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, + }); + 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 = uuidv4(); + 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)}/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], + snooze_schedule: [], + is_snoozed_until: null, + }); + expect(omit(matchSecond, 'updatedAt')).to.eql({ + id: createdSecondAlert.id, + actions: [], + tags: [myTag], + snooze_schedule: [], + is_snoozed_until: null, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle find alert request with executionStatus field appropriately', async () => { + const myTag = uuidv4(); + 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)}/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, + snooze_schedule: [], + is_snoozed_until: null, + }); + expect(omit(matchSecond, 'updatedAt')).to.eql({ + id: createdSecondAlert.id, + actions: [], + tags: [myTag], + execution_status: matchSecond.execution_status, + snooze_schedule: [], + is_snoozed_until: null, + }); + 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('other')}/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)}`); + } + }); + }); + } + + describe('Actions', () => { + const { user, space } = SuperuserAtSpace1; + + it('should return the actions correctly', 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: createdRule1 } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + enabled: true, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + { + id: 'system-connector-test.system-action', + params: {}, + }, + ], + }) + ) + .expect(200); + + objectRemover.add(space.id, createdRule1.id, 'rule', 'alerting'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + const action = response.body.data[0].actions[0]; + const systemAction = response.body.data[0].actions[1]; + const { uuid, ...restAction } = action; + const { uuid: systemActionUuid, ...restSystemAction } = systemAction; + + expect([restAction, restSystemAction]).to.eql([ + { + id: createdAction.id, + connector_type_id: 'test.noop', + group: 'default', + params: {}, + }, + { + id: 'system-connector-test.system-action', + connector_type_id: 'test.system-action', + params: {}, + }, + , + ]); + }); + }); + describe('stack alerts', () => { + const ruleTypes = [ + [ + ES_QUERY_ID, + { + searchType: 'esQuery', + timeWindowSize: 5, + timeWindowUnit: 'm', + threshold: [1000], + thresholdComparator: '>', + size: 100, + esQuery: '{\n "query":{\n "match_all" : {}\n }\n }', + aggType: 'count', + groupBy: 'all', + termSize: 5, + excludeHitsFromPreviousRun: false, + sourceFields: [], + index: ['.kibana'], + timeField: 'created_at', + }, + ], + [ + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + { + criteria: [ + { + comparator: '>', + metrics: [ + { + name: 'A', + aggType: 'count', + }, + ], + threshold: [100], + timeSize: 1, + timeUnit: 'm', + }, + ], + alertOnNoData: false, + alertOnGroupDisappear: false, + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: 'kibana-event-log-data-view', + }, + }, + ], + [ + ML_ANOMALY_DETECTION_RULE_TYPE_ID, + { + severity: 75, + resultType: 'bucket', + includeInterim: false, + jobSelection: { + jobIds: ['low_request_rate'], + }, + }, + ], + ]; + + const createRule = async (rule = {}) => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix('space1')}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData({ ...rule })) + .expect(200); + + objectRemover.add('space1', createdAlert.id, 'rule', 'alerting'); + }; + + for (const [ruleTypeId, params] of ruleTypes) { + it(`should get rules of ${ruleTypeId} rule type ID and stackAlerts consumer`, async () => { + /** + * We create two rules. The first one is a test.noop + * rule with stackAlerts as consumer. The second rule + * is has different rule type ID but with the same consumer as the first rule (stackAlerts). + * This way we can verify that the find API call returns only the rules the user is authorized to. + * Specifically only the second rule because the StackAlertsOnly user does not have + * access to the test.noop rule type. + */ + await createRule({ consumer: 'stackAlerts' }); + await createRule({ rule_type_id: ruleTypeId, params, consumer: 'stackAlerts' }); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('space1')}/api/alerting/rules/_find`) + .auth(StackAlertsOnly.username, StackAlertsOnly.password); + + expect(response.statusCode).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].rule_type_id).to.equal(ruleTypeId); + expect(response.body.data[0].consumer).to.equal('stackAlerts'); + }); + } + + for (const [ruleTypeId, params] of ruleTypes) { + it(`should NOT get rules of ${ruleTypeId} rule type ID and NOT stackAlerts consumer`, async () => { + /** + * We create two rules with logs as consumer. The user is authorized to + * access rules only with the stackAlerts consumers. + */ + await createRule({ consumer: 'logs' }); + await createRule({ rule_type_id: ruleTypeId, params, consumer: 'logs' }); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('space1')}/api/alerting/rules/_find`) + .auth(StackAlertsOnly.username, StackAlertsOnly.password); + + expect(response.statusCode).to.eql(200); + expect(response.body.total).to.equal(0); + }); + } + }); + }); +} 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 index f221b5869dd6b..c8afff8fcc476 100644 --- 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 @@ -207,12 +207,14 @@ const findTestUtils = ( 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); + .post(`${getUrlPrefix(space.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'kibana') + .auth(user.username, user.password) + .send({ + per_page: perPage, + sort_field: 'createdAt', + page: 2, + }); expect(secondResponse.body.data.map((alert: any) => alert.id)).to.eql(secondPage); } 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 ec938e8dc4abb..262fe8af301c8 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 @@ -22,6 +22,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./backfill')); loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./find_internal')); loadTestFile(require.resolve('./find_with_post')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts index 9932ee2c11a36..57d41424186b3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts @@ -341,13 +341,14 @@ export default function createAlertTests({ getService }: FtrProviderContext) { ) .expect(200); - const response = await supertest.get( - `${getUrlPrefix( - Spaces.space1.id - )}/internal/alerting/rules/_find?filter=alert.attributes.params.risk_score:40` - ); + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'kibana') + .send({ + filter: `alert.attributes.params.risk_score:40`, + }) + .expect(200); - expect(response.status).to.eql(200); objectRemover.add(Spaces.space1.id, createResponse.body.id, 'rule', 'alerting'); expect(response.body.total).to.equal(1); expect(response.body.data[0].mapped_params).to.eql({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts index c8aeba2b7e210..e730dd657d2f1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts @@ -26,13 +26,15 @@ async function createAlert( return createdAlert; } -const findTestUtils = ( - describeType: 'internal' | 'public', - supertest: SuperTestAgent, - objectRemover: ObjectRemover -) => { - describe(describeType, () => { +// eslint-disable-next-line import/no-default-export +export default function createFindTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('find public API', () => { + const objectRemover = new ObjectRemover(supertest); + afterEach(() => objectRemover.removeAll()); + describe('handle find alert request', function () { this.tags('skipFIPS'); it('should handle find alert request appropriately', async () => { @@ -72,9 +74,9 @@ const findTestUtils = ( objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` ); expect(response.status).to.eql(200); @@ -82,8 +84,6 @@ const findTestUtils = ( 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', @@ -123,14 +123,6 @@ const findTestUtils = ( 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 }), - is_snoozed_until: null, - } - : {}), }); expect(Date.parse(match.created_at)).to.be.greaterThan(0); expect(Date.parse(match.updated_at)).to.be.greaterThan(0); @@ -147,9 +139,9 @@ const findTestUtils = ( await supertest .get( - `${getUrlPrefix(Spaces.other.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` + `${getUrlPrefix( + Spaces.other.id + )}/api/alerting/rules/_find?search=test.noop&search_fields=alertTypeId` ) .expect(200, { page: 1, @@ -186,62 +178,50 @@ const findTestUtils = ( ]); }); - it(`it should${ - describeType === 'public' ? ' NOT' : '' - } allow filter on monitoring attributes`, async () => { + it(`it should NOT allow filter on monitoring attributes`, async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.monitoring.run.calculated_metrics.success_ratio>50` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?filter=alert.attributes.monitoring.run.calculated_metrics.success_ratio>50` ); - expect(response.status).to.eql(describeType === 'internal' ? 200 : 400); - if (describeType === 'public') { - expect(response.body.message).to.eql( - 'Error find rules: Filter is not supported on this field alert.attributes.monitoring.run.calculated_metrics.success_ratio' - ); - } + expect(response.status).to.eql(400); + expect(response.body.message).to.eql( + 'Error find rules: Filter is not supported on this field alert.attributes.monitoring.run.calculated_metrics.success_ratio' + ); }); - it(`it should${ - describeType === 'public' ? ' NOT' : '' - } allow ordering on monitoring attributes`, async () => { + it(`it should NOT allow ordering on monitoring attributes`, async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?sort_field=monitoring.run.calculated_metrics.success_ratio` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?sort_field=monitoring.run.calculated_metrics.success_ratio` ); - expect(response.status).to.eql(describeType === 'internal' ? 200 : 400); - if (describeType === 'public') { - expect(response.body.message).to.eql( - 'Error find rules: Sort is not supported on this field monitoring.run.calculated_metrics.success_ratio' - ); - } + expect(response.status).to.eql(400); + expect(response.body.message).to.eql( + 'Error find rules: Sort is not supported on this field monitoring.run.calculated_metrics.success_ratio' + ); }); - it(`it should${ - describeType === 'public' ? ' NOT' : '' - } allow search_fields on monitoring attributes`, async () => { + it(`it should NOT allow search_fields on monitoring attributes`, async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?search_fields=monitoring.run.calculated_metrics.success_ratio&search=50` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?search_fields=monitoring.run.calculated_metrics.success_ratio&search=50` ); - expect(response.status).to.eql(describeType === 'internal' ? 200 : 400); - if (describeType === 'public') { - expect(response.body.message).to.eql( - 'Error find rules: Search field monitoring.run.calculated_metrics.success_ratio not supported' - ); - } + expect(response.status).to.eql(400); + expect(response.body.message).to.eql( + 'Error find rules: Search field monitoring.run.calculated_metrics.success_ratio not supported' + ); }); it('should filter on string parameters', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.params.strValue:"my b"` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?filter=alert.attributes.params.strValue:"my b"` ); expect(response.status).to.eql(200); @@ -251,9 +231,7 @@ const findTestUtils = ( it('should filter on kueryNode parameters', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=${JSON.stringify( + `${getUrlPrefix(Spaces.space1.id)}/api/alerting/rules/_find?filter=${JSON.stringify( fromKueryExpression('alert.attributes.params.strValue:"my b"') )}` ); @@ -265,9 +243,9 @@ const findTestUtils = ( it('should sort by parameters', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?sort_field=params.severity&sort_order=asc` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?sort_field=params.severity&sort_order=asc` ); expect(response.body.data[0].params.severity).to.equal('low'); expect(response.body.data[1].params.severity).to.equal('medium'); @@ -276,9 +254,9 @@ const findTestUtils = ( it('should search by parameters', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?search_fields=params.severity&search=medium` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?search_fields=params.severity&search=medium` ); expect(response.status).to.eql(200); @@ -288,51 +266,31 @@ const findTestUtils = ( it('should filter on parameters', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.params.risk_score:40` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?filter=alert.attributes.params.risk_score:40` ); expect(response.status).to.eql(200); expect(response.body.total).to.equal(1); expect(response.body.data[0].params.risk_score).to.eql(40); - if (describeType === 'public') { - expect(response.body.data[0].mapped_params).to.eql(undefined); - } + expect(response.body.data[0].mapped_params).to.eql(undefined); }); it('should error if filtering on mapped parameters directly using the public API', async () => { const response = await supertest.get( - `${getUrlPrefix(Spaces.space1.id)}/${ - describeType === 'public' ? 'api' : 'internal' - }/alerting/rules/_find?filter=alert.attributes.mapped_params.risk_score:40` + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerting/rules/_find?filter=alert.attributes.mapped_params.risk_score:40` ); - if (describeType === 'public') { - expect(response.status).to.eql(400); - expect(response.body.message).to.eql( - 'Error find rules: Filter is not supported on this field alert.attributes.mapped_params.risk_score' - ); - } else { - expect(response.status).to.eql(200); - } + expect(response.status).to.eql(400); + expect(response.body.message).to.eql( + 'Error find rules: Filter is not supported on this field alert.attributes.mapped_params.risk_score' + ); }); }); - }); -}; - -// eslint-disable-next-line import/no-default-export -export default function createFindTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - - describe('find', () => { - const objectRemover = new ObjectRemover(supertest); - - afterEach(() => objectRemover.removeAll()); - - findTestUtils('public', supertest, objectRemover); - findTestUtils('internal', supertest, objectRemover); describe('legacy', function () { this.tags('skipFIPS'); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts new file mode 100644 index 0000000000000..25fc54a5229d3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find_internal.ts @@ -0,0 +1,355 @@ +/* + * 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 { Agent as SuperTestAgent } from 'supertest'; +import { fromKueryExpression } from '@kbn/es-query'; +import { Spaces } from '../../../scenarios'; +import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +async function createAlert( + objectRemover: ObjectRemover, + supertest: SuperTestAgent, + overwrites = {} +) { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData(overwrites)) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + return createdAlert; +} + +// eslint-disable-next-line import/no-default-export +export default function createFindTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('find internal API', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + describe('handle find alert request', function () { + this.tags('skipFIPS'); + it('should handle find alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.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(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + actions: [ + { + group: 'default', + id: createdAction.id, + params: {}, + frequency: { + summary: false, + notify_when: 'onThrottleInterval', + throttle: '1m', + }, + }, + ], + notify_when: undefined, + throttle: undefined, + }) + ) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + search: 'test.noop', + search_fields: 'alertTypeId', + }); + + expect(response.status).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', + revision: 0, + running: false, + consumer: 'alertsFixture', + schedule: { interval: '1m' }, + enabled: true, + actions: [ + { + group: 'default', + id: createdAction.id, + connector_type_id: 'test.noop', + params: {}, + frequency: { + summary: false, + notify_when: 'onThrottleInterval', + throttle: '1m', + }, + uuid: match.actions[0].uuid, + }, + ], + params: {}, + created_by: null, + api_key_owner: null, + api_key_created_by_user: null, + scheduled_task_id: match.scheduled_task_id, + updated_by: null, + throttle: null, + notify_when: null, + mute_all: false, + muted_alert_ids: [], + 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 } : {}), + monitoring: match.monitoring, + snooze_schedule: match.snooze_schedule, + ...(hasActiveSnoozes && { active_snoozes: activeSnoozes }), + is_snoozed_until: null, + }); + expect(Date.parse(match.created_at)).to.be.greaterThan(0); + expect(Date.parse(match.updated_at)).to.be.greaterThan(0); + }); + }); + + it(`shouldn't find alert from another space`, async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + await supertest + .post(`${getUrlPrefix(Spaces.other.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + search: 'test.noop', + search_fields: 'alertTypeId', + }) + .expect(200, { + page: 1, + per_page: 10, + total: 0, + data: [], + }); + }); + + describe('basic functionality', () => { + beforeEach(async () => { + await Promise.all([ + createAlert(objectRemover, supertest, { params: { strValue: 'my a' } }), + createAlert(objectRemover, supertest, { params: { strValue: 'my b' } }), + createAlert(objectRemover, supertest, { params: { strValue: 'my c' } }), + createAlert(objectRemover, supertest, { + params: { + risk_score: 60, + severity: 'high', + }, + }), + createAlert(objectRemover, supertest, { + params: { + risk_score: 40, + severity: 'medium', + }, + }), + createAlert(objectRemover, supertest, { + params: { + risk_score: 20, + severity: 'low', + }, + }), + ]); + }); + + it(`it should allow filter on monitoring attributes`, async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + filter: 'alert.attributes.monitoring.run.calculated_metrics.success_ratio>50', + }); + + expect(response.status).to.eql(200); + }); + + it(`it should allow ordering on monitoring attributes`, async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + sort_field: 'monitoring.run.calculated_metrics.success_ratio', + }); + + expect(response.status).to.eql(200); + }); + + it(`it should allow search_fields on monitoring attributes`, async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + search_fields: 'monitoring.run.calculated_metrics.success_ratio', + search: '50', + }); + + expect(response.status).to.eql(200); + }); + + it('should filter on string parameters', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + filter: 'alert.attributes.params.strValue:"my b"', + }); + + expect(response.status).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].params.strValue).to.eql('my b'); + }); + + it('should filter on kueryNode parameters', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + filter: JSON.stringify(fromKueryExpression('alert.attributes.params.strValue:"my b"')), + }); + + expect(response.status).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].params.strValue).to.eql('my b'); + }); + + it('should sort by parameters', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + sort_field: 'params.severity', + sort_order: 'asc', + }); + expect(response.body.data[0].params.severity).to.equal('low'); + expect(response.body.data[1].params.severity).to.equal('medium'); + expect(response.body.data[2].params.severity).to.equal('high'); + }); + + it('should search by parameters', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + search_fields: 'params.severity', + search: 'medium', + }); + + expect(response.status).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].params.severity).to.eql('medium'); + }); + + it('should filter on parameters', async () => { + const response = await supertest + .get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + filter: 'alert.attributes.params.risk_score:40', + }); + + expect(response.status).to.eql(200); + expect(response.body.total).to.equal(1); + expect(response.body.data[0].params.risk_score).to.eql(40); + }); + + it('should error if filtering on mapped parameters directly using the public API', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'foo') + .send({ + filter: 'alert.attributes.mapped_params.risk_score:40', + }); + + expect(response.status).to.eql(200); + }); + }); + + describe('legacy', function () { + this.tags('skipFIPS'); + it('should handle find alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send(getTestRuleData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); + + const response = await supertest.get( + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerts/_find?search=test.noop&search_fields=alertTypeId` + ); + + expect(response.status).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.be.greaterThan(0); + const match = response.body.data.find((obj: any) => obj.id === createdAlert.id); + expect(match).to.eql({ + id: createdAlert.id, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.noop', + consumer: 'alertsFixture', + schedule: { interval: '1m' }, + enabled: true, + actions: [], + params: {}, + createdBy: null, + apiKeyOwner: null, + apiKeyCreatedByUser: null, + scheduledTaskId: match.scheduledTaskId, + updatedBy: null, + throttle: '1m', + notifyWhen: 'onThrottleInterval', + muteAll: false, + mutedInstanceIds: [], + createdAt: match.createdAt, + updatedAt: match.updatedAt, + executionStatus: match.executionStatus, + revision: 0, + running: false, + ...(match.nextRun ? { nextRun: match.nextRun } : {}), + ...(match.lastRun ? { lastRun: match.lastRun } : {}), + }); + expect(Date.parse(match.createdAt)).to.be.greaterThan(0); + expect(Date.parse(match.updatedAt)).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts index 029775fbba383..025fa3b693dce 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts @@ -91,11 +91,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(Date.parse(response.body.next_run)).to.be.greaterThan(0); } - response = await supertest.get( - `${getUrlPrefix( - Spaces.space1.id - )}/internal/alerting/rules/_find?filter=alert.attributes.params.risk_score:40` - ); + response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_find`) + .set('kbn-xsrf', 'kibana') + .send({ filter: `alert.attributes.params.risk_score:40` }); expect(response.body.data[0].mapped_params).to.eql({ risk_score: 40, diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts index 38d308a17e7b0..91cb7eec19ef6 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/custom_threshold.ts @@ -209,7 +209,10 @@ export default ({ getService }: FtrProviderContext) => { }); it('saved the rule correctly', async () => { - const { body: rules } = await supertest.get('/internal/alerting/rules/_find'); + const { body: rules } = await supertest + .post('/internal/alerting/rules/_find') + .set('kbn-xsrf', 'kibana') + .send({}); expect(rules.data.length).toEqual(1); expect(rules.data[0]).toEqual( diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index abc2534efaf8a..4ec0f412e1f03 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -29,7 +29,12 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { const { body: { data: rules }, } = await supertest - .get(`${INTERNAL_RULE_ENDPOINT}/_find?search=${name}&search_fields=name`) + .post(`${INTERNAL_RULE_ENDPOINT}/_find`) + .set('kbn-xsrf', 'kibana') + .send({ + search: name, + search_fields: ['name'], + }) .expect(200); return rules.find((rule: any) => rule.name === name); } diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts index 270a9146f65a9..9622cb94bdd4f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/snoozing/rule_snoozing.cy.ts @@ -217,7 +217,7 @@ describe('rule snoozing', { tags: ['@ess', '@serverless', '@skipInServerlessMKI' describe('Handling errors', () => { it('shows an error if unable to load snooze settings', () => { createRule(getNewRule({ name: 'Test rule', enabled: false })).then(({ body: rule }) => { - cy.intercept('GET', `${INTERNAL_ALERTING_API_FIND_RULES_PATH}*`, { + cy.intercept('POST', `${INTERNAL_ALERTING_API_FIND_RULES_PATH}*`, { statusCode: 500, });