diff --git a/packages/kbn-check-saved-objects-cli/current_fields.json b/packages/kbn-check-saved-objects-cli/current_fields.json index ba6c5df03a7bb..de08dfcf97e7d 100644 --- a/packages/kbn-check-saved-objects-cli/current_fields.json +++ b/packages/kbn-check-saved-objects-cli/current_fields.json @@ -89,6 +89,15 @@ "updatedAt", "updatedBy" ], + "alerting_notification_policy": [ + "createdAt", + "createdBy", + "description", + "name", + "updatedAt", + "updatedBy", + "workflow_id" + ], "alerting_rule": [ "createdAt", "createdBy", diff --git a/packages/kbn-check-saved-objects-cli/current_mappings.json b/packages/kbn-check-saved-objects-cli/current_mappings.json index 158a84cbd65b8..016c5d97dba1e 100644 --- a/packages/kbn-check-saved-objects-cli/current_mappings.json +++ b/packages/kbn-check-saved-objects-cli/current_mappings.json @@ -298,6 +298,34 @@ } } }, + "alerting_notification_policy": { + "dynamic": false, + "properties": { + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } + }, + "description": { + "type": "text", + "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } + }, + "updatedAt": { + "type": "date" + }, + "updatedBy": { + "type": "keyword" + }, + "workflow_id": { + "type": "keyword" + } + } + }, "alerting_rule": { "dynamic": false, "properties": { diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts new file mode 100644 index 0000000000000..6da879791cf5a --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { NotificationPolicyClient } from './notification_policy_client'; +export type { + CreateNotificationPolicyData, + CreateNotificationPolicyParams, + NotificationPolicyResponse, + UpdateNotificationPolicyData, +} from './types'; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts new file mode 100644 index 0000000000000..dff37acd6d04d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts @@ -0,0 +1,380 @@ +/* + * 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 { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; + +import type { UserProfileServiceStart } from '@kbn/core-user-profile-server'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + type NotificationPolicySavedObjectAttributes, +} from '../../saved_objects'; +import type { NotificationPolicySavedObjectService } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service'; +import { createNotificationPolicySavedObjectService } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service.mock'; +import type { UserService } from '../services/user_service/user_service'; +import { createUserProfile, createUserService } from '../services/user_service/user_service.mock'; +import { NotificationPolicyClient } from './notification_policy_client'; + +describe('NotificationPolicyClient', () => { + let client: NotificationPolicyClient; + let notificationPolicySavedObjectService: NotificationPolicySavedObjectService; + let mockSavedObjectsClient: jest.Mocked; + let userService: UserService; + let userProfile: jest.Mocked; + + beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00.000Z')); + }); + + beforeEach(() => { + jest.clearAllMocks(); + + ({ notificationPolicySavedObjectService, mockSavedObjectsClient } = + createNotificationPolicySavedObjectService()); + ({ userService, userProfile } = createUserService()); + + client = new NotificationPolicyClient(notificationPolicySavedObjectService, userService); + + userProfile.getCurrent.mockResolvedValue(createUserProfile('elastic_profile_uid')); + + mockSavedObjectsClient.create.mockResolvedValue({ + id: 'policy-id-default', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + version: 'WzEsMV0=', + }); + mockSavedObjectsClient.update.mockResolvedValue({ + id: 'policy-id-default', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + version: 'WzEsMV0=', + }); + mockSavedObjectsClient.delete.mockResolvedValue({}); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + describe('createNotificationPolicy', () => { + it('creates a notification policy with correct attributes', async () => { + mockSavedObjectsClient.create.mockResolvedValueOnce({ + id: 'policy-id-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + version: 'WzEsMV0=', + }); + + const res = await client.createNotificationPolicy({ + data: { + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow', + }, + options: { id: 'policy-id-1' }, + }); + + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + expect.objectContaining({ + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow', + createdBy: 'elastic_profile_uid', + updatedBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedAt: '2025-01-01T00:00:00.000Z', + }), + { id: 'policy-id-1', overwrite: false } + ); + + expect(res).toEqual( + expect.objectContaining({ + id: 'policy-id-1', + version: 'WzEsMV0=', + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow', + createdBy: 'elastic_profile_uid', + updatedBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedAt: '2025-01-01T00:00:00.000Z', + }) + ); + }); + + it('creates a notification policy without custom id', async () => { + mockSavedObjectsClient.create.mockImplementationOnce(async (_type, _attrs, options) => { + return { + id: (options?.id ?? 'auto-generated-id') as string, + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + version: 'WzEsMV0=', + }; + }); + + const res = await client.createNotificationPolicy({ + data: { + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow', + }, + }); + + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + expect.objectContaining({ + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow', + }), + expect.objectContaining({ + overwrite: false, + id: expect.any(String), + }) + ); + + expect(res.id).toEqual(expect.any(String)); + expect(res.name).toBe('my-policy'); + expect(res.description).toBe('my-policy description'); + expect(res.workflow_id).toBe('my-workflow'); + }); + + it('throws 409 conflict when id already exists', async () => { + mockSavedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createConflictError( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-conflict' + ) + ); + + await expect( + client.createNotificationPolicy({ + data: { + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow', + }, + options: { id: 'policy-id-conflict' }, + }) + ).rejects.toMatchObject({ + output: { statusCode: 409 }, + }); + }); + }); + + describe('getNotificationPolicy', () => { + it('returns a notification policy by id', async () => { + const existingAttributes: NotificationPolicySavedObjectAttributes = { + name: 'test-policy', + description: 'test-policy description', + workflow_id: 'test-workflow', + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + mockSavedObjectsClient.get.mockResolvedValueOnce({ + id: 'policy-id-get-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: existingAttributes, + references: [], + version: 'WzEsMV0=', + }); + + const res = await client.getNotificationPolicy({ id: 'policy-id-get-1' }); + + expect(mockSavedObjectsClient.get).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-get-1', + undefined + ); + expect(res).toEqual({ + id: 'policy-id-get-1', + version: 'WzEsMV0=', + ...existingAttributes, + }); + }); + + it('throws 404 when notification policy is not found', async () => { + mockSavedObjectsClient.get.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createGenericNotFoundError( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-get-404' + ) + ); + + await expect(client.getNotificationPolicy({ id: 'policy-id-get-404' })).rejects.toMatchObject( + { + output: { statusCode: 404 }, + } + ); + }); + }); + + describe('updateNotificationPolicy', () => { + it('updates a notification policy successfully', async () => { + const existingAttributes: NotificationPolicySavedObjectAttributes = { + name: 'original-policy', + description: 'original-policy description', + workflow_id: 'original-workflow', + createdBy: 'creator_profile_uid', + createdAt: '2024-12-01T00:00:00.000Z', + updatedBy: 'updater_profile_uid', + updatedAt: '2024-12-01T00:00:00.000Z', + }; + mockSavedObjectsClient.get.mockResolvedValueOnce({ + id: 'policy-id-update-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + references: [], + version: 'WzEsMV0=', + attributes: existingAttributes, + }); + mockSavedObjectsClient.update.mockResolvedValueOnce({ + id: 'policy-id-update-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + version: 'WzIsMV0=', + }); + + const res = await client.updateNotificationPolicy({ + data: { name: 'updated-policy', workflow_id: 'updated-workflow' }, + options: { id: 'policy-id-update-1', version: 'WzEsMV0=' }, + }); + + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-update-1', + expect.objectContaining({ + name: 'updated-policy', + description: 'original-policy description', + workflow_id: 'updated-workflow', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + // Preserves original createdBy and createdAt + createdBy: 'creator_profile_uid', + createdAt: '2024-12-01T00:00:00.000Z', + }), + { version: 'WzEsMV0=' } + ); + + expect(res).toEqual( + expect.objectContaining({ + id: 'policy-id-update-1', + version: 'WzIsMV0=', + name: 'updated-policy', + description: 'original-policy description', + workflow_id: 'updated-workflow', + updatedAt: '2025-01-01T00:00:00.000Z', + }) + ); + }); + + it('throws 404 when notification policy is not found', async () => { + mockSavedObjectsClient.get.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createGenericNotFoundError( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-update-404' + ) + ); + + await expect( + client.updateNotificationPolicy({ + data: { workflow_id: 'some-workflow' }, + options: { id: 'policy-id-update-404', version: 'WzEsMV0=' }, + }) + ).rejects.toMatchObject({ + output: { statusCode: 404 }, + }); + }); + + it('throws 409 conflict when version is stale', async () => { + const existingAttributes: NotificationPolicySavedObjectAttributes = { + name: 'original-policy', + description: 'original-policy description', + workflow_id: 'original-workflow', + createdBy: 'creator_profile_uid', + createdAt: '2024-12-01T00:00:00.000Z', + updatedBy: 'updater_profile_uid', + updatedAt: '2024-12-01T00:00:00.000Z', + }; + mockSavedObjectsClient.get.mockResolvedValueOnce({ + id: 'policy-id-conflict', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + references: [], + version: 'WzEsMV0=', + attributes: existingAttributes, + }); + + mockSavedObjectsClient.update.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createConflictError( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-conflict' + ) + ); + + await expect( + client.updateNotificationPolicy({ + data: { workflow_id: 'new-workflow' }, + options: { id: 'policy-id-conflict', version: 'WzEsMV0=' }, + }) + ).rejects.toMatchObject({ + output: { statusCode: 409 }, + }); + }); + }); + + describe('deleteNotificationPolicy', () => { + it('deletes a notification policy successfully', async () => { + const existingAttributes: NotificationPolicySavedObjectAttributes = { + name: 'policy-to-delete', + description: 'policy-to-delete description', + workflow_id: 'workflow-to-delete', + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + mockSavedObjectsClient.get.mockResolvedValueOnce({ + id: 'policy-id-del-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + references: [], + version: 'WzEsMV0=', + attributes: existingAttributes, + }); + + await client.deleteNotificationPolicy({ id: 'policy-id-del-1' }); + + expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-del-1' + ); + }); + + it('throws 404 when notification policy is not found', async () => { + mockSavedObjectsClient.get.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createGenericNotFoundError( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-id-del-404' + ) + ); + + await expect( + client.deleteNotificationPolicy({ id: 'policy-id-del-404' }) + ).rejects.toMatchObject({ + output: { statusCode: 404 }, + }); + + expect(mockSavedObjectsClient.delete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts new file mode 100644 index 0000000000000..d65ca0d794526 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts @@ -0,0 +1,120 @@ +/* + * 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 Boom from '@hapi/boom'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { inject, injectable } from 'inversify'; +import { omit } from 'lodash'; +import { type NotificationPolicySavedObjectAttributes } from '../../saved_objects'; +import type { NotificationPolicySavedObjectServiceContract } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service'; +import { NotificationPolicySavedObjectService } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service'; +import type { UserServiceContract } from '../services/user_service/user_service'; +import { UserService } from '../services/user_service/user_service'; +import type { + CreateNotificationPolicyParams, + NotificationPolicyResponse, + UpdateNotificationPolicyParams, +} from './types'; + +@injectable() +export class NotificationPolicyClient { + constructor( + @inject(NotificationPolicySavedObjectService) + private readonly notificationPolicySavedObjectService: NotificationPolicySavedObjectServiceContract, + @inject(UserService) private readonly userService: UserServiceContract + ) {} + + public async createNotificationPolicy( + params: CreateNotificationPolicyParams + ): Promise { + const userProfileUid = await this.getUserProfileUid(); + const now = new Date().toISOString(); + + const attributes: NotificationPolicySavedObjectAttributes = { + name: params.data.name, + description: params.data.description, + workflow_id: params.data.workflow_id, + createdBy: userProfileUid, + createdAt: now, + updatedBy: userProfileUid, + updatedAt: now, + }; + + try { + const { id, version } = await this.notificationPolicySavedObjectService.create({ + attrs: attributes, + id: params.options?.id, + }); + + return { id, version, ...attributes }; + } catch (e) { + if (SavedObjectsErrorHelpers.isConflictError(e)) { + const conflictId = params.options?.id ?? 'unknown'; + throw Boom.conflict(`Notification policy with id "${conflictId}" already exists`); + } + throw e; + } + } + + public async getNotificationPolicy({ id }: { id: string }): Promise { + try { + const doc = await this.notificationPolicySavedObjectService.get(id); + return { id, version: doc.version, ...doc.attributes }; + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + throw Boom.notFound(`Notification policy with id "${id}" not found`); + } + throw e; + } + } + + public async updateNotificationPolicy( + params: UpdateNotificationPolicyParams + ): Promise { + const userProfileUid = await this.getUserProfileUid(); + const now = new Date().toISOString(); + + const existingNotificationPolicy = await this.getNotificationPolicy({ id: params.options.id }); + const existingAttrs: NotificationPolicySavedObjectAttributes = omit( + existingNotificationPolicy, + ['id', 'version'] + ); + + const nextAttrs: NotificationPolicySavedObjectAttributes = { + ...existingAttrs, + ...params.data, + updatedBy: userProfileUid, + updatedAt: now, + }; + + try { + const updated = await this.notificationPolicySavedObjectService.update({ + id: params.options.id, + attrs: nextAttrs, + version: params.options.version, + }); + + return { id: params.options.id, version: updated.version, ...nextAttrs }; + } catch (e) { + if (SavedObjectsErrorHelpers.isConflictError(e)) { + throw Boom.conflict( + `Notification policy with id "${params.options.id}" has already been updated by another user` + ); + } + throw e; + } + } + + public async deleteNotificationPolicy({ id }: { id: string }): Promise { + await this.getNotificationPolicy({ id }); + await this.notificationPolicySavedObjectService.delete({ id }); + } + + private async getUserProfileUid(): Promise { + return this.userService.getCurrentUserProfileUid(); + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts new file mode 100644 index 0000000000000..88b50b45213e3 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CreateNotificationPolicyData { + name: string; + description: string; + workflow_id: string; +} + +export interface UpdateNotificationPolicyData { + name?: string; + description?: string; + workflow_id?: string; +} + +export interface UpdateNotificationPolicyParams { + data: UpdateNotificationPolicyData; + options: { id: string; version: string }; +} + +export interface CreateNotificationPolicyParams { + data: CreateNotificationPolicyData; + options?: { id?: string }; +} + +export interface NotificationPolicyResponse { + id: string; + version?: string; + name: string; + description: string; + workflow_id: string; + createdBy: string | null; + createdAt: string; + updatedBy: string | null; + updatedAt: string | null; +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/security/privileges.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/security/privileges.ts index 4b7562da1354c..eaac75ebc43a6 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/security/privileges.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/security/privileges.ts @@ -22,6 +22,10 @@ export const ALERTING_V2_API_PRIVILEGES = { read: 'read-alerting-v2-alerts', write: 'write-alerting-v2-alerts', }, + notificationPolicies: { + read: 'read-alerting-v2-notification-policies', + write: 'write-alerting-v2-notification-policies', + }, } as const; const getPrivileges = () => ({ @@ -37,6 +41,8 @@ const getPrivileges = () => ({ ALERTING_V2_API_PRIVILEGES.rules.write, ALERTING_V2_API_PRIVILEGES.alerts.read, ALERTING_V2_API_PRIVILEGES.alerts.write, + ALERTING_V2_API_PRIVILEGES.notificationPolicies.read, + ALERTING_V2_API_PRIVILEGES.notificationPolicies.write, ], }, read: { @@ -46,7 +52,11 @@ const getPrivileges = () => ({ read: [], }, ui: [], - api: [ALERTING_V2_API_PRIVILEGES.rules.read, ALERTING_V2_API_PRIVILEGES.alerts.read], + api: [ + ALERTING_V2_API_PRIVILEGES.rules.read, + ALERTING_V2_API_PRIVILEGES.alerts.read, + ALERTING_V2_API_PRIVILEGES.notificationPolicies.read, + ], }, }); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.mock.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.mock.ts new file mode 100644 index 0000000000000..672b46a299a73 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.mock.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks'; +import { spacesMock } from '@kbn/spaces-plugin/server/mocks'; +import { NotificationPolicySavedObjectService } from './notification_policy_saved_object_service'; + +export function createNotificationPolicySavedObjectService(): { + notificationPolicySavedObjectService: NotificationPolicySavedObjectService; + mockSavedObjectsClient: jest.Mocked; +} { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockSavedObjectsClientFactory = jest.fn().mockReturnValue(mockSavedObjectsClient); + const mockSpaces = spacesMock.createStart(); + + const notificationPolicySavedObjectService = new NotificationPolicySavedObjectService( + mockSavedObjectsClientFactory, + mockSpaces + ); + + return { notificationPolicySavedObjectService, mockSavedObjectsClient }; +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts new file mode 100644 index 0000000000000..c1a13a15f515d --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts @@ -0,0 +1,113 @@ +/* + * 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 { PluginStart } from '@kbn/core-di'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import type { ISavedObjectsClientFactory } from '@kbn/core-di-server'; +import { SavedObjectsClientFactory } from '@kbn/core-di-server'; +import { inject, injectable } from 'inversify'; +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { SavedObjectsUtils } from '@kbn/core/server'; +import { NOTIFICATION_POLICY_SAVED_OBJECT_TYPE } from '../../../saved_objects'; +import type { NotificationPolicySavedObjectAttributes } from '../../../saved_objects'; +import type { AlertingServerStartDependencies } from '../../../types'; +import { spaceIdToNamespace } from '../../space_id_to_namespace'; + +export interface NotificationPolicySavedObjectServiceContract { + create(params: { + attrs: NotificationPolicySavedObjectAttributes; + id?: string; + }): Promise<{ id: string; version?: string }>; + get( + id: string, + spaceId?: string + ): Promise<{ id: string; attributes: NotificationPolicySavedObjectAttributes; version?: string }>; + update(params: { + id: string; + attrs: NotificationPolicySavedObjectAttributes; + version: string; + }): Promise<{ id: string; version?: string }>; + delete(params: { id: string }): Promise; +} + +@injectable() +export class NotificationPolicySavedObjectService + implements NotificationPolicySavedObjectServiceContract +{ + private readonly client: SavedObjectsClientContract; + + constructor( + @inject(SavedObjectsClientFactory) + private readonly savedObjectsClientFactory: ISavedObjectsClientFactory, + @inject(PluginStart('spaces')) + private readonly spaces: SpacesPluginStart + ) { + this.client = this.savedObjectsClientFactory({ + includedHiddenTypes: [NOTIFICATION_POLICY_SAVED_OBJECT_TYPE], + }); + } + + public async create({ + attrs, + id, + }: { + attrs: NotificationPolicySavedObjectAttributes; + id?: string; + }): Promise<{ id: string; version?: string }> { + const notificationPolicyId = id ?? SavedObjectsUtils.generateId(); + const result = await this.client.create( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attrs, + { + id: notificationPolicyId, + overwrite: false, + } + ); + + return { id: result.id, version: result.version }; + } + + public async get( + id: string, + spaceId?: string + ): Promise<{ + id: string; + attributes: NotificationPolicySavedObjectAttributes; + version?: string; + }> { + const namespace = spaceIdToNamespace(this.spaces, spaceId); + const doc = await this.client.get( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + id, + namespace ? { namespace } : undefined + ); + return { id: doc.id, attributes: doc.attributes, version: doc.version }; + } + + public async update({ + id, + attrs, + version, + }: { + id: string; + attrs: NotificationPolicySavedObjectAttributes; + version: string; + }): Promise<{ id: string; version?: string }> { + const result = await this.client.update( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + id, + attrs, + { version } + ); + + return { id: result.id, version: result.version }; + } + + public async delete({ id }: { id: string }): Promise { + await this.client.delete(NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, id); + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/constants.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/constants.ts index 8d3d15229a22e..82229af0d8cf8 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/routes/constants.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/constants.ts @@ -7,3 +7,5 @@ export const INTERNAL_ALERTING_V2_RULE_API_PATH = '/internal/alerting/v2/rule' as const; export const INTERNAL_ALERTING_V2_ALERT_API_PATH = '/internal/alerting/v2/alerts' as const; +export const INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH = + '/internal/alerting/v2/notification_policies' as const; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts new file mode 100644 index 0000000000000..cebe044b15a34 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts @@ -0,0 +1,78 @@ +/* + * 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 Boom from '@hapi/boom'; +import { Logger } from '@kbn/core-di'; +import type { RouteHandler } from '@kbn/core-di-server'; +import { Request, Response } from '@kbn/core-di-server'; +import type { KibanaRequest, KibanaResponseFactory, RouteSecurity } from '@kbn/core-http-server'; +import type { Logger as KibanaLogger } from '@kbn/logging'; +import { z } from '@kbn/zod'; +import { inject, injectable } from 'inversify'; +import { NotificationPolicyClient } from '../../lib/notification_policy_client'; +import { ALERTING_V2_API_PRIVILEGES } from '../../lib/security/privileges'; +import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants'; +import { buildRouteValidationWithZod } from '../route_validation'; + +const createNotificationPolicyParamsSchema = z.object({ + id: z.string().optional(), +}); + +const createNotificationPolicyBodySchema = z.object({ + name: z.string(), + description: z.string(), + workflow_id: z.string(), +}); + +@injectable() +export class CreateNotificationPolicyRoute implements RouteHandler { + static method = 'post' as const; + static path = `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/{id?}`; + static security: RouteSecurity = { + authz: { + requiredPrivileges: [ALERTING_V2_API_PRIVILEGES.notificationPolicies.write], + }, + }; + static options = { access: 'internal' } as const; + static validate = { + request: { + body: buildRouteValidationWithZod(createNotificationPolicyBodySchema), + params: buildRouteValidationWithZod(createNotificationPolicyParamsSchema), + }, + } as const; + + constructor( + @inject(Logger) private readonly logger: KibanaLogger, + @inject(Request) + private readonly request: KibanaRequest< + z.infer, + unknown, + z.infer + >, + @inject(Response) private readonly response: KibanaResponseFactory, + @inject(NotificationPolicyClient) + private readonly notificationPolicyClient: NotificationPolicyClient + ) {} + + async handle() { + try { + const created = await this.notificationPolicyClient.createNotificationPolicy({ + data: this.request.body, + options: { id: this.request.params.id }, + }); + + return this.response.ok({ body: created }); + } catch (e) { + const boom = Boom.isBoom(e) ? e : Boom.boomify(e); + this.logger.debug(`create notification policy route error: ${boom.message}`); + return this.response.customError({ + statusCode: boom.output.statusCode, + body: boom.output.payload, + }); + } + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/delete_notification_policy_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/delete_notification_policy_route.ts new file mode 100644 index 0000000000000..202f103eacc5c --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/delete_notification_policy_route.ts @@ -0,0 +1,65 @@ +/* + * 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 Boom from '@hapi/boom'; +import { Request, Response } from '@kbn/core-di-server'; +import type { KibanaRequest, KibanaResponseFactory, RouteSecurity } from '@kbn/core-http-server'; +import { z } from '@kbn/zod'; +import { inject, injectable } from 'inversify'; +import { NotificationPolicyClient } from '../../lib/notification_policy_client'; +import { ALERTING_V2_API_PRIVILEGES } from '../../lib/security/privileges'; +import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants'; +import { buildRouteValidationWithZod } from '../route_validation'; + +const deleteNotificationPolicyParamsSchema = z.object({ + id: z.string(), +}); + +@injectable() +export class DeleteNotificationPolicyRoute { + static method = 'delete' as const; + static path = `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/{id}`; + static security: RouteSecurity = { + authz: { + requiredPrivileges: [ALERTING_V2_API_PRIVILEGES.notificationPolicies.write], + }, + }; + static options = { access: 'internal' } as const; + static validate = { + request: { + params: buildRouteValidationWithZod(deleteNotificationPolicyParamsSchema), + }, + } as const; + + constructor( + @inject(Request) + private readonly request: KibanaRequest< + z.infer, + unknown, + unknown, + 'delete' + >, + @inject(Response) private readonly response: KibanaResponseFactory, + @inject(NotificationPolicyClient) + private readonly notificationPolicyClient: NotificationPolicyClient + ) {} + + async handle() { + try { + await this.notificationPolicyClient.deleteNotificationPolicy({ + id: this.request.params.id, + }); + return this.response.noContent(); + } catch (e) { + const boom = Boom.isBoom(e) ? e : Boom.boomify(e); + return this.response.customError({ + statusCode: boom.output.statusCode, + body: boom.output.payload, + }); + } + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/get_notification_policy_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/get_notification_policy_route.ts new file mode 100644 index 0000000000000..0982b6469e3c2 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/get_notification_policy_route.ts @@ -0,0 +1,64 @@ +/* + * 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 Boom from '@hapi/boom'; +import { Request, Response } from '@kbn/core-di-server'; +import type { KibanaRequest, KibanaResponseFactory, RouteSecurity } from '@kbn/core-http-server'; +import { z } from '@kbn/zod'; +import { inject, injectable } from 'inversify'; +import { NotificationPolicyClient } from '../../lib/notification_policy_client'; +import { ALERTING_V2_API_PRIVILEGES } from '../../lib/security/privileges'; +import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants'; +import { buildRouteValidationWithZod } from '../route_validation'; + +const getNotificationPolicyParamsSchema = z.object({ + id: z.string(), +}); + +@injectable() +export class GetNotificationPolicyRoute { + static method = 'get' as const; + static path = `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/{id}`; + static security: RouteSecurity = { + authz: { + requiredPrivileges: [ALERTING_V2_API_PRIVILEGES.notificationPolicies.read], + }, + }; + static options = { access: 'internal' } as const; + static validate = { + request: { + params: buildRouteValidationWithZod(getNotificationPolicyParamsSchema), + }, + } as const; + + constructor( + @inject(Request) + private readonly request: KibanaRequest< + z.infer, + unknown, + unknown + >, + @inject(Response) private readonly response: KibanaResponseFactory, + @inject(NotificationPolicyClient) + private readonly notificationPolicyClient: NotificationPolicyClient + ) {} + + async handle() { + try { + const notificationPolicy = await this.notificationPolicyClient.getNotificationPolicy({ + id: this.request.params.id, + }); + return this.response.ok({ body: notificationPolicy }); + } catch (e) { + const boom = Boom.isBoom(e) ? e : Boom.boomify(e); + return this.response.customError({ + statusCode: boom.output.statusCode, + body: boom.output.payload, + }); + } + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts new file mode 100644 index 0000000000000..63972a9743cc0 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts @@ -0,0 +1,75 @@ +/* + * 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 Boom from '@hapi/boom'; +import { Request, Response } from '@kbn/core-di-server'; +import type { KibanaRequest, KibanaResponseFactory, RouteSecurity } from '@kbn/core-http-server'; +import { z } from '@kbn/zod'; +import { inject, injectable } from 'inversify'; +import { omit } from 'lodash'; +import { NotificationPolicyClient } from '../../lib/notification_policy_client'; +import { ALERTING_V2_API_PRIVILEGES } from '../../lib/security/privileges'; +import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants'; +import { buildRouteValidationWithZod } from '../route_validation'; + +const updateNotificationPolicyParamsSchema = z.object({ + id: z.string(), +}); + +const updateNotificationPolicyBodySchema = z.object({ + name: z.string().optional(), + description: z.string().optional(), + workflow_id: z.string().optional(), + version: z.string(), +}); + +@injectable() +export class UpdateNotificationPolicyRoute { + static method = 'put' as const; + static path = `${INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH}/{id}`; + static security: RouteSecurity = { + authz: { + requiredPrivileges: [ALERTING_V2_API_PRIVILEGES.notificationPolicies.write], + }, + }; + static options = { access: 'internal' } as const; + static validate = { + request: { + body: buildRouteValidationWithZod(updateNotificationPolicyBodySchema), + params: buildRouteValidationWithZod(updateNotificationPolicyParamsSchema), + }, + } as const; + + constructor( + @inject(Request) + private readonly request: KibanaRequest< + z.infer, + unknown, + z.infer + >, + @inject(Response) private readonly response: KibanaResponseFactory, + @inject(NotificationPolicyClient) + private readonly notificationPolicyClient: NotificationPolicyClient + ) {} + + async handle() { + try { + const updated = await this.notificationPolicyClient.updateNotificationPolicy({ + data: omit(this.request.body, ['version']), + options: { id: this.request.params.id, version: this.request.body.version }, + }); + + return this.response.ok({ body: updated }); + } catch (e) { + const boom = Boom.isBoom(e) ? e : Boom.boomify(e); + return this.response.customError({ + statusCode: boom.output.statusCode, + body: boom.output.payload, + }); + } + } +} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/index.ts index 85a9cbc6859f1..c81c1f95818e0 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/index.ts @@ -5,14 +5,16 @@ * 2.0. */ -import type { Logger, SavedObject, SavedObjectsServiceSetup } from '@kbn/core/server'; import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; - -import type { RuleSavedObjectAttributes } from './schemas/rule_saved_object_attributes'; +import type { Logger, SavedObject, SavedObjectsServiceSetup } from '@kbn/core/server'; +import { notificationPolicyModelVersions, ruleModelVersions } from './model_versions'; +import { notificationPolicyMappings } from './notification_policy_mappings'; import { ruleMappings } from './rule_mappings'; -import { ruleModelVersions } from './model_versions'; +import type { NotificationPolicySavedObjectAttributes } from './schemas/notification_policy_saved_object_attributes'; +import type { RuleSavedObjectAttributes } from './schemas/rule_saved_object_attributes'; export const RULE_SAVED_OBJECT_TYPE = 'alerting_rule'; +export const NOTIFICATION_POLICY_SAVED_OBJECT_TYPE = 'alerting_notification_policy'; export function registerSavedObjects({ savedObjects, @@ -35,6 +37,22 @@ export function registerSavedObjects({ }, modelVersions: ruleModelVersions, }); + + savedObjects.registerType({ + name: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + indexPattern: ALERTING_CASES_SAVED_OBJECT_INDEX, + hidden: true, + namespaceType: 'multiple-isolated', + mappings: notificationPolicyMappings, + management: { + importableAndExportable: false, + getTitle(so: SavedObject) { + return `Notification Policy: [${so.attributes.name}]`; + }, + }, + modelVersions: notificationPolicyModelVersions, + }); } +export type { NotificationPolicySavedObjectAttributes } from './schemas/notification_policy_saved_object_attributes'; export type { RuleSavedObjectAttributes } from './schemas/rule_saved_object_attributes'; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/index.ts index 2b137f1a25af4..df7d790b2bc44 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/index.ts @@ -6,3 +6,4 @@ */ export { ruleModelVersions } from './rule_model_versions'; +export { notificationPolicyModelVersions } from './notification_policy_model_versions'; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/notification_policy_model_versions.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/notification_policy_model_versions.ts new file mode 100644 index 0000000000000..3a9f4ab1b7725 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/model_versions/notification_policy_model_versions.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; +import { notificationPolicySavedObjectAttributesSchemaV1 } from '../schemas/notification_policy_saved_object_attributes'; + +export const notificationPolicyModelVersions: SavedObjectsModelVersionMap = { + '1': { + changes: [], + schemas: { + forwardCompatibility: notificationPolicySavedObjectAttributesSchemaV1.extends( + {}, + { unknowns: 'ignore' } + ), + create: notificationPolicySavedObjectAttributesSchemaV1, + }, + }, +}; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts new file mode 100644 index 0000000000000..cb6dd4e9241e1 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsTypeMappingDefinition } from '@kbn/core-saved-objects-server'; + +/** + * Mappings for the notification policy saved object. + */ +export const notificationPolicyMappings: SavedObjectsTypeMappingDefinition = { + dynamic: false, + properties: { + name: { type: 'text', fields: { keyword: { type: 'keyword', ignore_above: 256 } } }, + description: { type: 'text', fields: { keyword: { type: 'keyword', ignore_above: 256 } } }, + workflow_id: { type: 'keyword' }, + createdBy: { type: 'keyword' }, + createdAt: { type: 'date' }, + updatedBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + }, +}; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/index.ts new file mode 100644 index 0000000000000..e84719a6ec82e --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { notificationPolicySavedObjectAttributesSchema } from './v1'; + +export type NotificationPolicySavedObjectAttributes = TypeOf< + typeof notificationPolicySavedObjectAttributesSchema +>; + +export { notificationPolicySavedObjectAttributesSchema as notificationPolicySavedObjectAttributesSchemaV1 }; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts new file mode 100644 index 0000000000000..e93e4874fcef1 --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +/** + * Attributes for the notification policy saved object. + */ +export const notificationPolicySavedObjectAttributesSchema = schema.object({ + name: schema.string(), + description: schema.string(), + workflow_id: schema.string(), + createdBy: schema.nullable(schema.string()), + updatedBy: schema.nullable(schema.string()), + createdAt: schema.string(), + updatedAt: schema.nullable(schema.string()), +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts index 7baa0b8ed551a..2f5aac72cfe83 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_routes.ts @@ -15,6 +15,10 @@ import { DeleteRuleRoute } from '../routes/delete_rule_route'; import { RunDispatchRoute } from '../routes/run_dispatch_route'; import { CreateAlertActionRoute } from '../routes/create_alert_action_route'; import { BulkCreateAlertActionRoute } from '../routes/bulk_create_alert_action_route'; +import { CreateNotificationPolicyRoute } from '../routes/notification_policies/create_notification_policy_route'; +import { GetNotificationPolicyRoute } from '../routes/notification_policies/get_notification_policy_route'; +import { UpdateNotificationPolicyRoute } from '../routes/notification_policies/update_notification_policy_route'; +import { DeleteNotificationPolicyRoute } from '../routes/notification_policies/delete_notification_policy_route'; export function bindRoutes({ bind }: ContainerModuleLoadOptions) { bind(Route).toConstantValue(CreateRuleRoute); @@ -25,4 +29,8 @@ export function bindRoutes({ bind }: ContainerModuleLoadOptions) { bind(Route).toConstantValue(RunDispatchRoute); bind(Route).toConstantValue(CreateAlertActionRoute); bind(Route).toConstantValue(BulkCreateAlertActionRoute); + bind(Route).toConstantValue(CreateNotificationPolicyRoute); + bind(Route).toConstantValue(GetNotificationPolicyRoute); + bind(Route).toConstantValue(UpdateNotificationPolicyRoute); + bind(Route).toConstantValue(DeleteNotificationPolicyRoute); } diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts index cb386c441789f..a0dd733a48cdc 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_services.ts @@ -10,6 +10,7 @@ import type { ContainerModuleLoadOptions } from 'inversify'; import { AlertActionsClient } from '../lib/alert_actions_client'; import { DispatcherService } from '../lib/dispatcher/dispatcher'; import { RulesClient } from '../lib/rules_client'; +import { NotificationPolicyClient } from '../lib/notification_policy_client'; import { LoggerService, LoggerServiceToken } from '../lib/services/logger_service/logger_service'; import { QueryService } from '../lib/services/query_service/query_service'; import { @@ -18,6 +19,7 @@ import { } from '../lib/services/query_service/tokens'; import { AlertingRetryService } from '../lib/services/retry_service'; import { RulesSavedObjectService } from '../lib/services/rules_saved_object_service/rules_saved_object_service'; +import { NotificationPolicySavedObjectService } from '../lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service'; import { StorageService } from '../lib/services/storage_service/storage_service'; import { StorageServiceInternalToken, @@ -38,6 +40,7 @@ import { export function bindServices({ bind }: ContainerModuleLoadOptions) { bind(AlertActionsClient).toSelf().inRequestScope(); bind(RulesClient).toSelf().inRequestScope(); + bind(NotificationPolicyClient).toSelf().inRequestScope(); bind(UserService).toSelf().inRequestScope(); bind(AlertingRetryService).toSelf().inSingletonScope(); bind(RetryServiceToken).toService(AlertingRetryService); @@ -68,6 +71,7 @@ export function bindServices({ bind }: ContainerModuleLoadOptions) { ); bind(RulesSavedObjectService).toSelf().inRequestScope(); + bind(NotificationPolicySavedObjectService).toSelf().inRequestScope(); bind(QueryServiceScopedToken) .toDynamicValue(({ get }) => { diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/index.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/index.ts index 1b343f8fa744e..27f35a6cce2fb 100644 --- a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/index.ts +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) describe('alerting_v2', () => { loadTestFile(require.resolve('./create_alert_action')); loadTestFile(require.resolve('./bulk_create_alert_action')); + loadTestFile(require.resolve('./notification_policy')); }); } diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts new file mode 100644 index 0000000000000..fe0f8273d8a6f --- /dev/null +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts @@ -0,0 +1,126 @@ +/* + * 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 type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import type { RoleCredentials } from '../../../services'; + +const NOTIFICATION_POLICY_API_PATH = '/internal/alerting/v2/notification_policies'; +const NOTIFICATION_POLICY_SO_TYPE = 'alerting_notification_policy'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const kibanaServer = getService('kibanaServer'); + + describe('Create Notification Policy API', function () { + let roleAuthc: RoleCredentials; + + before(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + }); + + after(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + it('should create a notification policy with auto-generated id', async () => { + const response = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'my-policy', + description: 'my-policy description', + workflow_id: 'my-workflow-id', + }); + + expect(response.status).to.be(200); + expect(response.body.id).to.be.a('string'); + expect(response.body.version).to.be.a('string'); + expect(response.body.name).to.be('my-policy'); + expect(response.body.description).to.be('my-policy description'); + expect(response.body.workflow_id).to.be('my-workflow-id'); + expect(response.body.createdAt).to.be.a('string'); + expect(response.body.updatedAt).to.be.a('string'); + }); + + it('should create a notification policy with a custom id', async () => { + const customId = 'custom-notification-policy-id'; + + const response = await supertestWithoutAuth + .post(`${NOTIFICATION_POLICY_API_PATH}/${customId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'another-policy', + description: 'another-policy description', + workflow_id: 'another-workflow-id', + }); + + expect(response.status).to.be(200); + expect(response.body.id).to.be(customId); + expect(response.body.name).to.be('another-policy'); + expect(response.body.description).to.be('another-policy description'); + expect(response.body.workflow_id).to.be('another-workflow-id'); + }); + + it('should return 409 when creating a notification policy with an existing id', async () => { + const existingId = 'existing-policy-id'; + + // Create the first policy + const firstResponse = await supertestWithoutAuth + .post(`${NOTIFICATION_POLICY_API_PATH}/${existingId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ name: 'policy-1', description: 'policy-1 description', workflow_id: 'workflow-1' }); + + expect(firstResponse.status).to.be(200); + + // Try to create another policy with the same id + const secondResponse = await supertestWithoutAuth + .post(`${NOTIFICATION_POLICY_API_PATH}/${existingId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ name: 'policy-2', description: 'policy-2 description', workflow_id: 'workflow-2' }); + + expect(secondResponse.status).to.be(409); + }); + + it('should return 400 when name is missing', async () => { + const response = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ description: 'my-policy description', workflow_id: 'my-workflow-id' }); + + expect(response.status).to.be(400); + }); + + it('should return 400 when description is missing', async () => { + const response = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ name: 'my-policy', workflow_id: 'my-workflow-id' }); + + expect(response.status).to.be(400); + }); + + it('should return 400 when workflow_id is missing', async () => { + const response = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ name: 'my-policy', description: 'my-policy description' }); + + expect(response.status).to.be(400); + }); + }); +} diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts new file mode 100644 index 0000000000000..6775a3006f96f --- /dev/null +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts @@ -0,0 +1,70 @@ +/* + * 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 type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import type { RoleCredentials } from '../../../services'; + +const NOTIFICATION_POLICY_API_PATH = '/internal/alerting/v2/notification_policies'; +const NOTIFICATION_POLICY_SO_TYPE = 'alerting_notification_policy'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const kibanaServer = getService('kibanaServer'); + + describe('Delete Notification Policy API', function () { + let roleAuthc: RoleCredentials; + + before(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + }); + + after(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + it('should delete a notification policy by id', async () => { + const createResponse = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'test-policy-for-delete', + description: 'test-policy-for-delete description', + workflow_id: 'test-workflow-for-delete', + }); + + const createdPolicyId = createResponse.body.id; + + const response = await supertestWithoutAuth + .delete(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + expect(response.status).to.be(204); + + const getResponse = await supertestWithoutAuth + .get(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + expect(getResponse.status).to.be(404); + }); + + it('should return 404 when deleting a non-existent notification policy', async () => { + const response = await supertestWithoutAuth + .delete(`${NOTIFICATION_POLICY_API_PATH}/non-existent-id`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + }); +} diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts new file mode 100644 index 0000000000000..0935164d1bd67 --- /dev/null +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts @@ -0,0 +1,70 @@ +/* + * 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 type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import type { RoleCredentials } from '../../../services'; + +const NOTIFICATION_POLICY_API_PATH = '/internal/alerting/v2/notification_policies'; +const NOTIFICATION_POLICY_SO_TYPE = 'alerting_notification_policy'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const kibanaServer = getService('kibanaServer'); + + describe('Get Notification Policy API', function () { + let roleAuthc: RoleCredentials; + + before(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + }); + + after(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + it('should get a notification policy by id', async () => { + const createResponse = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'policy-name', + description: 'policy-description', + workflow_id: 'policy-workflow-id', + }); + + const createdPolicyId = createResponse.body.id; + + const response = await supertestWithoutAuth + .get(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + expect(response.status).to.be(200); + expect(response.body.id).to.be(createdPolicyId); + expect(response.body.version).to.be.a('string'); + expect(response.body.name).to.be('policy-name'); + expect(response.body.description).to.be('policy-description'); + expect(response.body.workflow_id).to.be('policy-workflow-id'); + expect(response.body.createdAt).to.be.a('string'); + expect(response.body.updatedAt).to.be.a('string'); + }); + + it('should return 404 for non-existent notification policy', async () => { + const response = await supertestWithoutAuth + .get(`${NOTIFICATION_POLICY_API_PATH}/non-existent-id`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()); + + expect(response.status).to.be(404); + }); + }); +} diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts new file mode 100644 index 0000000000000..a9e37dab6b752 --- /dev/null +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('notification_policy', () => { + loadTestFile(require.resolve('./create_notification_policy')); + loadTestFile(require.resolve('./get_notification_policy')); + loadTestFile(require.resolve('./update_notification_policy')); + loadTestFile(require.resolve('./delete_notification_policy')); + }); +} diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts new file mode 100644 index 0000000000000..549e38600952e --- /dev/null +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts @@ -0,0 +1,142 @@ +/* + * 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 type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import type { RoleCredentials } from '../../../services'; + +const NOTIFICATION_POLICY_API_PATH = '/internal/alerting/v2/notification_policies'; +const NOTIFICATION_POLICY_SO_TYPE = 'alerting_notification_policy'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const samlAuth = getService('samlAuth'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const kibanaServer = getService('kibanaServer'); + + describe('Update Notification Policy API', function () { + let roleAuthc: RoleCredentials; + + before(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin'); + }); + + after(async () => { + await kibanaServer.savedObjects.clean({ types: [NOTIFICATION_POLICY_SO_TYPE] }); + await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc); + }); + + it('should update a notification policy', async () => { + const createResponse = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'original-policy', + description: 'original-policy-description', + workflow_id: 'original-workflow-id', + }); + + expect(createResponse.status).to.be(200); + + const createdPolicyId = createResponse.body.id as string; + const currentVersion = createResponse.body.version as string; + + const response = await supertestWithoutAuth + .put(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'updated-policy', + workflow_id: 'updated-workflow-id', + description: 'updated-policy-description', + version: currentVersion, + }); + + expect(response.status).to.be(200); + expect(response.body.id).to.be(createdPolicyId); + expect(response.body.version).to.be.a('string'); + expect(response.body.name).to.be('updated-policy'); + expect(response.body.workflow_id).to.be('updated-workflow-id'); + expect(response.body.description).to.be('updated-policy-description'); + expect(response.body.updatedAt).to.be.a('string'); + }); + + it('should update only name', async () => { + const createResponse = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'original-policy', + description: 'original-policy-description', + workflow_id: 'original-workflow-id', + }); + + expect(createResponse.status).to.be(200); + + const createdPolicyId = createResponse.body.id as string; + const currentVersion = createResponse.body.version as string; + + const response = await supertestWithoutAuth + .put(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ name: 'only-name-updated', version: currentVersion }); + + expect(response.status).to.be(200); + expect(response.body.version).to.be.a('string'); + expect(response.body.name).to.be('only-name-updated'); + expect(response.body.description).to.be('original-policy-description'); + expect(response.body.workflow_id).to.be('original-workflow-id'); + }); + + it('should update only description', async () => { + const createResponse = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'original-policy', + description: 'original-policy-description', + workflow_id: 'original-workflow-id', + }); + + expect(createResponse.status).to.be(200); + + const createdPolicyId = createResponse.body.id as string; + const currentVersion = createResponse.body.version as string; + + const response = await supertestWithoutAuth + .put(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ description: 'only-description-updated', version: currentVersion }); + + expect(response.status).to.be(200); + expect(response.body.version).to.be.a('string'); + expect(response.body.name).to.be('original-policy'); + expect(response.body.description).to.be('only-description-updated'); + expect(response.body.workflow_id).to.be('original-workflow-id'); + }); + + it('should return 404 when updating a non-existent notification policy', async () => { + const response = await supertestWithoutAuth + .put(`${NOTIFICATION_POLICY_API_PATH}/non-existent-id`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'some-name', + description: 'some-description', + workflow_id: 'some-workflow-id', + version: 'WzEsMV0=', + }); + + expect(response.status).to.be(404); + }); + }); +}