From 917c17df0f1c77cbe2931d65f30ead186f449488 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 19 Aug 2020 23:31:54 +0100 Subject: [PATCH 01/18] Revert "removed ESO migration from alerting" This reverts commit 04054fb6481e33616b1677b909c69db535c82e0f. --- .../alerts/server/saved_objects/index.ts | 2 + .../server/saved_objects/migrations.test.ts | 121 ++++++++++++++++++ .../alerts/server/saved_objects/migrations.ts | 53 ++++++++ .../spaces_only/tests/alerting/index.ts | 3 + .../spaces_only/tests/alerting/migrations.ts | 43 +++++++ 5 files changed, 222 insertions(+) create mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.test.ts create mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index c98d9bcbd9ae5..06ce8d673e6b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -6,6 +6,7 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; +import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export function setupSavedObjects( @@ -16,6 +17,7 @@ export function setupSavedObjects( name: 'alert', hidden: true, namespaceType: 'single', + migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, }); diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..19f4e918b7862 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { RawAlert } from '../types'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { migrationMocks } from 'src/core/server/mocks'; + +const { log } = migrationMocks.createContext(); +const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + +describe('7.9.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({}); + expect(migration790(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for alerting', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({ + consumer: 'alerting', + }); + expect(migration790(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'alerts', + }, + }); + }); +}); + +describe('7.10.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({}); + expect(migration710(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for metrics', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + consumer: 'metrics', + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'infrastructure', + }, + }); + }); +}); + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc { + return { + attributes: { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + apiKey: '', + apiKeyOwner: '', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + muteAll: false, + mutedInstanceIds: [], + createdBy: new Date().toISOString(), + updatedBy: new Date().toISOString(), + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: '1', + actionTypeId: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }, + id: uuid.v4(), + type: 'alert', + }; +} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..57a4005887093 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + SavedObjectMigrationMap, + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, +} from '../../../../../src/core/server'; +import { RawAlert } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function getMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationMap { + return { + /** + * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` + * prior to that we were using `alerting` and we need to keep these in sync + */ + '7.9.0': changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'), + /** + * In v7.10.0 we changed the Matrics plugin so it uses the `consumer` value of `infrastructure` + * prior to that we were using `metrics` and we need to keep these in sync + */ + '7.10.0': changeAlertingConsumer(encryptedSavedObjects, 'metrics', 'infrastructure'), + }; +} + +function changeAlertingConsumer( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + from: string, + to: string +): SavedObjectMigrationFn { + return encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return doc.attributes.consumer === from; + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumer === from ? to : consumer, + }, + }; + } + ); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index b927b563eb54a..78ca2af12ec3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -28,5 +28,8 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + + // note that this test will destroy existing spaces + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts new file mode 100644 index 0000000000000..d0e1be12e762f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('alerts'); + }); + + after(async () => { + await esArchiver.unload('alerts'); + }); + + it('7.9.0 migrates the `alerting` consumer to be the `alerts`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('alerts'); + }); + + it('7.10.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('infrastructure'); + }); + }); +} From e2df4b5cb672d46dcda5e31ef4fcab73281208dc Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 20 Aug 2020 15:42:41 +0100 Subject: [PATCH 02/18] add `meta` field to Alert and mark pre 7.10 alerts as legacy --- x-pack/plugins/alerts/server/alerts_client.ts | 17 ++++++++++- .../alerts/server/saved_objects/mappings.json | 7 +++++ .../server/saved_objects/migrations.test.ts | 30 ++++--------------- .../alerts/server/saved_objects/migrations.ts | 29 +++++++++++------- x-pack/plugins/alerts/server/types.ts | 5 ++++ .../spaces_only/tests/alerting/migrations.ts | 4 +-- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 80e021fc5cb6e..4fdf5cde709d6 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -25,6 +25,7 @@ import { SanitizedAlert, AlertTaskState, AlertStatus, + AlertMeta, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -268,6 +269,20 @@ export class AlertsClient { return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } + // public for now, but we won't want to expose this + public async getMeta({ id }: { id: string }): Promise<[SanitizedAlert, AlertMeta | undefined]> { + const result = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.authorization.ensureAuthorized( + result.attributes.alertTypeId, + result.attributes.consumer, + ReadOperations.Get + ); + return [ + this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references), + result.attributes.meta, + ]; + } + public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); await this.authorization.ensureAuthorized( @@ -860,7 +875,7 @@ export class AlertsClient { private getPartialAlertFromRaw( id: string, - { createdAt, ...rawAlert }: Partial, + { createdAt, meta, ...rawAlert }: Partial, updatedAt: SavedObject['updated_at'] = createdAt, references: SavedObjectReference[] | undefined ): PartialAlert { diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json index a7e85febf2446..ab2140d1f0161 100644 --- a/x-pack/plugins/alerts/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json @@ -76,6 +76,13 @@ }, "mutedInstanceIds": { "type": "keyword" + }, + "meta": { + "properties": { + "rbac": { + "type": "keyword" + } + } } } } diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 19f4e918b7862..cd83c21e4f876 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -13,7 +13,7 @@ import { migrationMocks } from 'src/core/server/mocks'; const { log } = migrationMocks.createContext(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); -describe('7.9.0', () => { +describe('7.10.0', () => { beforeEach(() => { jest.resetAllMocks(); encryptedSavedObjectsSetup.createMigration.mockImplementation( @@ -22,9 +22,9 @@ describe('7.9.0', () => { }); test('changes nothing on alerts by other plugins', () => { - const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const alert = getMockData({}); - expect(migration790(alert, { log })).toMatchObject(alert); + expect(migration710(alert, { log })).toMatchObject(alert); expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( expect.any(Function), @@ -33,11 +33,11 @@ describe('7.9.0', () => { }); test('migrates the consumer for alerting', () => { - const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const alert = getMockData({ consumer: 'alerting', }); - expect(migration790(alert, { log })).toMatchObject({ + expect(migration710(alert, { log })).toMatchObject({ ...alert, attributes: { ...alert.attributes, @@ -45,26 +45,6 @@ describe('7.9.0', () => { }, }); }); -}); - -describe('7.10.0', () => { - beforeEach(() => { - jest.resetAllMocks(); - encryptedSavedObjectsSetup.createMigration.mockImplementation( - (shouldMigrateWhenPredicate, migration) => migration - ); - }); - - test('changes nothing on alerts by other plugins', () => { - const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; - const alert = getMockData({}); - expect(migration710(alert, { log })).toMatchObject(alert); - - expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function) - ); - }); test('migrates the consumer for metrics', () => { const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 57a4005887093..19be72a15972f 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -17,25 +17,30 @@ export function getMigrations( return { /** * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` - * prior to that we were using `alerting` and we need to keep these in sync - */ - '7.9.0': changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'), - /** + * prior to that we were using `alerting` and we need to keep these in sync for RBAC to work in 7.10. * In v7.10.0 we changed the Matrics plugin so it uses the `consumer` value of `infrastructure` - * prior to that we were using `metrics` and we need to keep these in sync + * prior to that we were using `metrics` and we need to keep these in sync for RBAC to work in 7.10. */ - '7.10.0': changeAlertingConsumer(encryptedSavedObjects, 'metrics', 'infrastructure'), + '7.10.0': changeAlertingConsumer( + encryptedSavedObjects, + new Map( + Object.entries({ + alerting: 'alerts', + metrics: 'infrastructure', + }) + ) + ), }; } function changeAlertingConsumer( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, - from: string, - to: string + mapping: Map ): SavedObjectMigrationFn { return encryptedSavedObjects.createMigration( function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - return doc.attributes.consumer === from; + // migrate all documents in 7.10 in order to add the "meta" RBAC field + return true; }, (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { const { @@ -45,7 +50,11 @@ function changeAlertingConsumer( ...doc, attributes: { ...doc.attributes, - consumer: consumer === from ? to : consumer, + consumer: mapping.get(consumer) ?? consumer, + // mark any alert predating 7.10 as a legacy alert in terms of RBAC support + meta: { + rbac: 'legacy', + }, }, }; } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 20943ba28885c..32c43ec2c6a3e 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -111,6 +111,10 @@ export interface RawAlertAction extends SavedObjectAttributes { params: AlertActionParams; } +export interface AlertMeta extends SavedObjectAttributes { + rbac?: string; +} + export type PartialAlert = Pick & Partial>; export interface RawAlert extends SavedObjectAttributes { @@ -131,6 +135,7 @@ export interface RawAlert extends SavedObjectAttributes { throttle: string | null; muteAll: boolean; mutedInstanceIds: string[]; + meta?: AlertMeta; } export type AlertInfoParams = Pick< diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index d0e1be12e762f..fa14951dcb6c9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { getUrlPrefix } from '../../../common/lib'; +import { getUrlPrefix, getTestAlertData } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -22,7 +22,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { await esArchiver.unload('alerts'); }); - it('7.9.0 migrates the `alerting` consumer to be the `alerts`', async () => { + it('7.10.0 migrates the `alerting` consumer to be the `alerts`', async () => { const response = await supertest.get( `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` ); From 5a7ee01742f9544916999e9b6338cd9a4b6b1467 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 3 Sep 2020 18:25:45 +0100 Subject: [PATCH 03/18] exempt legacy alerts from rbac --- .../plugins/actions/server/actions_client.ts | 13 +- .../actions_authorization.test.ts | 38 +- .../authorization/actions_authorization.ts | 55 +- ...should_legacy_rbac_apply_by_source.test.ts | 114 + .../should_legacy_rbac_apply_by_source.ts | 25 + .../server/create_execute_function.test.ts | 47 +- .../actions/server/create_execute_function.ts | 57 +- x-pack/plugins/actions/server/index.ts | 2 + .../server/lib/action_execution_source.ts | 47 + .../actions/server/lib/action_executor.ts | 16 +- x-pack/plugins/actions/server/lib/index.ts | 5 + .../actions/server/lib/task_runner_factory.ts | 37 +- x-pack/plugins/actions/server/plugin.ts | 62 +- .../actions/server/saved_objects/index.ts | 1 + .../alerts/server/alerts_client.test.ts | 52 +- x-pack/plugins/alerts/server/alerts_client.ts | 99 +- .../alerts/server/alerts_client_factory.ts | 11 +- .../alerts_authorization.mock.ts | 1 + .../authorization/alerts_authorization.ts | 7 +- x-pack/plugins/alerts/server/plugin.ts | 3 + .../alerts/server/saved_objects/mappings.json | 2 +- .../alerts/server/saved_objects/migrations.ts | 6 +- .../create_execution_handler.test.ts | 98 +- .../task_runner/create_execution_handler.ts | 9 +- .../server/task_runner/task_runner.test.ts | 58 +- x-pack/plugins/alerts/server/types.ts | 4 +- .../fixtures/plugins/alerts/kibana.json | 2 +- .../fixtures/plugins/alerts/server/plugin.ts | 9 +- .../fixtures/plugins/alerts/server/routes.ts | 104 +- .../common/lib/alert_utils.ts | 38 + .../tests/alerting/index.ts | 4 + .../tests/alerting/rbac_legacy.ts | 184 ++ .../security_and_spaces/tests/index.ts | 75 +- .../spaces_only/tests/alerting/migrations.ts | 2 +- .../es_archives/alerts_legacy/data.json | 116 + .../es_archives/alerts_legacy/mappings.json | 2680 +++++++++++++++++ 36 files changed, 3866 insertions(+), 217 deletions(-) create mode 100644 x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts create mode 100644 x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts create mode 100644 x-pack/plugins/actions/server/lib/action_execution_source.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts create mode 100644 x-pack/test/functional/es_archives/alerts_legacy/data.json create mode 100644 x-pack/test/functional/es_archives/alerts_legacy/mappings.json diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index d46ad3e2e2423..06c9555f3a18d 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -30,6 +30,7 @@ import { } from './create_execute_function'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionType } from '../common'; +import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -298,13 +299,19 @@ export class ActionsClient { public async execute({ actionId, params, + source, }: Omit): Promise> { - await this.authorization.ensureAuthorized('execute'); - return this.actionExecutor.execute({ actionId, params, request: this.request }); + if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + await this.authorization.ensureAuthorized('execute'); + } + return this.actionExecutor.execute({ actionId, params, source, request: this.request }); } public async enqueueExecution(options: EnqueueExecutionOptions): Promise { - await this.authorization.ensureAuthorized('execute'); + const { source } = options; + if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + await this.authorization.ensureAuthorized('execute'); + } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); } diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index a48124cdbcb6a..7f5f8c097750b 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -9,6 +9,7 @@ import { ActionsAuthorization } from './actions_authorization'; import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { AuthenticatedUser } from '../../../security/server'; const request = {} as KibanaRequest; @@ -19,12 +20,13 @@ const mockAuthorizationAction = (type: string, operation: string) => `${type}/${ function mockSecurity() { const security = securityMock.createSetup(); const authorization = security.authz; + const authentication = security.authc; // typescript is having trouble inferring jest's automocking (authorization.actions.savedObject.get as jest.MockedFunction< typeof authorization.actions.savedObject.get >).mockImplementation(mockAuthorizationAction); authorization.mode.useRbacForRequest.mockReturnValue(true); - return { authorization }; + return { authorization, authentication }; } beforeEach(() => { @@ -188,4 +190,38 @@ describe('ensureAuthorized', () => { ] `); }); + + test('exempts users from requiring privileges to execute actions when shouldLegacyRbacApply is true', async () => { + const { authorization, authentication } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const actionsAuthorization = new ActionsAuthorization({ + request, + authorization, + authentication, + auditLogger, + shouldLegacyRbacApply: true, + }); + + authentication.getCurrentUser.mockReturnValueOnce(({ + username: 'some-user', + } as unknown) as AuthenticatedUser); + + await actionsAuthorization.ensureAuthorized('execute', 'myType'); + + expect(authorization.actions.savedObject.get).not.toHaveBeenCalled(); + expect(checkPrivileges).not.toHaveBeenCalled(); + + expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "execute", + "myType", + ] + `); + }); }); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index da5a5a1cdc3eb..c951fe9bbdcb5 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -14,6 +14,15 @@ export interface ConstructorOptions { request: KibanaRequest; auditLogger: ActionsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; + authentication?: SecurityPluginSetup['authc']; + // In order to support legacy Alerts which predate the introduciton of the + // Actions feature in Kibana we need a way of "dialing down" the level of + // authorization for certain opearations. + // Specifically, we want to allow these old alerts and their scheduled + // actions to continue to execute - which requires that we exempt auth on + // `get` for Connectors and `execute` for Action execution when used by + // these legacy alerts + shouldLegacyRbacApply?: boolean; } const operationAlias: Record< @@ -27,33 +36,57 @@ const operationAlias: Record< list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'find'), }; +const LEGACY_RBAC_EXEMPT_OPERATIONS = new Set(['get', 'execute']); + export class ActionsAuthorization { private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; + private readonly authentication?: SecurityPluginSetup['authc']; private readonly auditLogger: ActionsAuthorizationAuditLogger; + private readonly shouldLegacyRbacApply: boolean; - constructor({ request, authorization, auditLogger }: ConstructorOptions) { + constructor({ + request, + authorization, + authentication, + auditLogger, + shouldLegacyRbacApply = false, + }: ConstructorOptions) { this.request = request; this.authorization = authorization; + this.authentication = authentication; this.auditLogger = auditLogger; + this.shouldLegacyRbacApply = shouldLegacyRbacApply; } public async ensureAuthorized(operation: string, actionTypeId?: string) { const { authorization } = this; if (authorization?.mode?.useRbacForRequest(this.request)) { - const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username } = await checkPrivileges( - operationAlias[operation] - ? operationAlias[operation](authorization) - : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation) - ); - if (hasAllRequested) { - this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); + if (this.isOperationExemptDueToLegacyRbac(operation)) { + this.auditLogger.actionsAuthorizationSuccess( + this.authentication?.getCurrentUser(this.request)?.username ?? '', + operation, + actionTypeId + ); } else { - throw Boom.forbidden( - this.auditLogger.actionsAuthorizationFailure(username, operation, actionTypeId) + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested, username } = await checkPrivileges( + operationAlias[operation] + ? operationAlias[operation](authorization) + : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation) ); + if (hasAllRequested) { + this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); + } else { + throw Boom.forbidden( + this.auditLogger.actionsAuthorizationFailure(username, operation, actionTypeId) + ); + } } } } + + private isOperationExemptDueToLegacyRbac(operation: string) { + return this.shouldLegacyRbacApply && LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation); + } } diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts new file mode 100644 index 0000000000000..4d7e8990ed02b --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { shouldLegacyRbacApplyBySource } from './should_legacy_rbac_apply_by_source'; +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import uuid from 'uuid'; +import { asSavedObjectExecutionSource } from '../lib'; + +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +describe(`#shouldLegacyRbacApplyBySource`, () => { + test('should return false if no source is provided', async () => { + expect(await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient)).toEqual(false); + }); + + test('should return false if source is not an alert', async () => { + expect( + await shouldLegacyRbacApplyBySource( + unsecuredSavedObjectsClient, + asSavedObjectExecutionSource({ + type: 'action', + id: uuid.v4(), + }) + ) + ).toEqual(false); + }); + + test('should return false if source alert is not marked as legacy', async () => { + const id = uuid.v4(); + unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id })); + expect( + await shouldLegacyRbacApplyBySource( + unsecuredSavedObjectsClient, + asSavedObjectExecutionSource({ + type: 'alert', + id, + }) + ) + ).toEqual(false); + }); + + test('should return true if source alert is marked as legacy', async () => { + const id = uuid.v4(); + unsecuredSavedObjectsClient.get.mockResolvedValue( + mockAlert({ id, attributes: { meta: { versionLastmodified: 'pre-7.10.0' } } }) + ); + expect( + await shouldLegacyRbacApplyBySource( + unsecuredSavedObjectsClient, + asSavedObjectExecutionSource({ + type: 'alert', + id, + }) + ) + ).toEqual(true); + }); + + test('should return false if source alert is marked as modern', async () => { + const id = uuid.v4(); + unsecuredSavedObjectsClient.get.mockResolvedValue( + mockAlert({ id, attributes: { meta: { versionLastmodified: '7.10.0' } } }) + ); + expect( + await shouldLegacyRbacApplyBySource( + unsecuredSavedObjectsClient, + asSavedObjectExecutionSource({ + type: 'alert', + id, + }) + ) + ).toEqual(false); + }); + + test('should return false if source alert is marked with a last modified version', async () => { + const id = uuid.v4(); + unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id, attributes: { meta: {} } })); + expect( + await shouldLegacyRbacApplyBySource( + unsecuredSavedObjectsClient, + asSavedObjectExecutionSource({ + type: 'alert', + id, + }) + ) + ).toEqual(false); + }); +}); + +const mockAlert = (overrides: Record = {}) => ({ + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + ...overrides, +}); diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts new file mode 100644 index 0000000000000..ee6e2a70c7026 --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { ActionExecutionSource, isSavedObjectExecutionSource } from '../lib'; + +const LEGACY_VERSION = 'pre-7.10.0'; + +export async function shouldLegacyRbacApplyBySource( + savedObjectsClient: SavedObjectsClientContract, + executionSource?: ActionExecutionSource +): Promise { + return isSavedObjectExecutionSource(executionSource) && executionSource?.source?.type === 'alert' + ? ( + await savedObjectsClient.get<{ + meta?: { + versionLastmodified?: string; + }; + }>('alert', executionSource.source.id) + ).attributes.meta?.versionLastmodified === LEGACY_VERSION + : false; +} diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 04d4d92945cdb..7682f01ed769d 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'src/core/server'; +import uuid from 'uuid'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { createExecutionEnqueuerFunction } from './create_execute_function'; import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { actionTypeRegistryMock } from './action_type_registry.mock'; +import { + asHttpRequestExecutionSource, + asSavedObjectExecutionSource, +} from './lib/action_execution_source'; const mockTaskManager = taskManagerMock.start(); const savedObjectsClient = savedObjectsClientMock.create(); +const request = {} as KibanaRequest; beforeEach(() => jest.resetAllMocks()); @@ -41,6 +48,7 @@ describe('execute()', () => { params: { baz: false }, spaceId: 'default', apiKey: Buffer.from('123:abc').toString('base64'), + source: asHttpRequestExecutionSource(request), }); expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` @@ -59,11 +67,15 @@ describe('execute()', () => { ] `); expect(savedObjectsClient.get).toHaveBeenCalledWith('action', '123'); - expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', { - actionId: '123', - params: { baz: false }, - apiKey: Buffer.from('123:abc').toString('base64'), - }); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'action_task_params', + { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + {} + ); }); test('schedules the action with all given parameters with a preconfigured action', async () => { @@ -82,6 +94,8 @@ describe('execute()', () => { }, ], }); + const source = { type: 'alert', id: uuid.v4() }; + savedObjectsClient.get.mockResolvedValueOnce({ id: '123', type: 'action', @@ -101,6 +115,7 @@ describe('execute()', () => { params: { baz: false }, spaceId: 'default', apiKey: Buffer.from('123:abc').toString('base64'), + source: asSavedObjectExecutionSource(source), }); expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1); expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` @@ -119,11 +134,23 @@ describe('execute()', () => { ] `); expect(savedObjectsClient.get).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', { - actionId: '123', - params: { baz: false }, - apiKey: Buffer.from('123:abc').toString('base64'), - }); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'action_task_params', + { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + { + references: [ + { + id: source.id, + name: 'source', + type: source.type, + }, + ], + } + ); }); test('throws when passing isESOUsingEphemeralEncryptionKey with true as a value', async () => { diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 85052eef93e05..b226583fade52 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -8,6 +8,8 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './types'; import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects'; +import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; +import { isSavedObjectExecutionSource } from './lib'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -16,15 +18,14 @@ interface CreateExecuteFunctionOptions { preconfiguredActions: PreConfiguredAction[]; } -export interface ExecuteOptions { +export interface ExecuteOptions extends Pick { id: string; - params: Record; spaceId: string; apiKey: string | null; } export type ExecutionEnqueuer = ( - savedObjectsClient: SavedObjectsClientContract, + unsecuredSavedObjectsClient: SavedObjectsClientContract, options: ExecuteOptions ) => Promise; @@ -35,8 +36,8 @@ export function createExecutionEnqueuerFunction({ preconfiguredActions, }: CreateExecuteFunctionOptions) { return async function execute( - savedObjectsClient: SavedObjectsClientContract, - { id, params, spaceId, apiKey }: ExecuteOptions + unsecuredSavedObjectsClient: SavedObjectsClientContract, + { id, params, spaceId, source, apiKey }: ExecuteOptions ) { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( @@ -44,19 +45,24 @@ export function createExecutionEnqueuerFunction({ ); } - const actionTypeId = await getActionTypeId(id); + const actionTypeId = await getActionTypeId( + unsecuredSavedObjectsClient, + preconfiguredActions, + id + ); if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } - const actionTaskParamsRecord = await savedObjectsClient.create( + const actionTaskParamsRecord = await unsecuredSavedObjectsClient.create( ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, { actionId: id, params, apiKey, - } + }, + executionSourceAsSavedObjectReferences(source) ); await taskManager.schedule({ @@ -68,15 +74,34 @@ export function createExecutionEnqueuerFunction({ state: {}, scope: ['actions'], }); + }; +} - async function getActionTypeId(actionId: string): Promise { - const pcAction = preconfiguredActions.find((action) => action.id === actionId); - if (pcAction) { - return pcAction.actionTypeId; +function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorOptions['source']) { + return isSavedObjectExecutionSource(executionSource) + ? { + references: [ + { + name: 'source', + ...executionSource.source, + }, + ], } + : {}; +} - const actionSO = await savedObjectsClient.get('action', actionId); - return actionSO.attributes.actionTypeId; - } - }; +async function getActionTypeId( + unsecuredSavedObjectsClient: SavedObjectsClientContract, + preconfiguredActions: PreConfiguredAction[], + actionId: string +): Promise { + const pcAction = preconfiguredActions.find((action) => action.id === actionId); + if (pcAction) { + return pcAction.actionTypeId; + } + + const { + attributes: { actionTypeId }, + } = await unsecuredSavedObjectsClient.get('action', actionId); + return actionTypeId; } diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index fef70c3a48455..cf574b1009c86 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -22,6 +22,8 @@ export { } from './types'; export { PluginSetupContract, PluginStartContract } from './plugin'; +export { asSavedObjectExecutionSource } from './lib'; + export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); export const config = { diff --git a/x-pack/plugins/actions/server/lib/action_execution_source.ts b/x-pack/plugins/actions/server/lib/action_execution_source.ts new file mode 100644 index 0000000000000..53f815f070bea --- /dev/null +++ b/x-pack/plugins/actions/server/lib/action_execution_source.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest, SavedObjectReference } from 'src/core/server'; + +export enum ActionExecutionSourceType { + SAVED_OBJECT = 'SAVED_OBJECT', + HTTP_REQUEST = 'HTTP_REQUEST', +} + +export interface ActionExecutionSource { + type: ActionExecutionSourceType; + source: T; +} +export type HttpRequestExecutionSource = ActionExecutionSource; +export type SavedObjectExecutionSource = ActionExecutionSource>; + +export function asHttpRequestExecutionSource(source: KibanaRequest): HttpRequestExecutionSource { + return { + type: ActionExecutionSourceType.HTTP_REQUEST, + source, + }; +} + +export function asSavedObjectExecutionSource( + source: Omit +): SavedObjectExecutionSource { + return { + type: ActionExecutionSourceType.SAVED_OBJECT, + source, + }; +} + +export function isHttpRequestExecutionSource( + executionSource?: ActionExecutionSource +): executionSource is HttpRequestExecutionSource { + return executionSource?.type === ActionExecutionSourceType.HTTP_REQUEST; +} + +export function isSavedObjectExecutionSource( + executionSource?: ActionExecutionSource +): executionSource is SavedObjectExecutionSource { + return executionSource?.type === ActionExecutionSourceType.SAVED_OBJECT; +} diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 97c08124f5546..a607dc0de0bda 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, KibanaRequest } from '../../../../../src/core/server'; +import { Logger, KibanaRequest } from 'src/core/server'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { ActionTypeExecutorResult, @@ -16,15 +16,19 @@ import { } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; -import { EVENT_LOG_ACTIONS, PluginStartContract } from '../plugin'; +import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { ActionsClient } from '../actions_client'; +import { ActionExecutionSource } from './action_execution_source'; export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - getActionsClientWithRequest: PluginStartContract['getActionsClientWithRequest']; + getActionsClientWithRequest: ( + request: KibanaRequest, + executionSource?: ActionExecutionSource + ) => Promise>; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; @@ -32,10 +36,11 @@ export interface ActionExecutorContext { proxySettings?: ProxySettings; } -export interface ExecuteOptions { +export interface ExecuteOptions { actionId: string; request: KibanaRequest; params: Record; + source?: ActionExecutionSource; } export type ActionExecutorContract = PublicMethodsOf; @@ -61,6 +66,7 @@ export class ActionExecutor { actionId, params, request, + source, }: ExecuteOptions): Promise> { if (!this.isInitialized) { throw new Error('ActionExecutor not initialized'); @@ -88,7 +94,7 @@ export class ActionExecutor { const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; const { actionTypeId, name, config, secrets } = await getActionInfo( - await getActionsClientWithRequest(request), + await getActionsClientWithRequest(request, source), encryptedSavedObjectsClient, preconfiguredActions, actionId, diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index f03b6de1fc5fb..d2d27eb3d67b6 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -15,3 +15,8 @@ export { ActionTypeDisabledReason, isErrorThatHandlesItsOwnResponse, } from './errors'; +export { + ActionExecutionSource, + asSavedObjectExecutionSource, + isSavedObjectExecutionSource, +} from './action_execution_source'; diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 10a8501e856d2..aeeeb4ed7d520 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -4,9 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pick } from 'lodash'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { map, fromNullable, getOrElse } from 'fp-ts/lib/Option'; +import { + Logger, + SavedObjectsClientContract, + KibanaRequest, + SavedObjectReference, +} from 'src/core/server'; import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; -import { Logger, CoreStart, KibanaRequest } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { ActionTypeDisabledError } from './errors'; @@ -18,6 +26,7 @@ import { ActionTypeExecutorResult, } from '../types'; import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { asSavedObjectExecutionSource } from './action_execution_source'; export interface TaskRunnerContext { logger: Logger; @@ -25,7 +34,7 @@ export interface TaskRunnerContext { encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; - getScopedSavedObjectsClient: CoreStart['savedObjects']['getScopedClient']; + getUnsecuredSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract; } export class TaskRunnerFactory { @@ -56,7 +65,7 @@ export class TaskRunnerFactory { encryptedSavedObjectsClient, spaceIdToNamespace, getBasePath, - getScopedSavedObjectsClient, + getUnsecuredSavedObjectsClient, } = this.taskRunnerContext!; return { @@ -66,6 +75,7 @@ export class TaskRunnerFactory { const { attributes: { actionId, params, apiKey }, + references, } = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskParamsId, @@ -100,6 +110,7 @@ export class TaskRunnerFactory { params, actionId, request: fakeRequest, + ...getSourceFromReferences(references), }); } catch (e) { if (e instanceof ActionTypeDisabledError) { @@ -121,8 +132,14 @@ export class TaskRunnerFactory { // Cleanup action_task_params object now that we're done with it try { - const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); - await savedObjectsClient.delete(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskParamsId); + // If the request has reached this far we can assume the user is allowed to run clean up + // We would idealy secure every operation but in order to support clean up of legacy alerts + // we allow this operation in an unsecured manner + // Once support for legacy alert RBAC is dropped, this can be secured + await getUnsecuredSavedObjectsClient(fakeRequest).delete( + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + actionTaskParamsId + ); } catch (e) { // Log error only, we shouldn't fail the task because of an error here (if ever there's retry logic) logger.error( @@ -133,3 +150,13 @@ export class TaskRunnerFactory { }; } } + +function getSourceFromReferences(references: SavedObjectReference[]) { + return pipe( + fromNullable(references.find((ref) => ref.name === 'source')), + map((source) => ({ + source: asSavedObjectExecutionSource(pick(source, 'id', 'type')), + })), + getOrElse(() => ({})) + ); +} diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 413e6663105b8..d2122cd0b5e3c 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -65,10 +65,13 @@ import { setupSavedObjects, ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + ALERT_SAVED_OBJECT_TYPE, } from './saved_objects'; import { ACTIONS_FEATURE } from './feature'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger'; +import { ActionExecutionSource } from './lib/action_execution_source'; +import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -109,7 +112,11 @@ export interface ActionsPluginsStart { taskManager: TaskManagerStartContract; } -const includedHiddenTypes = [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE]; +const includedHiddenTypes = [ + ACTION_SAVED_OBJECT_TYPE, + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + ALERT_SAVED_OBJECT_TYPE, +]; export class ActionsPlugin implements Plugin, PluginStartContract> { private readonly kibanaIndex: Promise; @@ -265,29 +272,39 @@ export class ActionsPlugin implements Plugin, Plugi isESOUsingEphemeralEncryptionKey, preconfiguredActions, instantiateAuthorization, + getUnsecuredSavedObjectsClient, } = this; const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ includedHiddenTypes, }); - const getActionsClientWithRequest = async (request: KibanaRequest) => { + const getActionsClientWithRequest = async ( + request: KibanaRequest, + sourceContext?: ActionExecutionSource + ) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` ); } + + const unsecuredSavedObjectsClient = getUnsecuredSavedObjectsClient( + core.savedObjects, + request + ); + return new ActionsClient({ - unsecuredSavedObjectsClient: core.savedObjects.getScopedClient(request, { - excludedWrappers: ['security'], - includedHiddenTypes, - }), + unsecuredSavedObjectsClient, actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex: await kibanaIndex, scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, request, - authorization: instantiateAuthorization(request), + authorization: instantiateAuthorization( + request, + await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient, sourceContext) + ), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ taskManager: plugins.taskManager, @@ -298,8 +315,13 @@ export class ActionsPlugin implements Plugin, Plugi }); }; + // Ensure the public API cannot be used to circumvent authorization + // using our legacy exemption mechanism + const secureGetActionsClientWithRequest = (request: KibanaRequest) => + getActionsClientWithRequest(request); + this.eventLogService!.registerSavedObjectProvider('action', (request) => { - const client = getActionsClientWithRequest(request); + const client = secureGetActionsClientWithRequest(request); return async (type: string, id: string) => (await client).get({ id }); }); @@ -334,10 +356,8 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, getBasePath: this.getBasePath, spaceIdToNamespace: this.spaceIdToNamespace, - getScopedSavedObjectsClient: (request: KibanaRequest) => - core.savedObjects.getScopedClient(request, { - includedHiddenTypes, - }), + getUnsecuredSavedObjectsClient: (request: KibanaRequest) => + this.getUnsecuredSavedObjectsClient(core.savedObjects, request), }); scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); @@ -352,15 +372,29 @@ export class ActionsPlugin implements Plugin, Plugi getActionsAuthorizationWithRequest(request: KibanaRequest) { return instantiateAuthorization(request); }, - getActionsClientWithRequest, + getActionsClientWithRequest: secureGetActionsClientWithRequest, preconfiguredActions, }; } - private instantiateAuthorization = (request: KibanaRequest) => { + private getUnsecuredSavedObjectsClient = ( + savedObjects: CoreStart['savedObjects'], + request: KibanaRequest + ) => + savedObjects.getScopedClient(request, { + excludedWrappers: ['security'], + includedHiddenTypes, + }); + + private instantiateAuthorization = ( + request: KibanaRequest, + shouldLegacyRbacApply: boolean = false + ) => { return new ActionsAuthorization({ request, + shouldLegacyRbacApply, authorization: this.security?.authz, + authentication: this.security?.authc, auditLogger: new ActionsAuthorizationAuditLogger( this.security?.audit.getLogger(ACTIONS_FEATURE.id) ), diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 54f186acc1ba5..afc51ea4842f5 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -9,6 +9,7 @@ import mappings from './mappings.json'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export const ACTION_SAVED_OBJECT_TYPE = 'action'; +export const ALERT_SAVED_OBJECT_TYPE = 'alert'; export const ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE = 'action_task_params'; export function setupSavedObjects( diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index d994269366ae6..058c34f631a3b 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -31,6 +31,7 @@ const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertsAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); +const kibanaVersion = 'v7.10.0'; const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, @@ -46,6 +47,7 @@ const alertsClientParams: jest.Mocked = { encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), getEventLogClient: jest.fn(), + kibanaVersion, }; beforeEach(() => { @@ -424,10 +426,13 @@ describe('create()', () => { expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "scheduledTaskId": "task-123", - } - `); + Object { + "meta": Object { + "versionLastmodified": "v7.10.0", + }, + "scheduledTaskId": "task-123", + } + `); }); test('creates an alert with multiple actions', async () => { @@ -1171,6 +1176,9 @@ describe('enable()', () => { alertTypeId: 'myType', consumer: 'myApp', enabled: true, + meta: { + versionLastmodified: kibanaVersion, + }, updatedBy: 'elastic', apiKey: null, apiKeyOwner: null, @@ -1256,6 +1264,9 @@ describe('enable()', () => { alertTypeId: 'myType', consumer: 'myApp', enabled: true, + meta: { + versionLastmodified: kibanaVersion, + }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', @@ -1421,6 +1432,9 @@ describe('disable()', () => { apiKey: null, apiKeyOwner: null, enabled: false, + meta: { + versionLastmodified: kibanaVersion, + }, scheduledTaskId: null, updatedBy: 'elastic', actions: [ @@ -1461,6 +1475,9 @@ describe('disable()', () => { apiKey: null, apiKeyOwner: null, enabled: false, + meta: { + versionLastmodified: kibanaVersion, + }, scheduledTaskId: null, updatedBy: 'elastic', actions: [ @@ -1569,6 +1586,9 @@ describe('muteAll()', () => { await alertsClient.muteAll({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + meta: { + versionLastmodified: kibanaVersion, + }, muteAll: true, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1654,6 +1674,9 @@ describe('unmuteAll()', () => { await alertsClient.unmuteAll({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + meta: { + versionLastmodified: kibanaVersion, + }, muteAll: false, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1737,6 +1760,9 @@ describe('muteInstance()', () => { 'alert', '1', { + meta: { + versionLastmodified: kibanaVersion, + }, mutedInstanceIds: ['2'], updatedBy: 'elastic', }, @@ -1870,6 +1896,9 @@ describe('unmuteInstance()', () => { 'alert', '1', { + meta: { + versionLastmodified: kibanaVersion, + }, mutedInstanceIds: [], updatedBy: 'elastic', }, @@ -3111,6 +3140,9 @@ describe('update()', () => { "apiKeyOwner": null, "consumer": "myApp", "enabled": true, + "meta": Object { + "versionLastmodified": "v7.10.0", + }, "name": "abc", "params": Object { "bar": true, @@ -3268,6 +3300,9 @@ describe('update()', () => { "apiKeyOwner": "elastic", "consumer": "myApp", "enabled": true, + "meta": Object { + "versionLastmodified": "v7.10.0", + }, "name": "abc", "params": Object { "bar": true, @@ -3419,6 +3454,9 @@ describe('update()', () => { "apiKeyOwner": null, "consumer": "myApp", "enabled": false, + "meta": Object { + "versionLastmodified": "v7.10.0", + }, "name": "abc", "params": Object { "bar": true, @@ -4046,6 +4084,9 @@ describe('updateApiKey()', () => { }, }, ], + meta: { + versionLastmodified: kibanaVersion, + }, }, { version: '123' } ); @@ -4082,6 +4123,9 @@ describe('updateApiKey()', () => { }, }, ], + meta: { + versionLastmodified: kibanaVersion, + }, }, { version: '123' } ); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 4fdf5cde709d6..32635b7673e78 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -12,6 +12,7 @@ import { SavedObjectsClientContract, SavedObjectReference, SavedObject, + PluginInitializerContext, } from 'src/core/server'; import { ActionsClient, ActionsAuthorization } from '../../actions/server'; import { @@ -75,6 +76,7 @@ export interface ConstructorOptions { invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; getActionsClient: () => Promise; getEventLogClient: () => Promise; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; } export interface MuteOptions extends IndexType { @@ -161,7 +163,8 @@ export class AlertsClient { private readonly getActionsClient: () => Promise; private readonly actionsAuthorization: ActionsAuthorization; private readonly getEventLogClient: () => Promise; - encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + private readonly encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + private readonly kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; constructor({ alertTypeRegistry, @@ -178,6 +181,7 @@ export class AlertsClient { getActionsClient, actionsAuthorization, getEventLogClient, + kibanaVersion, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -193,6 +197,7 @@ export class AlertsClient { this.getActionsClient = getActionsClient; this.actionsAuthorization = actionsAuthorization; this.getEventLogClient = getEventLogClient; + this.kibanaVersion = kibanaVersion; } public async create({ data, options }: CreateOptions): Promise { @@ -226,10 +231,14 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], }; - const createdAlert = await this.unsecuredSavedObjectsClient.create('alert', rawAlert, { - ...options, - references, - }); + const createdAlert = await this.unsecuredSavedObjectsClient.create( + 'alert', + this.updateMeta(rawAlert), + { + ...options, + references, + } + ); if (data.enabled) { let scheduledTask; try { @@ -269,20 +278,6 @@ export class AlertsClient { return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } - // public for now, but we won't want to expose this - public async getMeta({ id }: { id: string }): Promise<[SanitizedAlert, AlertMeta | undefined]> { - const result = await this.unsecuredSavedObjectsClient.get('alert', id); - await this.authorization.ensureAuthorized( - result.attributes.alertTypeId, - result.attributes.consumer, - ReadOperations.Get - ); - return [ - this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references), - result.attributes.meta, - ]; - } - public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); await this.authorization.ensureAuthorized( @@ -389,7 +384,7 @@ export class AlertsClient { } public async delete({ id }: { id: string }) { - let taskIdToRemove: string | undefined; + let taskIdToRemove: string | undefined | null; let apiKeyToInvalidate: string | null = null; let attributes: RawAlert; @@ -498,14 +493,14 @@ export class AlertsClient { const updatedObject = await this.unsecuredSavedObjectsClient.update( 'alert', id, - { + this.updateMeta({ ...attributes, ...data, ...apiKeyAttributes, params: validatedAlertTypeParams as RawAlert['params'], actions, updatedBy: username, - }, + }), { version, references, @@ -563,7 +558,7 @@ export class AlertsClient { WriteOperations.UpdateApiKey ); - if (attributes.actions.length) { + if (attributes.actions.length && !this.authorization.shouldUseLegacyAuthorization(attributes)) { await this.actionsAuthorization.ensureAuthorized('execute'); } @@ -571,14 +566,14 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update( 'alert', id, - { + this.updateMeta({ ...attributes, ...this.apiKeyAsAlertAttributes( await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), username ), updatedBy: username, - }, + }), { version } ); @@ -641,7 +636,7 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update( 'alert', id, - { + this.updateMeta({ ...attributes, enabled: true, ...this.apiKeyAsAlertAttributes( @@ -651,7 +646,7 @@ export class AlertsClient { username ), updatedBy: username, - }, + }), { version } ); const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); @@ -697,14 +692,14 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update( 'alert', id, - { + this.updateMeta({ ...attributes, enabled: false, scheduledTaskId: null, apiKey: null, apiKeyOwner: null, updatedBy: await this.getUserName(), - }, + }), { version } ); @@ -729,11 +724,15 @@ export class AlertsClient { await this.actionsAuthorization.ensureAuthorized('execute'); } - await this.unsecuredSavedObjectsClient.update('alert', id, { - muteAll: true, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - }); + await this.unsecuredSavedObjectsClient.update( + 'alert', + id, + this.updateMeta({ + muteAll: true, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }) + ); } public async unmuteAll({ id }: { id: string }) { @@ -748,11 +747,15 @@ export class AlertsClient { await this.actionsAuthorization.ensureAuthorized('execute'); } - await this.unsecuredSavedObjectsClient.update('alert', id, { - muteAll: false, - mutedInstanceIds: [], - updatedBy: await this.getUserName(), - }); + await this.unsecuredSavedObjectsClient.update( + 'alert', + id, + this.updateMeta({ + muteAll: false, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }) + ); } public async muteInstance({ alertId, alertInstanceId }: MuteOptions) { @@ -777,10 +780,10 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update( 'alert', alertId, - { + this.updateMeta({ mutedInstanceIds, updatedBy: await this.getUserName(), - }, + }), { version } ); } @@ -811,11 +814,10 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update( 'alert', alertId, - { + this.updateMeta({ updatedBy: await this.getUserName(), - mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), - }, + }), { version } ); } @@ -875,7 +877,7 @@ export class AlertsClient { private getPartialAlertFromRaw( id: string, - { createdAt, meta, ...rawAlert }: Partial, + { createdAt, meta, scheduledTaskId, ...rawAlert }: Partial, updatedAt: SavedObject['updated_at'] = createdAt, references: SavedObjectReference[] | undefined ): PartialAlert { @@ -890,6 +892,7 @@ export class AlertsClient { : [], ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), ...(createdAt ? { createdAt: new Date(createdAt) } : {}), + ...(scheduledTaskId ? { scheduledTaskId } : {}), }; } @@ -957,6 +960,12 @@ export class AlertsClient { private generateAPIKeyName(alertTypeId: string, alertName: string) { return truncate(`Alerting: ${alertTypeId}/${alertName}`, { length: 256 }); } + + private updateMeta>(alertAttributes: T): T { + alertAttributes.meta = alertAttributes.meta ?? {}; + alertAttributes.meta.versionLastmodified = this.kibanaVersion; + return alertAttributes; + } } function parseDate(dateString: string | undefined, propertyName: string, defaultValue: Date): Date { diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 83202424c9773..eccd810391307 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + KibanaRequest, + Logger, + SavedObjectsServiceStart, + PluginInitializerContext, +} from 'src/core/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server'; import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; @@ -30,6 +35,7 @@ export interface AlertsClientFactoryOpts { actions: ActionsPluginStartContract; features: FeaturesPluginStart; eventLog: IEventLogClientService; + kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; } export class AlertsClientFactory { @@ -45,6 +51,7 @@ export class AlertsClientFactory { private actions!: ActionsPluginStartContract; private features!: FeaturesPluginStart; private eventLog!: IEventLogClientService; + private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -62,6 +69,7 @@ export class AlertsClientFactory { this.actions = options.actions; this.features = options.features; this.eventLog = options.eventLog; + this.kibanaVersion = options.kibanaVersion; } public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { @@ -80,6 +88,7 @@ export class AlertsClientFactory { return new AlertsClient({ spaceId, + kibanaVersion: this.kibanaVersion, logger: this.logger, taskManager: this.taskManager, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts index d7705f834ad41..3728daa946d5b 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts @@ -14,6 +14,7 @@ const createAlertsAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByAlertTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), + shouldUseLegacyAuthorization: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index b2a214eae9316..ab6c537b32dd2 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -8,12 +8,13 @@ import Boom from 'boom'; import { map, mapValues, remove, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; -import { AlertTypeRegistry } from '../types'; +import { AlertTypeRegistry, RawAlert } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; +import { LEGACY_LAST_MODIFIED_VERSION } from '../saved_objects/migrations'; export enum ReadOperations { Get = 'get', @@ -109,6 +110,10 @@ export class AlertsAuthorization { ); } + public shouldUseLegacyAuthorization(alert: RawAlert): boolean { + return alert.meta?.versionLastmodified === LEGACY_LAST_MODIFIED_VERSION; + } + private shouldCheckAuthorization(): boolean { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index d5843bd531d84..d109730e1b08f 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -109,6 +109,7 @@ export class AlertingPlugin { private readonly alertsClientFactory: AlertsClientFactory; private readonly telemetryLogger: Logger; private readonly kibanaIndex: Promise; + private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; @@ -123,6 +124,7 @@ export class AlertingPlugin { map((config: SharedGlobalConfig) => config.kibana.index) ) .toPromise(); + this.kibanaVersion = initializerContext.env.packageInfo.version; } public async setup( @@ -240,6 +242,7 @@ export class AlertingPlugin { actions: plugins.actions, features: plugins.features, eventLog: plugins.eventLog, + kibanaVersion: this.kibanaVersion, }); const getAlertsClientWithRequest = (request: KibanaRequest) => { diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json index ab2140d1f0161..040a7f873c79c 100644 --- a/x-pack/plugins/alerts/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json @@ -79,7 +79,7 @@ }, "meta": { "properties": { - "rbac": { + "versionLastmodified": { "type": "keyword" } } diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 19be72a15972f..5b568a31f2fe3 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -11,6 +11,8 @@ import { import { RawAlert } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; + export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { @@ -51,9 +53,9 @@ function changeAlertingConsumer( attributes: { ...doc.attributes, consumer: mapping.get(consumer) ?? consumer, - // mark any alert predating 7.10 as a legacy alert in terms of RBAC support + // mark any alert predating 7.10 as a legacy alert meta: { - rbac: 'legacy', + versionLastmodified: LEGACY_LAST_MODIFIED_VERSION, }, }, }; diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 677040d8174e3..2f0754d34492f 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -10,6 +10,7 @@ import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { KibanaRequest } from 'kibana/server'; +import { asSavedObjectExecutionSource } from '../../../actions/server'; const alertType: AlertType = { id: 'test', @@ -79,20 +80,27 @@ test('enqueues execution per selected action', async () => { ).toHaveBeenCalledWith(createExecutionHandlerParams.request); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", + Array [ + Object { + "apiKey": "MTIzOmFiYw==", + "id": "1", + "params": Object { + "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", + "contextVal": "My goes here", + "foo": true, + "stateVal": "My goes here", + }, + "source": Object { + "source": Object { "id": "1", - "params": Object { - "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", - "contextVal": "My goes here", - "foo": true, - "stateVal": "My goes here", - }, - "spaceId": "default", + "type": "alert", }, - ] - `); + "type": "SAVED_OBJECT", + }, + "spaceId": "default", + }, + ] + `); const eventLogger = createExecutionHandlerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); @@ -161,6 +169,10 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => contextVal: 'My other goes here', stateVal: 'My other goes here', }, + source: asSavedObjectExecutionSource({ + id: '1', + type: 'alert', + }), spaceId: 'default', apiKey: createExecutionHandlerParams.apiKey, }); @@ -231,20 +243,27 @@ test('context attribute gets parameterized', async () => { }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", + Array [ + Object { + "apiKey": "MTIzOmFiYw==", + "id": "1", + "params": Object { + "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", + "contextVal": "My context-val goes here", + "foo": true, + "stateVal": "My goes here", + }, + "source": Object { + "source": Object { "id": "1", - "params": Object { - "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", - "contextVal": "My context-val goes here", - "foo": true, - "stateVal": "My goes here", - }, - "spaceId": "default", + "type": "alert", }, - ] - `); + "type": "SAVED_OBJECT", + }, + "spaceId": "default", + }, + ] + `); }); test('state attribute gets parameterized', async () => { @@ -257,20 +276,27 @@ test('state attribute gets parameterized', async () => { }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", + Array [ + Object { + "apiKey": "MTIzOmFiYw==", + "id": "1", + "params": Object { + "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", + "contextVal": "My goes here", + "foo": true, + "stateVal": "My state-val goes here", + }, + "source": Object { + "source": Object { "id": "1", - "params": Object { - "alertVal": "My 1 name-of-alert default tag-A,tag-B 2 goes here", - "contextVal": "My goes here", - "foo": true, - "stateVal": "My state-val goes here", - }, - "spaceId": "default", + "type": "alert", }, - ] - `); + "type": "SAVED_OBJECT", + }, + "spaceId": "default", + }, + ] + `); }); test(`logs an error when action group isn't part of actionGroups available for the alertType`, async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index bf074e2c60ee3..f873b0178ece9 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -7,7 +7,10 @@ import { map } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; -import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; +import { + PluginStartContract as ActionsPluginStartContract, + asSavedObjectExecutionSource, +} from '../../../actions/server'; import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { @@ -97,6 +100,10 @@ export function createExecutionHandler({ params: action.params, spaceId, apiKey, + source: asSavedObjectExecutionSource({ + id: alertId, + type: 'alert', + }), }); const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 58b1fa4a123e1..801d30b6406ee 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -211,17 +211,24 @@ describe('Task Runner', () => { await taskRunner.run(); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", - "id": "1", - "params": Object { - "foo": true, - }, - "spaceId": undefined, - }, - ] - `); + Array [ + Object { + "apiKey": "MTIzOmFiYw==", + "id": "1", + "params": Object { + "foo": true, + }, + "source": Object { + "source": Object { + "id": "1", + "type": "alert", + }, + "type": "SAVED_OBJECT", + }, + "spaceId": undefined, + }, + ] + `); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); @@ -351,17 +358,24 @@ describe('Task Runner', () => { }); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "apiKey": "MTIzOmFiYw==", - "id": "1", - "params": Object { - "foo": true, - }, - "spaceId": undefined, - }, - ] - `); + Array [ + Object { + "apiKey": "MTIzOmFiYw==", + "id": "1", + "params": Object { + "foo": true, + }, + "source": Object { + "source": Object { + "id": "1", + "type": "alert", + }, + "type": "SAVED_OBJECT", + }, + "spaceId": undefined, + }, + ] + `); const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 32c43ec2c6a3e..b1fbd3b8a6ae7 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -112,7 +112,7 @@ export interface RawAlertAction extends SavedObjectAttributes { } export interface AlertMeta extends SavedObjectAttributes { - rbac?: string; + versionLastmodified?: string; } export type PartialAlert = Pick & Partial>; @@ -126,7 +126,7 @@ export interface RawAlert extends SavedObjectAttributes { schedule: SavedObjectAttributes; actions: RawAlertAction[]; params: SavedObjectAttributes; - scheduledTaskId?: string; + scheduledTaskId?: string | null; createdBy: string | null; updatedBy: string | null; createdAt: string; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json index 083386480c540..192c4b031a4b3 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["taskManager", "features", "actions", "alerts"], + "requiredPlugins": ["taskManager", "features", "actions", "alerts", "encryptedSavedObjects"], "optionalPlugins": ["spaces"], "server": true, "ui": false diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 1b8a380eaaeb2..3668092dd0b5b 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -12,11 +12,13 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../.. import { defineAlertTypes } from './alert_types'; import { defineActionTypes } from './action_types'; import { defineRoutes } from './routes'; +import { SpacesPluginSetup } from '../../../../../../../plugins/spaces/server'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; alerts: AlertingPluginSetup; + spaces?: SpacesPluginSetup; } export interface FixtureStartDeps { @@ -24,7 +26,10 @@ export interface FixtureStartDeps { } export class FixturePlugin implements Plugin { - public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { + public setup( + core: CoreSetup, + { features, actions, alerts, spaces }: FixtureSetupDeps + ) { features.registerFeature({ id: 'alertsFixture', name: 'Alerts', @@ -97,7 +102,7 @@ export class FixturePlugin implements Plugin, + { spaces }: Partial +) { const router = core.http.createRouter(); router.put( { @@ -54,4 +60,100 @@ export function defineRoutes(core: CoreSetup) { return res.ok({ body: result }); } ); + + router.put( + { + path: '/api/alerts_fixture/{id}/reschedule_task', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + runAt: schema.string(), + }), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const { id } = req.params; + const { runAt } = req.body; + + const [{ savedObjects }] = await core.getStartServices(); + const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { + includedHiddenTypes: ['task', 'alert'], + }); + const alert = await savedObjectsWithTasksAndAlerts.get('alert', id); + const result = await savedObjectsWithTasksAndAlerts.update( + 'task', + alert.attributes.scheduledTaskId!, + { runAt } + ); + return res.ok({ body: result }); + } + ); + + router.put( + { + path: '/api/alerts_fixture/swap_api_keys/from/{apiKeyFromId}/to/{apiKeyToId}', + validate: { + params: schema.object({ + apiKeyFromId: schema.string(), + apiKeyToId: schema.string(), + }), + body: schema.object({ + spaceId: schema.maybe(schema.string()), + }), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const { apiKeyFromId, apiKeyToId } = req.params; + + let namespace: string | undefined; + if (spaces && req.body.spaceId) { + namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); + } + const [{ savedObjects }, { encryptedSavedObjects }] = await core.getStartServices(); + const encryptedSavedObjectsWithAlerts = await encryptedSavedObjects.getClient({ + includedHiddenTypes: ['alert'], + }); + const savedObjectsWithAlerts = await savedObjects.getScopedClient(req, { + excludedWrappers: ['security', 'spaces'], + includedHiddenTypes: ['alert'], + }); + + const [fromAlert, toAlert] = await Promise.all([ + encryptedSavedObjectsWithAlerts.getDecryptedAsInternalUser( + 'alert', + apiKeyFromId, + { + namespace, + } + ), + savedObjectsWithAlerts.get('alert', apiKeyToId, { + namespace, + }), + ]); + + const result = await savedObjectsWithAlerts.update( + 'alert', + apiKeyToId, + { + ...toAlert.attributes, + apiKey: fromAlert.attributes.apiKey, + apiKeyOwner: fromAlert.attributes.apiKeyOwner, + }, + { + namespace, + } + ); + return res.ok({ body: result }); + } + ); } diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index a68f8de39d48e..c51b16b6ed283 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -8,6 +8,7 @@ import { Space, User } from '../types'; import { ObjectRemover } from './object_remover'; import { getUrlPrefix } from './space_test_utils'; import { ES_TEST_INDEX_NAME } from './es_test_index_tool'; +import { getTestAlertData } from './get_test_alert_data'; export interface AlertUtilsOpts { user?: User; @@ -23,6 +24,10 @@ export interface CreateAlertWithActionOpts { overwrites?: Record; reference: string; } +export interface CreateNoopAlertOpts { + objectRemover?: ObjectRemover; + overwrites?: Record; +} interface UpdateAlwaysFiringAction { alertId: string; @@ -265,6 +270,39 @@ export class AlertUtils { } return response; } + + public swapApiKeys(apiKeyFromId: string, apiKeyToId: string) { + let request = this.supertestWithoutAuth + .put(`/api/alerts_fixture/swap_api_keys/from/${apiKeyFromId}/to/${apiKeyToId}`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + request = request.auth(this.user.username, this.user.password); + } + return request.send({ spaceId: this.space.id }); + } + + public async createNoopAlert({ objectRemover, overwrites = {} }: CreateNoopAlertOpts) { + const objRemover = objectRemover || this.objectRemover; + + if (!objRemover) { + throw new Error('objectRemover is required'); + } + + let request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + request = request.auth(this.user.username, this.user.password); + } + const response = await request.send({ + ...getTestAlertData(), + ...overwrites, + }); + if (response.statusCode === 200) { + objRemover.add(this.space.id, response.body.id, 'alert', 'alerts'); + } + return response; + } } export function getConsumerUnauthorizedErrorMessage( diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 45fa075a65978..1f0ff23c59c60 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -25,5 +25,9 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); loadTestFile(require.resolve('./alerts')); + + // note that this test will destroy existing spaces + loadTestFile(require.resolve('./rbac_legacy')); + // loadTestFile(require.resolve('./CREATE_DATA')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts new file mode 100644 index 0000000000000..7b5f83bdc4c14 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../common/lib'; +import { setupSpacesAndUsers } from '..'; + +// eslint-disable-next-line import/no-default-export +export default function alertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const securityService = getService('security'); + const spacesService = getService('spaces'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + + const MIGRATED_ACTION_ID = '9a0a702d-8ae9-4415-a0b7-45d1866644f5'; + const MIGRATED_ALERT_ID = 'afb46baf-77bb-4cb7-a323-5eb7b22c82aa'; + + describe('alerts', () => { + const authorizationIndex = '.kibana-test-authorization'; + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + await esTestIndexTool.destroy(); + await esArchiver.load('alerts_legacy'); + await esTestIndexTool.setup(); + await es.indices.create({ index: authorizationIndex }); + await setupSpacesAndUsers(spacesService, securityService); + }); + beforeEach(async () => {}); + + after(async () => { + await esTestIndexTool.destroy(); + await es.indices.delete({ index: authorizationIndex }); + await esArchiver.unload('alerts_legacy'); + }); + afterEach(async () => { + objectRemover.removeAll(); + }); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + + describe(scenario.id, () => { + let alertUtils: AlertUtils; + + before(async () => { + alertUtils = new AlertUtils({ + user, + space, + supertestWithoutAuth, + indexRecordActionId: MIGRATED_ACTION_ID, + objectRemover, + }); + }); + + it('should schedule actions on legacy alerts', async () => { + const reference = 'alert:legacy:migrated-to-7.10'; + + const getResponse = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${MIGRATED_ALERT_ID}`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + expect(getResponse.statusCode).to.eql(403); + // expect(createNoopAlertResponse.statusCode).to.eql(403); + break; + case 'space_1_all at space2': + expect(getResponse.statusCode).to.eql(404); + // expect(createNoopAlertResponse.statusCode).to.eql(403); + break; + case 'global_read at space1': + case 'space_1_all at space1': + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(getResponse.status).to.eql(200); + break; + case 'space_1_all_alerts_none_actions at space1': + expect(getResponse.status).to.eql(200); + + const createNoopAlertResponse = await alertUtils.createNoopAlert({}); + + // swap out api key to run as user with no Actions privileges + const swapResponse = await alertUtils.swapApiKeys( + createNoopAlertResponse.body.id, + MIGRATED_ALERT_ID + ); + expect(swapResponse.statusCode).to.eql(200); + // ensure the alert is till marked as legacy despite the update of the Api key + // this is important as proper update *should* update the legacy status of the alert + // and we want to ensure we don't accidentally introduce a change that might break our support of legacy alerts + expect(swapResponse.body.id).to.eql(MIGRATED_ALERT_ID); + expect(swapResponse.body.attributes.meta.versionLastmodified).to.eql('pre-7.10.0'); + + // loading the archive likely caused the task to fail so ensure it's rescheduled to run in 10 seconds, + // otherwise this test will stall for 5 minutes + // no other attributes are touched, only runAt, so unless it would have ran when runAt expired, it + // won't run now + await supertest + .put( + `${getUrlPrefix( + space.id + )}/api/alerts_fixture/${MIGRATED_ALERT_ID}/reschedule_task` + ) + .set('kbn-xsrf', 'foo') + .send({ + runAt: getRunAt(10000), + }) + .expect(200); + + // ensure the alert still runs and that it can schedule actions + const numberOfAlertExecutions = ( + await esTestIndexTool.search('alert:test.always-firing', reference) + ).hits.total.value; + + const numberOfActionExecutions = ( + await esTestIndexTool.search('action:test.index-record', reference) + ).hits.total.value; + + // wait for alert to execute and for its action to be scheduled and run + await retry.try(async () => { + const alertSearchResult = await esTestIndexTool.search( + 'alert:test.always-firing', + reference + ); + + const actionSearchResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + + expect(alertSearchResult.hits.total.value).to.be.greaterThan( + numberOfAlertExecutions + ); + expect(actionSearchResult.hits.total.value).to.be.greaterThan( + numberOfActionExecutions + ); + }); + + // update the alert as super user so that it is no longer a legacy alert + await alertUtils.updateAlwaysFiringAction({ + alertId: MIGRATED_ALERT_ID, + actionId: MIGRATED_ACTION_ID, + user: Superuser, + reference: alertUtils.generateReference(), + overwrites: { + name: 'Updated Alert', + }, + }); + + // attempt to update alert as user with no Actions privileges - as it is no longer a legacy alert + // this should fail, as the user doesn't have the `execute` privilege for Actions + const updatedKeyResponse = await alertUtils.getUpdateApiKeyRequest(MIGRATED_ALERT_ID); + + expect(updatedKeyResponse.statusCode).to.eql(403); + expect(updatedKeyResponse.body).to.eql({ + error: 'Forbidden', + message: 'Unauthorized to execute actions', + statusCode: 403, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} + +function getRunAt(delayInMs: number) { + const runAt = new Date(); + runAt.setMilliseconds(new Date().getMilliseconds() + delayInMs); + return runAt.toISOString(); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index f544ef2d771eb..7daa223dc2d43 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -8,6 +8,47 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { isCustomRoleSpecification } from '../../common/types'; import { Spaces, Users } from '../scenarios'; +export async function setupSpacesAndUsers( + spacesService: ReturnType, + securityService: ReturnType +) { + for (const space of Spaces) { + await spacesService.create(space); + } + + for (const user of Users) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + + await securityService.user.create(user.username, { + password: user.password, + full_name: user.fullName, + roles: roles.map((role) => role.name), + }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + elasticsearch: role.elasticsearch, + }); + } + } + } +} + +export async function tearDownUsers(securityService: ReturnType) { + for (const user of Users) { + await securityService.user.delete(user.username); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } + } + } +} + // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile, @@ -21,41 +62,11 @@ export default function alertingApiIntegrationTests({ this.tags('ciGroup5'); before(async () => { - for (const space of Spaces) { - await spacesService.create(space); - } - - for (const user of Users) { - const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; - - await securityService.user.create(user.username, { - password: user.password, - full_name: user.fullName, - roles: roles.map((role) => role.name), - }); - - for (const role of roles) { - if (isCustomRoleSpecification(role)) { - await securityService.role.create(role.name, { - kibana: role.kibana, - elasticsearch: role.elasticsearch, - }); - } - } - } + await setupSpacesAndUsers(spacesService, securityService); }); after(async () => { - for (const user of Users) { - await securityService.user.delete(user.username); - - const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; - for (const role of roles) { - if (isCustomRoleSpecification(role)) { - await securityService.role.delete(role.name); - } - } - } + await tearDownUsers(securityService); await esArchiver.unload('empty_kibana'); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index fa14951dcb6c9..81f7c8c97ba8c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { getUrlPrefix, getTestAlertData } from '../../../common/lib'; +import { getUrlPrefix } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/functional/es_archives/alerts_legacy/data.json b/x-pack/test/functional/es_archives/alerts_legacy/data.json new file mode 100644 index 0000000000000..cb2a10a630ef0 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts_legacy/data.json @@ -0,0 +1,116 @@ +{ + "type": "doc", + "value": { + "id": "space1:action:9a0a702d-8ae9-4415-a0b7-45d1866644f5", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": "test.index-record", + "config": { + "unencrypted": "This value shouldn't get encrypted" + }, + "name": "My action", + "secrets": "s01+Uuh+Y/DC+2qMiuZ83eBNeWmNoEQXuB8m72k5hhY0sMmiuQXZEopkg/Go3uHZu5Hpv9qgpbk/QZ7FJ6m5p4p9oH5HYN8jFoxTERjMtOwCfoiT1LMC9pG6hdixoa6sxNxkE4SpaJMGhFgz1JLv1aYEiiXOO2nvqPs2HdsoGOZbrOWzoZufX3b7" + }, + "namespace": "space1", + "references": [ + ], + "type": "action", + "updated_at": "2020-08-21T18:42:40.534Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space1:alert:afb46baf-77bb-4cb7-a323-5eb7b22c82aa", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": "test.index-record", + "group": "default", + "params": { + "index": ".kibana-alerting-test-data", + "message": "alertId: {{alertId}},\nalertName: {{alertName}},\nspaceId: {{spaceId}},\ntags: {{tags}},\nalertInstanceId: {{alertInstanceId}},\ninstanceContextValue: {{context.instanceContextValue}},\ninstanceStateValue: {{state.instanceStateValue}}", + "reference": "alert:legacy:migrated-to-7.10" + } + } + ], + "alertTypeId": "test.always-firing", + "apiKey": "Z+YEHldPc6a9zBnZ9aVzSL/PMBGI7YD44qNTNJHJZa+zT9uQMhJ9L0LWXiMh6T651ccOF4bY0inxybmx9UOSLxupGaf3OEbjJZId1wMyRi7gNsMuNUYdikQZaehuumt83Cwqx1VsFRyWKnPSPpuKtly2PefcQtWUaHOW8d3OJKgC+TKx/aIWyXi97hyaSzh2OQbdmDVhPZ5oSw==", + "apiKeyOwner": "superuser", + "consumer": "alertsFixture", + "createdAt": "2020-08-21T18:42:41.022Z", + "createdBy": "superuser", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + "index": ".kibana-alerting-test-data", + "reference": "alert:legacy:migrated-to-7.10" + }, + "schedule": { + "interval": "10s" + }, + "scheduledTaskId": "18317240-e3de-11ea-bd7f-a36a36a9a646", + "tags": [ + "tag-A", + "tag-B" + ], + "throttle": "10s", + "updatedBy": "superuser" + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "namespace": "space1", + "references": [ + { + "id": "9a0a702d-8ae9-4415-a0b7-45d1866644f5", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-08-21T18:42:41.665Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:18317240-e3de-11ea-bd7f-a36a36a9a646", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 1, + "ownerId": "kibana:5b2de169-2785-441b-ae8c-186a1936b17d", + "params": "{\"alertId\":\"afb46baf-77bb-4cb7-a323-5eb7b22c82aa\",\"spaceId\":\"space1\"}", + "retryAt": "2020-08-21T18:52:42.845Z", + "runAt": "2020-08-21T18:42:41.636Z", + "scheduledAt": "2020-08-21T18:42:41.636Z", + "scope": [ + "alerting" + ], + "startedAt": "2020-08-21T18:42:42.845Z", + "state": "{\"previousStartedAt\":null,\"alertTypeState\":{},\"alertInstances\":{}}", + "status": "idle", + "taskType": "alerting:test.always-firing" + }, + "type": "task", + "updated_at": "2020-08-21T18:42:42.897Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/alerts_legacy/mappings.json b/x-pack/test/functional/es_archives/alerts_legacy/mappings.json new file mode 100644 index 0000000000000..68a60bec854c9 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts_legacy/mappings.json @@ -0,0 +1,2680 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "ee7356e3d77d357fe62a10350eed4b3c", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "74eb4b909f81222fa1ddeaba2881a37e", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "epm-packages": "8f6e0b09ea0374c4ffe98c3755373cff", + "exception-list": "497afa2f881a675d72d58e20057f3d8b", + "exception-list-agnostic": "497afa2f881a675d72d58e20057f3d8b", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "6012d61d15e72564e47fc3402332756e", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8aa988c376e65443fefc26f1075e93a3", + "ingest-package-policies": "f74dfe498e1849267cda41580b2be110", + "ingest_manager_settings": "012cf278ec84579495110bb827d1ed09", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "7f9e077078cab612f6a58e3bfdedb71a", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "94bc38c7a421d15fbfe8ea565370a421", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "44d6bd48a1a653bcb60ea01614b9e3c9", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "policy_id": { + "type": "keyword" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "inventoryDefaultView": { + "type": "keyword" + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_url": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "source": { + "type": "keyword" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "task": "235412e52d09e7165fac8a67a43ad6b4", + "type": "2f4316de49999235636386fe51dc06c1", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0" + } + }, + "dynamic": "strict", + "properties": { + "migrationVersion": { + "dynamic": "true", + "properties": { + "task": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 6f7f628c0442bb29c3fcd1096e6310d1e30144bd Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 4 Sep 2020 18:12:51 +0100 Subject: [PATCH 04/18] test legacy for superuser and none actions user --- .../server/lib/task_runner_factory.test.ts | 4 +- .../alerts/server/alerts_client.test.ts | 12 +- x-pack/plugins/alerts/server/alerts_client.ts | 1 - .../server/alerts_client_factory.test.ts | 3 + .../tests/alerting/index.ts | 1 - .../tests/alerting/rbac_legacy.ts | 223 +++++++++++------- .../es_archives/alerts_legacy/data.json | 137 +++++++++-- 7 files changed, 263 insertions(+), 118 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 78522682054e1..18cbd9f9c5fad 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -71,13 +71,13 @@ const taskRunnerFactoryInitializerParams = { logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient, getBasePath: jest.fn().mockReturnValue(undefined), - getScopedSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient), + getUnsecuredSavedObjectsClient: jest.fn().mockReturnValue(services.savedObjectsClient), }; beforeEach(() => { jest.resetAllMocks(); actionExecutorInitializerParams.getServices.mockReturnValue(services); - taskRunnerFactoryInitializerParams.getScopedSavedObjectsClient.mockReturnValue( + taskRunnerFactoryInitializerParams.getUnsecuredSavedObjectsClient.mockReturnValue( services.savedObjectsClient ); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index c0de818f58cf6..76e786594a8c1 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -374,6 +374,9 @@ describe('create()', () => { "createdAt": "2019-02-12T21:01:22.479Z", "createdBy": "elastic", "enabled": true, + "meta": Object { + "versionLastmodified": "v7.10.0", + }, "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", @@ -427,9 +430,6 @@ describe('create()', () => { expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { - "meta": Object { - "versionLastmodified": "v7.10.0", - }, "scheduledTaskId": "task-123", } `); @@ -1002,6 +1002,9 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', enabled: true, + meta: { + versionLastmodified: 'v7.10.0', + }, schedule: { interval: '10s' }, throttle: null, muteAll: false, @@ -1115,6 +1118,9 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', enabled: false, + meta: { + versionLastmodified: 'v7.10.0', + }, schedule: { interval: '10s' }, throttle: null, muteAll: false, diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 4e95d2738ee56..6906de48386c0 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -26,7 +26,6 @@ import { SanitizedAlert, AlertTaskState, AlertStatus, - AlertMeta, } from './types'; import { validateAlertTypeParams } from './lib'; import { diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 99e81344715a5..ac91d689798c9 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -44,6 +44,7 @@ const alertsClientFactoryParams: jest.Mocked = { actions: actionsMock.createStart(), features, eventLog: eventLogMock.createStart(), + kibanaVersion: '7.10.0', }; const fakeRequest = ({ app: {}, @@ -126,6 +127,7 @@ test('creates an alerts client with proper constructor arguments when security i createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, + kibanaVersion: '7.10.0', }); }); @@ -169,6 +171,7 @@ test('creates an alerts client with proper constructor arguments', async () => { encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), + kibanaVersion: '7.10.0', }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 1f0ff23c59c60..9dbfebff14720 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -28,6 +28,5 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { // note that this test will destroy existing spaces loadTestFile(require.resolve('./rbac_legacy')); - // loadTestFile(require.resolve('./CREATE_DATA')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 7b5f83bdc4c14..4d9b3456fd10a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -21,8 +21,13 @@ export default function alertTests({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esTestIndexTool = new ESTestIndexTool(es, retry); - const MIGRATED_ACTION_ID = '9a0a702d-8ae9-4415-a0b7-45d1866644f5'; - const MIGRATED_ALERT_ID = 'afb46baf-77bb-4cb7-a323-5eb7b22c82aa'; + const MIGRATED_ACTION_ID = '17f38826-5a8d-4a76-975a-b496e7fffe0b'; + const MIGRATED_ALERT_ID: Record = { + space_1_all_alerts_none_actions: '6ee9630a-a20e-44af-9465-217a3717d2ab', + // space_1_all_with_restricted_fixture: '5cc59319-74ee-4edc-8646-a79ea91067cd', + // space_1_all: 'd41a6abb-b93b-46df-a80a-926221ea847c', + superuser: 'b384be60-ec53-4b26-857e-0253ee55b277', + }; describe('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; @@ -38,13 +43,12 @@ export default function alertTests({ getService }: FtrProviderContext) { beforeEach(async () => {}); after(async () => { + objectRemover.removeAll(); await esTestIndexTool.destroy(); await es.indices.delete({ index: authorizationIndex }); await esArchiver.unload('alerts_legacy'); }); - afterEach(async () => { - objectRemover.removeAll(); - }); + afterEach(async () => {}); for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; @@ -63,106 +67,54 @@ export default function alertTests({ getService }: FtrProviderContext) { }); it('should schedule actions on legacy alerts', async () => { - const reference = 'alert:legacy:migrated-to-7.10'; - - const getResponse = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${MIGRATED_ALERT_ID}`) - .auth(user.username, user.password); + const reference = `alert:migrated-to-7.10:${user.username}`; + const migratedAlertId = MIGRATED_ALERT_ID[user.username]; switch (scenario.id) { case 'no_kibana_privileges at space1': - expect(getResponse.statusCode).to.eql(403); - // expect(createNoopAlertResponse.statusCode).to.eql(403); - break; case 'space_1_all at space2': - expect(getResponse.statusCode).to.eql(404); - // expect(createNoopAlertResponse.statusCode).to.eql(403); - break; case 'global_read at space1': + // These cases are not relevant as we're testing the migration of alerts which + // were valid pre 7.10.0 and which become invalid after the introduction of RBAC in 7.10.0 + break; case 'space_1_all at space1': - case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(getResponse.status).to.eql(200); + break; + case 'superuser at space1': + await ensureLegacyAlertHasBeenMigrated(migratedAlertId); + + await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); + + await ensureAlertIsRunning(); + + await updateAlertSoThatItIsNoLongerLegacy(migratedAlertId); + + // console.log( + // 'update alert as user with privileges - so it is no longer a legacy alert' + // ); + // update alert as user with privileges - so it is no longer a legacy alert + const updatedKeyResponse = await alertUtils.getUpdateApiKeyRequest(migratedAlertId); + expect(updatedKeyResponse.statusCode).to.eql(204); + + await ensureAlertIsRunning(); break; case 'space_1_all_alerts_none_actions at space1': - expect(getResponse.status).to.eql(200); + await ensureLegacyAlertHasBeenMigrated(migratedAlertId); - const createNoopAlertResponse = await alertUtils.createNoopAlert({}); + await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); - // swap out api key to run as user with no Actions privileges - const swapResponse = await alertUtils.swapApiKeys( - createNoopAlertResponse.body.id, - MIGRATED_ALERT_ID - ); - expect(swapResponse.statusCode).to.eql(200); - // ensure the alert is till marked as legacy despite the update of the Api key - // this is important as proper update *should* update the legacy status of the alert - // and we want to ensure we don't accidentally introduce a change that might break our support of legacy alerts - expect(swapResponse.body.id).to.eql(MIGRATED_ALERT_ID); - expect(swapResponse.body.attributes.meta.versionLastmodified).to.eql('pre-7.10.0'); - - // loading the archive likely caused the task to fail so ensure it's rescheduled to run in 10 seconds, - // otherwise this test will stall for 5 minutes - // no other attributes are touched, only runAt, so unless it would have ran when runAt expired, it - // won't run now - await supertest - .put( - `${getUrlPrefix( - space.id - )}/api/alerts_fixture/${MIGRATED_ALERT_ID}/reschedule_task` - ) - .set('kbn-xsrf', 'foo') - .send({ - runAt: getRunAt(10000), - }) - .expect(200); - - // ensure the alert still runs and that it can schedule actions - const numberOfAlertExecutions = ( - await esTestIndexTool.search('alert:test.always-firing', reference) - ).hits.total.value; - - const numberOfActionExecutions = ( - await esTestIndexTool.search('action:test.index-record', reference) - ).hits.total.value; - - // wait for alert to execute and for its action to be scheduled and run - await retry.try(async () => { - const alertSearchResult = await esTestIndexTool.search( - 'alert:test.always-firing', - reference - ); - - const actionSearchResult = await esTestIndexTool.search( - 'action:test.index-record', - reference - ); - - expect(alertSearchResult.hits.total.value).to.be.greaterThan( - numberOfAlertExecutions - ); - expect(actionSearchResult.hits.total.value).to.be.greaterThan( - numberOfActionExecutions - ); - }); + await ensureAlertIsRunning(); - // update the alert as super user so that it is no longer a legacy alert - await alertUtils.updateAlwaysFiringAction({ - alertId: MIGRATED_ALERT_ID, - actionId: MIGRATED_ACTION_ID, - user: Superuser, - reference: alertUtils.generateReference(), - overwrites: { - name: 'Updated Alert', - }, - }); + await updateAlertSoThatItIsNoLongerLegacy(migratedAlertId); // attempt to update alert as user with no Actions privileges - as it is no longer a legacy alert // this should fail, as the user doesn't have the `execute` privilege for Actions - const updatedKeyResponse = await alertUtils.getUpdateApiKeyRequest(MIGRATED_ALERT_ID); + const failedUpdateKeyResponse = await alertUtils.getUpdateApiKeyRequest( + migratedAlertId + ); - expect(updatedKeyResponse.statusCode).to.eql(403); - expect(updatedKeyResponse.body).to.eql({ + expect(failedUpdateKeyResponse.statusCode).to.eql(403); + expect(failedUpdateKeyResponse.body).to.eql({ error: 'Forbidden', message: 'Unauthorized to execute actions', statusCode: 403, @@ -171,6 +123,99 @@ export default function alertTests({ getService }: FtrProviderContext) { default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } + + async function ensureLegacyAlertHasBeenMigrated(alertId: string) { + // console.log(`ensureLegacyAlertHasBeenMigrated(${alertId})`); + const getResponse = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`) + .auth(user.username, user.password); + expect(getResponse.status).to.eql(200); + } + + async function updateMigratedAlertToUseApiKeyOfCurrentUser(alertId: string) { + // console.log(`updateMigratedAlertToUseApiKeyOfCurrentUser(${alertId})`); + const createNoopAlertResponse = await alertUtils.createNoopAlert({}); + + // swap out api key to run as user with no Actions privileges + const swapResponse = await alertUtils.swapApiKeys( + createNoopAlertResponse.body.id, + alertId + ); + expect(swapResponse.statusCode).to.eql(200); + // ensure the alert is till marked as legacy despite the update of the Api key + // this is important as proper update *should* update the legacy status of the alert + // and we want to ensure we don't accidentally introduce a change that might break our support of legacy alerts + expect(swapResponse.body.id).to.eql(alertId); + expect(swapResponse.body.attributes.meta.versionLastmodified).to.eql('pre-7.10.0'); + + // loading the archive likely caused the task to fail so ensure it's rescheduled to run in 2 seconds, + // otherwise this test will stall for 5 minutes + // no other attributes are touched, only runAt, so unless it would have ran when runAt expired, it + // won't run now + const taskRescheduleResult = await supertest + .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reschedule_task`) + .set('kbn-xsrf', 'foo') + .send({ + runAt: getRunAt(2000), + }) + .expect(200); + + // console.log(JSON.stringify(taskRescheduleResult.body)); + } + + async function ensureAlertIsRunning() { + // console.log(`ensureAlertIsRunning(${reference})`); + // ensure the alert still runs and that it can schedule actions + const numberOfAlertExecutions = ( + await esTestIndexTool.search('alert:test.always-firing', reference) + ).hits.total.value; + + const numberOfActionExecutions = ( + await esTestIndexTool.search('action:test.index-record', reference) + ).hits.total.value; + + // wait for alert to execute and for its action to be scheduled and run + await retry.try(async () => { + const alertSearchResult = await esTestIndexTool.search( + 'alert:test.always-firing', + reference + ); + + const actionSearchResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + + expect(alertSearchResult.hits.total.value).to.be.greaterThan(numberOfAlertExecutions); + expect(actionSearchResult.hits.total.value).to.be.greaterThan( + numberOfActionExecutions + ); + }); + } + + async function updateAlertSoThatItIsNoLongerLegacy(alertId: string) { + // console.log(`updateAlertSoThatItIsNoLongerLegacy(${alertId})`); + // update the alert as super user (to avoid privilege limitations) so that it is no longer a legacy alert + await alertUtils.updateAlwaysFiringAction({ + alertId, + actionId: MIGRATED_ACTION_ID, + user: Superuser, + reference, + overwrites: { + name: 'Updated Alert', + schedule: { interval: '2s' }, + throttle: '2s', + }, + }); + // return supertest + // .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`) + // .set('kbn-xsrf', 'foo') + // .auth(Superuser.username, Superuser.password) + // .send({ + // name: 'Updated Alert', + // }) + // .expect(200); + } }); }); } diff --git a/x-pack/test/functional/es_archives/alerts_legacy/data.json b/x-pack/test/functional/es_archives/alerts_legacy/data.json index cb2a10a630ef0..d61b7f0e7db68 100644 --- a/x-pack/test/functional/es_archives/alerts_legacy/data.json +++ b/x-pack/test/functional/es_archives/alerts_legacy/data.json @@ -1,7 +1,7 @@ { "type": "doc", "value": { - "id": "space1:action:9a0a702d-8ae9-4415-a0b7-45d1866644f5", + "id": "space1:action:17f38826-5a8d-4a76-975a-b496e7fffe0b", "index": ".kibana_1", "source": { "action": { @@ -10,13 +10,13 @@ "unencrypted": "This value shouldn't get encrypted" }, "name": "My action", - "secrets": "s01+Uuh+Y/DC+2qMiuZ83eBNeWmNoEQXuB8m72k5hhY0sMmiuQXZEopkg/Go3uHZu5Hpv9qgpbk/QZ7FJ6m5p4p9oH5HYN8jFoxTERjMtOwCfoiT1LMC9pG6hdixoa6sxNxkE4SpaJMGhFgz1JLv1aYEiiXOO2nvqPs2HdsoGOZbrOWzoZufX3b7" + "secrets": "/ZW1x3z3KWzPB0H3ociZNVBeWJGQytz2P5OEUCmz4ahO7S8709DDJ+uC9ztGRig7e73GtCgT6XU42Cn8l8rm90INusLWf2oXaXbdGctjMDtTXv5S3b75YdEwIHClhLTLswmraGQHUwMsTMe+EVh9gUhwGsjDLTQAiDqsDonMto8XkTdtyP6C5axn" }, "namespace": "space1", "references": [ ], "type": "action", - "updated_at": "2020-08-21T18:42:40.534Z" + "updated_at": "2020-09-04T11:50:50.815Z" } } } @@ -24,7 +24,7 @@ { "type": "doc", "value": { - "id": "space1:alert:afb46baf-77bb-4cb7-a323-5eb7b22c82aa", + "id": "space1:alert:6ee9630a-a20e-44af-9465-217a3717d2ab", "index": ".kibana_1", "source": { "alert": { @@ -36,15 +36,15 @@ "params": { "index": ".kibana-alerting-test-data", "message": "alertId: {{alertId}},\nalertName: {{alertName}},\nspaceId: {{spaceId}},\ntags: {{tags}},\nalertInstanceId: {{alertInstanceId}},\ninstanceContextValue: {{context.instanceContextValue}},\ninstanceStateValue: {{state.instanceStateValue}}", - "reference": "alert:legacy:migrated-to-7.10" + "reference": "alert:migrated-to-7.10:space_1_all_alerts_none_actions" } } ], "alertTypeId": "test.always-firing", - "apiKey": "Z+YEHldPc6a9zBnZ9aVzSL/PMBGI7YD44qNTNJHJZa+zT9uQMhJ9L0LWXiMh6T651ccOF4bY0inxybmx9UOSLxupGaf3OEbjJZId1wMyRi7gNsMuNUYdikQZaehuumt83Cwqx1VsFRyWKnPSPpuKtly2PefcQtWUaHOW8d3OJKgC+TKx/aIWyXi97hyaSzh2OQbdmDVhPZ5oSw==", + "apiKey": "5kB2ao8TYvwpj+CGsgnAmWvzvuPCt9Wu6q4d4Y56rTKbqO352RjiRG8dbvEzKcRveiMjfKYFOXs6dlh8cMIRBrBnF7z19+A1sq+RhrvMiqXFxbN+udBEwtUXNjgoaFH5Ajvm0t5Yg3f4XSGhaWc/n2URo6eXq/OkVsp/xjHs1rfGow5dojDEtotkFuvzPLstH2mBImYLwwejvw==", "apiKeyOwner": "superuser", "consumer": "alertsFixture", - "createdAt": "2020-08-21T18:42:41.022Z", + "createdAt": "2020-09-04T11:51:04.584Z", "createdBy": "superuser", "enabled": true, "muteAll": false, @@ -53,17 +53,17 @@ "name": "abc", "params": { "index": ".kibana-alerting-test-data", - "reference": "alert:legacy:migrated-to-7.10" + "reference": "alert:migrated-to-7.10:space_1_all_alerts_none_actions" }, "schedule": { - "interval": "10s" + "interval": "2s" }, - "scheduledTaskId": "18317240-e3de-11ea-bd7f-a36a36a9a646", + "scheduledTaskId": "e9c069d0-eea4-11ea-a285-352ee3aecffa", "tags": [ "tag-A", "tag-B" ], - "throttle": "10s", + "throttle": "2s", "updatedBy": "superuser" }, "migrationVersion": { @@ -72,13 +72,13 @@ "namespace": "space1", "references": [ { - "id": "9a0a702d-8ae9-4415-a0b7-45d1866644f5", + "id": "17f38826-5a8d-4a76-975a-b496e7fffe0b", "name": "action_0", "type": "action" } ], "type": "alert", - "updated_at": "2020-08-21T18:42:41.665Z" + "updated_at": "2020-09-04T11:51:05.222Z" } } } @@ -86,7 +86,7 @@ { "type": "doc", "value": { - "id": "task:18317240-e3de-11ea-bd7f-a36a36a9a646", + "id": "task:e9c069d0-eea4-11ea-a285-352ee3aecffa", "index": ".kibana_task_manager_1", "source": { "migrationVersion": { @@ -95,22 +95,115 @@ "references": [ ], "task": { - "attempts": 1, - "ownerId": "kibana:5b2de169-2785-441b-ae8c-186a1936b17d", - "params": "{\"alertId\":\"afb46baf-77bb-4cb7-a323-5eb7b22c82aa\",\"spaceId\":\"space1\"}", - "retryAt": "2020-08-21T18:52:42.845Z", - "runAt": "2020-08-21T18:42:41.636Z", - "scheduledAt": "2020-08-21T18:42:41.636Z", + "attempts": 0, + "params": "{\"alertId\":\"6ee9630a-a20e-44af-9465-217a3717d2ab\",\"spaceId\":\"space1\"}", + "retryAt": null, + "runAt": "2020-09-04T11:51:05.197Z", + "scheduledAt": "2020-09-04T11:51:05.197Z", "scope": [ "alerting" ], - "startedAt": "2020-08-21T18:42:42.845Z", + "startedAt": null, "state": "{\"previousStartedAt\":null,\"alertTypeState\":{},\"alertInstances\":{}}", "status": "idle", "taskType": "alerting:test.always-firing" }, "type": "task", - "updated_at": "2020-08-21T18:42:42.897Z" + "updated_at": "2020-09-04T11:51:05.197Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space1:alert:b384be60-ec53-4b26-857e-0253ee55b277", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": "test.index-record", + "group": "default", + "params": { + "index": ".kibana-alerting-test-data", + "message": "alertId: {{alertId}},\nalertName: {{alertName}},\nspaceId: {{spaceId}},\ntags: {{tags}},\nalertInstanceId: {{alertInstanceId}},\ninstanceContextValue: {{context.instanceContextValue}},\ninstanceStateValue: {{state.instanceStateValue}}", + "reference": "alert:migrated-to-7.10:superuser" + } + } + ], + "alertTypeId": "test.always-firing", + "apiKey": "Aj+qLwjCZ/WUeMUmLbe9xsf3/O4YAGGYg063WHGoPdftE7WKRxhn45ZUGu4SZ45BAloAsX113sCjIjhSwyVFTHG41lv0sGDLheviV/FwxmNnUDwX8Bn5PcbFw6n+eRsgBgXXC1qgNjhd1SfuEPd1BfmtjT58NLPN6G5NMKRAeWA7Tp5U25Aw6JYaPXwyFV5sfNhWi21pLs2fJQ==", + "apiKeyOwner": "superuser", + "consumer": "alertsFixture", + "createdAt": "2020-09-04T11:50:54.493Z", + "createdBy": "superuser", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + "index": ".kibana-alerting-test-data", + "reference": "alert:migrated-to-7.10:superuser" + }, + "schedule": { + "interval": "2s" + }, + "scheduledTaskId": "e39a02f0-eea4-11ea-a285-352ee3aecffa", + "tags": [ + "tag-A", + "tag-B" + ], + "throttle": "2s", + "updatedBy": "superuser" + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "namespace": "space1", + "references": [ + { + "id": "17f38826-5a8d-4a76-975a-b496e7fffe0b", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-09-04T11:50:54.902Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:e39a02f0-eea4-11ea-a285-352ee3aecffa", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "ownerId": null, + "params": "{\"alertId\":\"b384be60-ec53-4b26-857e-0253ee55b277\",\"spaceId\":\"space1\"}", + "retryAt": null, + "runAt": "2020-09-04T11:51:04.804Z", + "scheduledAt": "2020-09-04T11:50:54.879Z", + "scope": [ + "alerting" + ], + "startedAt": null, + "state": "{\"previousStartedAt\":null,\"alertTypeState\":{},\"alertInstances\":{}}", + "status": "idle", + "taskType": "alerting:test.always-firing" + }, + "type": "task", + "updated_at": "2020-09-04T11:51:04.273Z" } } } \ No newline at end of file From b226e32d96016773b44dd6ac41099639573e0b8a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 4 Sep 2020 18:44:26 +0100 Subject: [PATCH 05/18] test legacy for space_1_all and restricted users --- .../tests/alerting/index.ts | 1 + .../tests/alerting/rbac_legacy.ts | 8 +- .../es_archives/alerts_legacy/data.json | 188 ++++++++++++++++++ 3 files changed, 193 insertions(+), 4 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 9dbfebff14720..1f0ff23c59c60 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -28,5 +28,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { // note that this test will destroy existing spaces loadTestFile(require.resolve('./rbac_legacy')); + // loadTestFile(require.resolve('./CREATE_DATA')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 4d9b3456fd10a..0f5fd1e045b29 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -24,8 +24,8 @@ export default function alertTests({ getService }: FtrProviderContext) { const MIGRATED_ACTION_ID = '17f38826-5a8d-4a76-975a-b496e7fffe0b'; const MIGRATED_ALERT_ID: Record = { space_1_all_alerts_none_actions: '6ee9630a-a20e-44af-9465-217a3717d2ab', - // space_1_all_with_restricted_fixture: '5cc59319-74ee-4edc-8646-a79ea91067cd', - // space_1_all: 'd41a6abb-b93b-46df-a80a-926221ea847c', + space_1_all_with_restricted_fixture: '5cc59319-74ee-4edc-8646-a79ea91067cd', + space_1_all: 'd41a6abb-b93b-46df-a80a-926221ea847c', superuser: 'b384be60-ec53-4b26-857e-0253ee55b277', }; @@ -76,11 +76,11 @@ export default function alertTests({ getService }: FtrProviderContext) { case 'global_read at space1': // These cases are not relevant as we're testing the migration of alerts which // were valid pre 7.10.0 and which become invalid after the introduction of RBAC in 7.10.0 + // these cases were invalid pre 7.10.0 and remain invalid post 7.10.0 break; case 'space_1_all at space1': - case 'space_1_all_with_restricted_fixture at space1': - break; case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); diff --git a/x-pack/test/functional/es_archives/alerts_legacy/data.json b/x-pack/test/functional/es_archives/alerts_legacy/data.json index d61b7f0e7db68..11e5e0cdc3f05 100644 --- a/x-pack/test/functional/es_archives/alerts_legacy/data.json +++ b/x-pack/test/functional/es_archives/alerts_legacy/data.json @@ -206,4 +206,192 @@ "updated_at": "2020-09-04T11:51:04.273Z" } } +} + +{ + "type": "doc", + "value": { + "id": "space1:alert:5cc59319-74ee-4edc-8646-a79ea91067cd", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": "test.index-record", + "group": "default", + "params": { + "index": ".kibana-alerting-test-data", + "message": "alertId: {{alertId}},\nalertName: {{alertName}},\nspaceId: {{spaceId}},\ntags: {{tags}},\nalertInstanceId: {{alertInstanceId}},\ninstanceContextValue: {{context.instanceContextValue}},\ninstanceStateValue: {{state.instanceStateValue}}", + "reference": "alert:migrated-to-7.10:space_1_all_with_restricted_fixture" + } + } + ], + "alertTypeId": "test.always-firing", + "apiKey": "OFpEg7T+jq0nVoGkqHEN5W0fgcEu4LmCXCz6F5EzQ8mgxr2+270/nqv2+zSTnkef7rK2Xz2MDmQsdrRrsAFb5ItFb5YnR/zfqbq0+QBlvgqChKORE1+ggI1oQsYnjDtPWOqMwbUUNGYQXdyUZVdRqeeoYq847NgSo5UBoCJgEZ9ah1gzPA6XYvzbBtJdZ+QgCjfuXsrBSlImhQ==", + "apiKeyOwner": "superuser", + "consumer": "alertsFixture", + "createdAt": "2020-09-04T11:51:02.571Z", + "createdBy": "superuser", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + "index": ".kibana-alerting-test-data", + "reference": "alert:migrated-to-7.10:space_1_all_with_restricted_fixture" + }, + "schedule": { + "interval": "2s" + }, + "scheduledTaskId": "e8885f00-eea4-11ea-a285-352ee3aecffa", + "tags": [ + "tag-A", + "tag-B" + ], + "throttle": "2s", + "updatedBy": "superuser" + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "namespace": "space1", + "references": [ + { + "id": "17f38826-5a8d-4a76-975a-b496e7fffe0b", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-09-04T11:51:03.175Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:e8885f00-eea4-11ea-a285-352ee3aecffa", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 1, + "ownerId": "kibana:5b2de169-2785-441b-ae8c-186a1936b17d", + "params": "{\"alertId\":\"5cc59319-74ee-4edc-8646-a79ea91067cd\",\"spaceId\":\"space1\"}", + "retryAt": "2020-09-04T12:01:05.793Z", + "runAt": "2020-09-04T11:51:03.152Z", + "scheduledAt": "2020-09-04T11:51:03.152Z", + "scope": [ + "alerting" + ], + "startedAt": "2020-09-04T11:51:05.793Z", + "state": "{\"previousStartedAt\":null,\"alertTypeState\":{},\"alertInstances\":{}}", + "status": "running", + "taskType": "alerting:test.always-firing" + }, + "type": "task", + "updated_at": "2020-09-04T11:51:05.794Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space1:alert:d41a6abb-b93b-46df-a80a-926221ea847c", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": "test.index-record", + "group": "default", + "params": { + "index": ".kibana-alerting-test-data", + "message": "alertId: {{alertId}},\nalertName: {{alertName}},\nspaceId: {{spaceId}},\ntags: {{tags}},\nalertInstanceId: {{alertInstanceId}},\ninstanceContextValue: {{context.instanceContextValue}},\ninstanceStateValue: {{state.instanceStateValue}}", + "reference": "alert:migrated-to-7.10:space_1_all" + } + } + ], + "alertTypeId": "test.always-firing", + "apiKey": "5qbDONVFRacd8IukMksh11NZ8OFxSN31sJRgIdRw4GHME2qKqyqczHz/CDANEjKF78M98yc5xSr8vNnco2PDOHTD7YfMdnlrl1/lOdIkJ/IM8YxGBCLZfk5hRx/dD3X84ZtSEZ2hYWjAemFKtD5GpTWtczhkx9jRgUMbPgBzrhyEmDJoHMQQSOEpIJKmQpAcpHGsE5/+SVIcGQ==", + "apiKeyOwner": "superuser", + "consumer": "alertsFixture", + "createdAt": "2020-09-04T11:50:58.533Z", + "createdBy": "superuser", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + "index": ".kibana-alerting-test-data", + "reference": "alert:migrated-to-7.10:space_1_all" + }, + "schedule": { + "interval": "2s" + }, + "scheduledTaskId": "e616c2c0-eea4-11ea-a285-352ee3aecffa", + "tags": [ + "tag-A", + "tag-B" + ], + "throttle": "2s", + "updatedBy": "superuser" + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "namespace": "space1", + "references": [ + { + "id": "17f38826-5a8d-4a76-975a-b496e7fffe0b", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-09-04T11:50:59.083Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:e616c2c0-eea4-11ea-a285-352ee3aecffa", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 1, + "ownerId": "kibana:5b2de169-2785-441b-ae8c-186a1936b17d", + "params": "{\"alertId\":\"d41a6abb-b93b-46df-a80a-926221ea847c\",\"spaceId\":\"space1\"}", + "retryAt": "2020-09-04T12:01:05.793Z", + "runAt": "2020-09-04T11:51:04.804Z", + "scheduledAt": "2020-09-04T11:50:59.052Z", + "scope": [ + "alerting" + ], + "startedAt": "2020-09-04T11:51:05.793Z", + "state": "{\"previousStartedAt\":null,\"alertTypeState\":{},\"alertInstances\":{}}", + "status": "idle", + "taskType": "alerting:test.always-firing" + }, + "type": "task", + "updated_at": "2020-09-04T11:51:05.794Z" + } + } } \ No newline at end of file From 071496501684dff4292a7ac5bf7d49136af9e4eb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 4 Sep 2020 19:52:44 +0100 Subject: [PATCH 06/18] added test for global read user --- .../fixtures/plugins/alerts/kibana.json | 2 +- .../fixtures/plugins/alerts/server/plugin.ts | 6 +- .../fixtures/plugins/alerts/server/routes.ts | 146 ++++++++++-------- .../common/lib/alert_utils.ts | 4 +- .../tests/alerting/index.ts | 1 - .../tests/alerting/rbac_legacy.ts | 62 ++++---- .../es_archives/alerts_legacy/data.json | 94 +++++++++++ 7 files changed, 214 insertions(+), 101 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json index 192c4b031a4b3..d8838133f4057 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["taskManager", "features", "actions", "alerts", "encryptedSavedObjects"], + "requiredPlugins": ["taskManager", "features", "actions", "alerts", "security", "encryptedSavedObjects"], "optionalPlugins": ["spaces"], "server": true, "ui": false diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 3668092dd0b5b..3ffac37aba17e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -13,12 +13,14 @@ import { defineAlertTypes } from './alert_types'; import { defineActionTypes } from './action_types'; import { defineRoutes } from './routes'; import { SpacesPluginSetup } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginSetup } from '../../../../../../../plugins/security/server'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; actions: ActionsPluginSetup; alerts: AlertingPluginSetup; spaces?: SpacesPluginSetup; + security?: SecurityPluginSetup; } export interface FixtureStartDeps { @@ -28,7 +30,7 @@ export interface FixtureStartDeps { export class FixturePlugin implements Plugin { public setup( core: CoreSetup, - { features, actions, alerts, spaces }: FixtureSetupDeps + { features, actions, alerts, spaces, security }: FixtureSetupDeps ) { features.registerFeature({ id: 'alertsFixture', @@ -102,7 +104,7 @@ export class FixturePlugin implements Plugin, - { spaces }: Partial + { spaces, security }: Partial ) { const router = core.http.createRouter(); + router.put( + { + path: '/api/alerts_fixture/{id}/replace_api_key', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + spaceId: schema.maybe(schema.string()), + }), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const { id } = req.params; + + if (!security) { + return res.ok({ + body: {}, + }); + } + + const [{ savedObjects }, { encryptedSavedObjects }] = await core.getStartServices(); + const encryptedSavedObjectsWithAlerts = await encryptedSavedObjects.getClient({ + includedHiddenTypes: ['alert'], + }); + const savedObjectsWithAlerts = await savedObjects.getScopedClient(req, { + excludedWrappers: ['security', 'spaces'], + includedHiddenTypes: ['alert'], + }); + + let namespace: string | undefined; + if (spaces && req.body.spaceId) { + namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); + } + + const user = await security.authc.getCurrentUser(req); + if (!user) { + return res.internalError({}); + } + + // Create an API key using the new grant API - in this case the Kibana system user is creating the + // API key for the user, instead of having the user create it themselves, which requires api_key + // privileges + const createAPIKeyResult = await security.authc.grantAPIKeyAsInternalUser(req, { + name: `alert:migrated-to-7.10:${user.username}`, + role_descriptors: {}, + }); + + if (!createAPIKeyResult) { + return res.internalError({}); + } + + const result = await savedObjectsWithAlerts.update( + 'alert', + id, + { + ...( + await encryptedSavedObjectsWithAlerts.getDecryptedAsInternalUser( + 'alert', + id, + { + namespace, + } + ) + ).attributes, + apiKey: Buffer.from(`${createAPIKeyResult.id}:${createAPIKeyResult.api_key}`).toString( + 'base64' + ), + apiKeyOwner: user.username, + }, + { + namespace, + } + ); + return res.ok({ body: result }); + } + ); + router.put( { path: '/api/alerts_fixture/saved_object/{type}/{id}', @@ -94,66 +176,4 @@ export function defineRoutes( return res.ok({ body: result }); } ); - - router.put( - { - path: '/api/alerts_fixture/swap_api_keys/from/{apiKeyFromId}/to/{apiKeyToId}', - validate: { - params: schema.object({ - apiKeyFromId: schema.string(), - apiKeyToId: schema.string(), - }), - body: schema.object({ - spaceId: schema.maybe(schema.string()), - }), - }, - }, - async ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> => { - const { apiKeyFromId, apiKeyToId } = req.params; - - let namespace: string | undefined; - if (spaces && req.body.spaceId) { - namespace = spaces.spacesService.spaceIdToNamespace(req.body.spaceId); - } - const [{ savedObjects }, { encryptedSavedObjects }] = await core.getStartServices(); - const encryptedSavedObjectsWithAlerts = await encryptedSavedObjects.getClient({ - includedHiddenTypes: ['alert'], - }); - const savedObjectsWithAlerts = await savedObjects.getScopedClient(req, { - excludedWrappers: ['security', 'spaces'], - includedHiddenTypes: ['alert'], - }); - - const [fromAlert, toAlert] = await Promise.all([ - encryptedSavedObjectsWithAlerts.getDecryptedAsInternalUser( - 'alert', - apiKeyFromId, - { - namespace, - } - ), - savedObjectsWithAlerts.get('alert', apiKeyToId, { - namespace, - }), - ]); - - const result = await savedObjectsWithAlerts.update( - 'alert', - apiKeyToId, - { - ...toAlert.attributes, - apiKey: fromAlert.attributes.apiKey, - apiKeyOwner: fromAlert.attributes.apiKeyOwner, - }, - { - namespace, - } - ); - return res.ok({ body: result }); - } - ); } diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 5d8995d662fc2..797769fd64471 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -271,9 +271,9 @@ export class AlertUtils { return response; } - public swapApiKeys(apiKeyFromId: string, apiKeyToId: string) { + public replaceApiKeys(id: string) { let request = this.supertestWithoutAuth - .put(`/api/alerts_fixture/swap_api_keys/from/${apiKeyFromId}/to/${apiKeyToId}`) + .put(`/api/alerts_fixture/${id}/replace_api_key`) .set('kbn-xsrf', 'foo'); if (this.user) { request = request.auth(this.user.username, this.user.password); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 1f0ff23c59c60..9dbfebff14720 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -28,6 +28,5 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { // note that this test will destroy existing spaces loadTestFile(require.resolve('./rbac_legacy')); - // loadTestFile(require.resolve('./CREATE_DATA')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 0f5fd1e045b29..3315255885ca5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -26,6 +26,7 @@ export default function alertTests({ getService }: FtrProviderContext) { space_1_all_alerts_none_actions: '6ee9630a-a20e-44af-9465-217a3717d2ab', space_1_all_with_restricted_fixture: '5cc59319-74ee-4edc-8646-a79ea91067cd', space_1_all: 'd41a6abb-b93b-46df-a80a-926221ea847c', + global_read: '362e362b-a137-4aa2-9434-43e3d0d84a34', superuser: 'b384be60-ec53-4b26-857e-0253ee55b277', }; @@ -40,15 +41,12 @@ export default function alertTests({ getService }: FtrProviderContext) { await es.indices.create({ index: authorizationIndex }); await setupSpacesAndUsers(spacesService, securityService); }); - beforeEach(async () => {}); after(async () => { - objectRemover.removeAll(); await esTestIndexTool.destroy(); await es.indices.delete({ index: authorizationIndex }); await esArchiver.unload('alerts_legacy'); }); - afterEach(async () => {}); for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; @@ -73,7 +71,6 @@ export default function alertTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - case 'global_read at space1': // These cases are not relevant as we're testing the migration of alerts which // were valid pre 7.10.0 and which become invalid after the introduction of RBAC in 7.10.0 // these cases were invalid pre 7.10.0 and remain invalid post 7.10.0 @@ -89,15 +86,35 @@ export default function alertTests({ getService }: FtrProviderContext) { await updateAlertSoThatItIsNoLongerLegacy(migratedAlertId); - // console.log( - // 'update alert as user with privileges - so it is no longer a legacy alert' - // ); // update alert as user with privileges - so it is no longer a legacy alert const updatedKeyResponse = await alertUtils.getUpdateApiKeyRequest(migratedAlertId); expect(updatedKeyResponse.statusCode).to.eql(204); await ensureAlertIsRunning(); break; + case 'global_read at space1': + await ensureLegacyAlertHasBeenMigrated(migratedAlertId); + + await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); + + await ensureAlertIsRunning(); + + await updateAlertSoThatItIsNoLongerLegacy(migratedAlertId); + + // attempt to update alert as user with no Alerts privileges - as it is no longer a legacy alert + // this should fail, as the user doesn't have the `updateApiKey` privilege for Alerts + const failedUpdateKeyDueToAlertsPrivilegesResponse = await alertUtils.getUpdateApiKeyRequest( + migratedAlertId + ); + + expect(failedUpdateKeyDueToAlertsPrivilegesResponse.statusCode).to.eql(403); + expect(failedUpdateKeyDueToAlertsPrivilegesResponse.body).to.eql({ + error: 'Forbidden', + message: + 'Unauthorized to updateApiKey a "test.always-firing" alert for "alertsFixture"', + statusCode: 403, + }); + break; case 'space_1_all_alerts_none_actions at space1': await ensureLegacyAlertHasBeenMigrated(migratedAlertId); @@ -109,12 +126,12 @@ export default function alertTests({ getService }: FtrProviderContext) { // attempt to update alert as user with no Actions privileges - as it is no longer a legacy alert // this should fail, as the user doesn't have the `execute` privilege for Actions - const failedUpdateKeyResponse = await alertUtils.getUpdateApiKeyRequest( + const failedUpdateKeyDueToActionsPrivilegesResponse = await alertUtils.getUpdateApiKeyRequest( migratedAlertId ); - expect(failedUpdateKeyResponse.statusCode).to.eql(403); - expect(failedUpdateKeyResponse.body).to.eql({ + expect(failedUpdateKeyDueToActionsPrivilegesResponse.statusCode).to.eql(403); + expect(failedUpdateKeyDueToActionsPrivilegesResponse.body).to.eql({ error: 'Forbidden', message: 'Unauthorized to execute actions', statusCode: 403, @@ -125,7 +142,6 @@ export default function alertTests({ getService }: FtrProviderContext) { } async function ensureLegacyAlertHasBeenMigrated(alertId: string) { - // console.log(`ensureLegacyAlertHasBeenMigrated(${alertId})`); const getResponse = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`) .auth(user.username, user.password); @@ -133,14 +149,8 @@ export default function alertTests({ getService }: FtrProviderContext) { } async function updateMigratedAlertToUseApiKeyOfCurrentUser(alertId: string) { - // console.log(`updateMigratedAlertToUseApiKeyOfCurrentUser(${alertId})`); - const createNoopAlertResponse = await alertUtils.createNoopAlert({}); - - // swap out api key to run as user with no Actions privileges - const swapResponse = await alertUtils.swapApiKeys( - createNoopAlertResponse.body.id, - alertId - ); + // swap out api key to run as the current user + const swapResponse = await alertUtils.replaceApiKeys(alertId); expect(swapResponse.statusCode).to.eql(200); // ensure the alert is till marked as legacy despite the update of the Api key // this is important as proper update *should* update the legacy status of the alert @@ -152,19 +162,16 @@ export default function alertTests({ getService }: FtrProviderContext) { // otherwise this test will stall for 5 minutes // no other attributes are touched, only runAt, so unless it would have ran when runAt expired, it // won't run now - const taskRescheduleResult = await supertest + await supertest .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reschedule_task`) .set('kbn-xsrf', 'foo') .send({ runAt: getRunAt(2000), }) .expect(200); - - // console.log(JSON.stringify(taskRescheduleResult.body)); } async function ensureAlertIsRunning() { - // console.log(`ensureAlertIsRunning(${reference})`); // ensure the alert still runs and that it can schedule actions const numberOfAlertExecutions = ( await esTestIndexTool.search('alert:test.always-firing', reference) @@ -194,7 +201,6 @@ export default function alertTests({ getService }: FtrProviderContext) { } async function updateAlertSoThatItIsNoLongerLegacy(alertId: string) { - // console.log(`updateAlertSoThatItIsNoLongerLegacy(${alertId})`); // update the alert as super user (to avoid privilege limitations) so that it is no longer a legacy alert await alertUtils.updateAlwaysFiringAction({ alertId, @@ -207,14 +213,6 @@ export default function alertTests({ getService }: FtrProviderContext) { throttle: '2s', }, }); - // return supertest - // .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`) - // .set('kbn-xsrf', 'foo') - // .auth(Superuser.username, Superuser.password) - // .send({ - // name: 'Updated Alert', - // }) - // .expect(200); } }); }); diff --git a/x-pack/test/functional/es_archives/alerts_legacy/data.json b/x-pack/test/functional/es_archives/alerts_legacy/data.json index 11e5e0cdc3f05..770e8e7c15617 100644 --- a/x-pack/test/functional/es_archives/alerts_legacy/data.json +++ b/x-pack/test/functional/es_archives/alerts_legacy/data.json @@ -394,4 +394,98 @@ "updated_at": "2020-09-04T11:51:05.794Z" } } +} + +{ + "type": "doc", + "value": { + "id": "space1:alert:362e362b-a137-4aa2-9434-43e3d0d84a34", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": "test.index-record", + "group": "default", + "params": { + "index": ".kibana-alerting-test-data", + "message": "alertId: {{alertId}},\nalertName: {{alertName}},\nspaceId: {{spaceId}},\ntags: {{tags}},\nalertInstanceId: {{alertInstanceId}},\ninstanceContextValue: {{context.instanceContextValue}},\ninstanceStateValue: {{state.instanceStateValue}}", + "reference": "alert:migrated-to-7.10:global_read" + } + } + ], + "alertTypeId": "test.always-firing", + "apiKey": "asFMeAR9MRRO5bwGiA5z3arqWOs9ZVPHYTvLL2S9JFRZSsA88yauEPxHeri3UpHWqLktDbOSpR4C1bmIQySDt1VOpe13JzGC/Z+/S0I9PaFQ8oHQ2VqjQ26j9hxuS+B9sXtAOhgYu2WBQw1ugeW2Eo86c9wpFux4p3SbKiNhwyC/9WUR5qI63cBnvRs8Zi8ePOMdnTJoiBzduw==", + "apiKeyOwner": "superuser", + "consumer": "alertsFixture", + "createdAt": "2020-09-04T11:50:56.513Z", + "createdBy": "superuser", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "abc", + "params": { + "index": ".kibana-alerting-test-data", + "reference": "alert:migrated-to-7.10:global_read" + }, + "schedule": { + "interval": "2s" + }, + "scheduledTaskId": "e4df5430-eea4-11ea-a285-352ee3aecffa", + "tags": [ + "tag-A", + "tag-B" + ], + "throttle": "2s", + "updatedBy": "superuser" + }, + "migrationVersion": { + "alert": "7.9.0" + }, + "namespace": "space1", + "references": [ + { + "id": "17f38826-5a8d-4a76-975a-b496e7fffe0b", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2020-09-04T11:50:57.048Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:e4df5430-eea4-11ea-a285-352ee3aecffa", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.6.0" + }, + "references": [ + ], + "task": { + "attempts": 1, + "ownerId": "kibana:5b2de169-2785-441b-ae8c-186a1936b17d", + "params": "{\"alertId\":\"362e362b-a137-4aa2-9434-43e3d0d84a34\",\"spaceId\":\"space1\"}", + "retryAt": "2020-09-04T12:01:05.793Z", + "runAt": "2020-09-04T11:51:04.804Z", + "scheduledAt": "2020-09-04T11:50:57.011Z", + "scope": [ + "alerting" + ], + "startedAt": "2020-09-04T11:51:05.793Z", + "state": "{\"previousStartedAt\":null,\"alertTypeState\":{},\"alertInstances\":{}}", + "status": "running", + "taskType": "alerting:test.always-firing" + }, + "type": "task", + "updated_at": "2020-09-04T11:51:05.794Z" + } + } } \ No newline at end of file From f661e329f7f849fe7260609f7689a1104ca8d361 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 4 Sep 2020 20:35:31 +0100 Subject: [PATCH 07/18] migrate as legacy --- .../alerts/server/saved_objects/migrations.test.ts | 12 +++++------- .../alerts/server/saved_objects/migrations.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index f851f874327d5..34ddc32693b2a 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -23,9 +23,7 @@ describe('7.10.0', () => { test('marks alerts as legacy', () => { const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; - const alert = getMockData({ - consumer: 'metrics', - }); + const alert = getMockData({}); expect(migration710(alert, { log })).toMatchObject({ ...alert, attributes: { @@ -75,9 +73,9 @@ describe('7.10.0', () => { describe('7.10.0 migrates with failure', () => { beforeEach(() => { jest.resetAllMocks(); - encryptedSavedObjectsSetup.createMigration.mockRejectedValueOnce( - new Error(`Can't migrate!`) as never - ); + encryptedSavedObjectsSetup.createMigration.mockImplementationOnce(() => () => { + throw new Error(`Can't migrate!`); + }); }); test('should show the proper exception', () => { @@ -93,7 +91,7 @@ describe('7.10.0 migrates with failure', () => { }, }); expect(log.error).toHaveBeenCalledWith( - `encryptedSavedObject migration failed for alert ${alert.id} with error: migrationFunc is not a function`, + `encryptedSavedObject migration failed for alert ${alert.id} with error: Can't migrate!`, { alertDocument: { ...alert, diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 6e4685104ba43..ee595908c8a78 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -32,7 +32,7 @@ export function getMigrations( } else if (doc.attributes.consumer === 'metrics') { return executeMigration(doc, context, infrastructureMigration); } - return executeMigration(doc, context, markAsLegacyAlert); + return executeMigration(doc, context, markAsLegacyAlert(encryptedSavedObjects)); }, }; } From a184efabd5e8a74a3bada685d28c476d8c5925b6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 4 Sep 2020 20:45:53 +0100 Subject: [PATCH 08/18] simplified migration as we now migrate all alerts pre 7.10 --- .../alerts/server/saved_objects/migrations.ts | 53 +++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index ee595908c8a78..730ad2fbdde2b 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -17,27 +17,24 @@ export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { - const alertsMigration = changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'); - - const infrastructureMigration = changeAlertingConsumer( + const migrationWhenRBACWasIntroduced = markAsLegacyAndChangeConsumer( encryptedSavedObjects, - 'metrics', - 'infrastructure' + new Map( + Object.entries({ + alerting: 'alerts', + metrics: 'infrastructure', + }) + ) ); return { '7.10.0': (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => { - if (doc.attributes.consumer === 'alerting') { - return executeMigration(doc, context, alertsMigration); - } else if (doc.attributes.consumer === 'metrics') { - return executeMigration(doc, context, infrastructureMigration); - } - return executeMigration(doc, context, markAsLegacyAlert(encryptedSavedObjects)); + return executeMigrationWithErrorHandling(doc, context, migrationWhenRBACWasIntroduced); }, }; } -function executeMigration( +function executeMigrationWithErrorHandling( doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext, migrationFunc: SavedObjectMigrationFn @@ -53,14 +50,14 @@ function executeMigration( return doc; } -function changeAlertingConsumer( +function markAsLegacyAndChangeConsumer( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, - from: string, - to: string + consumersToChange: Map ): SavedObjectMigrationFn { return encryptedSavedObjects.createMigration( function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - return doc.attributes.consumer === from; + // migrate all documents in 7.10 in order to add the "meta" RBAC field + return true; }, (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { const { @@ -70,29 +67,7 @@ function changeAlertingConsumer( ...doc, attributes: { ...doc.attributes, - consumer: consumer === from ? to : consumer, - // mark any alert predating 7.10 as a legacy alert - meta: { - versionLastmodified: LEGACY_LAST_MODIFIED_VERSION, - }, - }, - }; - } - ); -} - -function markAsLegacyAlert( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup -): SavedObjectMigrationFn { - return encryptedSavedObjects.createMigration( - function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - return true; - }, - (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, + consumer: consumersToChange.get(consumer) ?? consumer, // mark any alert predating 7.10 as a legacy alert meta: { versionLastmodified: LEGACY_LAST_MODIFIED_VERSION, From 5e580064450f4b1a93791e18a90ad671b135d4d7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 4 Sep 2020 20:51:41 +0100 Subject: [PATCH 09/18] cleaned up migration code --- .../alerts/server/saved_objects/migrations.ts | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 730ad2fbdde2b..18e167dbfadcb 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -17,43 +17,39 @@ export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { - const migrationWhenRBACWasIntroduced = markAsLegacyAndChangeConsumer( - encryptedSavedObjects, - new Map( - Object.entries({ - alerting: 'alerts', - metrics: 'infrastructure', - }) - ) - ); + const migrationWhenRBACWasIntroduced = markAsLegacyAndChangeConsumer(encryptedSavedObjects); return { - '7.10.0': (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => { - return executeMigrationWithErrorHandling(doc, context, migrationWhenRBACWasIntroduced); - }, + '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced), }; } function executeMigrationWithErrorHandling( - doc: SavedObjectUnsanitizedDoc, - context: SavedObjectMigrationContext, migrationFunc: SavedObjectMigrationFn ) { - try { - return migrationFunc(doc, context); - } catch (ex) { - context.log.error( - `encryptedSavedObject migration failed for alert ${doc.id} with error: ${ex.message}`, - { alertDocument: doc } - ); - } - return doc; + return (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => { + try { + return migrationFunc(doc, context); + } catch (ex) { + context.log.error( + `encryptedSavedObject migration failed for alert ${doc.id} with error: ${ex.message}`, + { alertDocument: doc } + ); + } + return doc; + }; } function markAsLegacyAndChangeConsumer( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, - consumersToChange: Map + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationFn { + const consumersToChange: Map = new Map( + Object.entries({ + alerting: 'alerts', + metrics: 'infrastructure', + }) + ); + return encryptedSavedObjects.createMigration( function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { // migrate all documents in 7.10 in order to add the "meta" RBAC field From b0a5c87dd39fd17b551e8965885cc7c97c5a7855 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 7 Sep 2020 10:19:43 +0100 Subject: [PATCH 10/18] fixed tests --- .../common/fixtures/plugins/alerts/kibana.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json index d8838133f4057..df61fde6518d0 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json @@ -3,8 +3,8 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["taskManager", "features", "actions", "alerts", "security", "encryptedSavedObjects"], - "optionalPlugins": ["spaces"], + "requiredPlugins": ["taskManager", "features", "actions", "alerts", "encryptedSavedObjects"], + "optionalPlugins": ["security", "spaces"], "server": true, "ui": false } From a58eb4e8a784c3a79f52ff3d01da5dbb68ac70f3 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 7 Sep 2020 10:42:17 +0100 Subject: [PATCH 11/18] added docs and cleaned up code --- x-pack/plugins/actions/README.md | 5 +++++ .../server/authorization/actions_authorization.test.ts | 4 ++-- .../server/authorization/actions_authorization.ts | 10 +++++----- x-pack/plugins/actions/server/index.ts | 2 +- x-pack/plugins/actions/server/lib/index.ts | 2 ++ x-pack/plugins/actions/server/plugin.ts | 8 ++++---- x-pack/plugins/actions/server/routes/execute.test.ts | 4 +++- x-pack/plugins/actions/server/routes/execute.ts | 2 ++ 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 868f6f180cc91..18f32432ee95a 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -301,6 +301,7 @@ The following table describes the properties of the `options` object. | -------- | ---------------------------------------------------- | ------ | | id | The id of the action you want to execute. | string | | params | The `params` value to give the action type executor. | object | +| source | The source of the execution, either a reponse to an HTTP request or a references to a Saved Object. | object, optional | ## Example @@ -316,6 +317,10 @@ const result = await actionsClient.execute({ subject: 'My email subject', body: 'My email body', }, + source: asSavedObjectExecutionSource({ + id: '573891ae-8c48-49cb-a197-0cd5ec34a88b', + type: 'alert' + }), }); ``` diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index 7f5f8c097750b..01bf0265127de 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -191,7 +191,7 @@ describe('ensureAuthorized', () => { `); }); - test('exempts users from requiring privileges to execute actions when shouldLegacyRbacApply is true', async () => { + test('exempts users from requiring privileges to execute actions when shouldUseLegacyRbac is true', async () => { const { authorization, authentication } = mockSecurity(); const checkPrivileges: jest.MockedFunction { authorization, authentication, auditLogger, - shouldLegacyRbacApply: true, + shouldUseLegacyRbac: true, }); authentication.getCurrentUser.mockReturnValueOnce(({ diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index c951fe9bbdcb5..02a36253a7236 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -22,7 +22,7 @@ export interface ConstructorOptions { // actions to continue to execute - which requires that we exempt auth on // `get` for Connectors and `execute` for Action execution when used by // these legacy alerts - shouldLegacyRbacApply?: boolean; + shouldUseLegacyRbac?: boolean; } const operationAlias: Record< @@ -43,20 +43,20 @@ export class ActionsAuthorization { private readonly authorization?: SecurityPluginSetup['authz']; private readonly authentication?: SecurityPluginSetup['authc']; private readonly auditLogger: ActionsAuthorizationAuditLogger; - private readonly shouldLegacyRbacApply: boolean; + private readonly shouldUseLegacyRbac: boolean; constructor({ request, authorization, authentication, auditLogger, - shouldLegacyRbacApply = false, + shouldUseLegacyRbac = false, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.authentication = authentication; this.auditLogger = auditLogger; - this.shouldLegacyRbacApply = shouldLegacyRbacApply; + this.shouldUseLegacyRbac = shouldUseLegacyRbac; } public async ensureAuthorized(operation: string, actionTypeId?: string) { @@ -87,6 +87,6 @@ export class ActionsAuthorization { } private isOperationExemptDueToLegacyRbac(operation: string) { - return this.shouldLegacyRbacApply && LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation); + return this.shouldUseLegacyRbac && LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation); } } diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 5b0b3bc9f55bf..bbe298c0585a3 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -23,7 +23,7 @@ export { } from './types'; export { PluginSetupContract, PluginStartContract } from './plugin'; -export { asSavedObjectExecutionSource } from './lib'; +export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './lib'; export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index d2d27eb3d67b6..e97875b91cf33 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -19,4 +19,6 @@ export { ActionExecutionSource, asSavedObjectExecutionSource, isSavedObjectExecutionSource, + asHttpRequestExecutionSource, + isHttpRequestExecutionSource, } from './action_execution_source'; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index e42fb03b3439b..574633ccc2223 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -281,7 +281,7 @@ export class ActionsPlugin implements Plugin, Plugi const getActionsClientWithRequest = async ( request: KibanaRequest, - sourceContext?: ActionExecutionSource + source?: ActionExecutionSource ) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( @@ -303,7 +303,7 @@ export class ActionsPlugin implements Plugin, Plugi request, authorization: instantiateAuthorization( request, - await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient, sourceContext) + await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient, source) ), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ @@ -389,11 +389,11 @@ export class ActionsPlugin implements Plugin, Plugi private instantiateAuthorization = ( request: KibanaRequest, - shouldLegacyRbacApply: boolean = false + shouldUseLegacyRbac: boolean = false ) => { return new ActionsAuthorization({ request, - shouldLegacyRbacApply, + shouldUseLegacyRbac, authorization: this.security?.authz, authentication: this.security?.authc, auditLogger: new ActionsAuthorizationAuditLogger( diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index b668e3460828a..e0c83711818ad 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -8,7 +8,7 @@ import { executeActionRoute } from './execute'; import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; +import { verifyApiAccess, ActionTypeDisabledError, asHttpRequestExecutionSource } from '../lib'; import { actionsClientMock } from '../actions_client.mock'; import { ActionTypeExecutorResult } from '../types'; @@ -61,6 +61,7 @@ describe('executeActionRoute', () => { params: { someData: 'data', }, + source: asHttpRequestExecutionSource(req), }); expect(res.ok).toHaveBeenCalled(); @@ -97,6 +98,7 @@ describe('executeActionRoute', () => { expect(actionsClient.execute).toHaveBeenCalledWith({ actionId: '1', params: {}, + source: asHttpRequestExecutionSource(req), }); expect(res.ok).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index f15a117106210..8191b6946d332 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -15,6 +15,7 @@ import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from import { ActionTypeExecutorResult } from '../types'; import { BASE_ACTION_API_PATH } from '../../common'; +import { asHttpRequestExecutionSource } from '../lib/action_execution_source'; const paramSchema = schema.object({ id: schema.string(), @@ -51,6 +52,7 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState) const body: ActionTypeExecutorResult = await actionsClient.execute({ params, actionId: id, + source: asHttpRequestExecutionSource(req), }); return body ? res.ok({ From c8b8815205aee0f9aec748f7cc832740c83cea3a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 7 Sep 2020 14:12:36 +0100 Subject: [PATCH 12/18] corrected name of variable --- .../authorization/should_legacy_rbac_apply_by_source.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts index ee6e2a70c7026..ef1847f7fd8c0 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts @@ -10,12 +10,12 @@ import { ActionExecutionSource, isSavedObjectExecutionSource } from '../lib'; const LEGACY_VERSION = 'pre-7.10.0'; export async function shouldLegacyRbacApplyBySource( - savedObjectsClient: SavedObjectsClientContract, + unsecuredSavedObjectsClient: SavedObjectsClientContract, executionSource?: ActionExecutionSource ): Promise { return isSavedObjectExecutionSource(executionSource) && executionSource?.source?.type === 'alert' ? ( - await savedObjectsClient.get<{ + await unsecuredSavedObjectsClient.get<{ meta?: { versionLastmodified?: string; }; From 23308807e69231c5264ec83cc99e428bdb8ea81a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 7 Sep 2020 14:16:47 +0100 Subject: [PATCH 13/18] use constant where possible --- .../authorization/should_legacy_rbac_apply_by_source.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts index ef1847f7fd8c0..58922a4301e0b 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts @@ -6,6 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { ActionExecutionSource, isSavedObjectExecutionSource } from '../lib'; +import { ALERT_SAVED_OBJECT_TYPE } from '../saved_objects'; const LEGACY_VERSION = 'pre-7.10.0'; @@ -13,13 +14,14 @@ export async function shouldLegacyRbacApplyBySource( unsecuredSavedObjectsClient: SavedObjectsClientContract, executionSource?: ActionExecutionSource ): Promise { - return isSavedObjectExecutionSource(executionSource) && executionSource?.source?.type === 'alert' + return isSavedObjectExecutionSource(executionSource) && + executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE ? ( await unsecuredSavedObjectsClient.get<{ meta?: { versionLastmodified?: string; }; - }>('alert', executionSource.source.id) + }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) ).attributes.meta?.versionLastmodified === LEGACY_VERSION : false; } From 5afd31d4aadefdb71c352735fb3d343a56981301 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 7 Sep 2020 15:37:27 +0100 Subject: [PATCH 14/18] added docs --- x-pack/plugins/actions/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 18f32432ee95a..62d484f428e4b 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -272,12 +272,14 @@ The following table describes the properties of the `options` object. | params | The `params` value to give the action type executor. | object | | spaceId | The space id the action is within. | string | | apiKey | The Elasticsearch API key to use for context. (Note: only required and used when security is enabled). | string | +| source | The source of the execution, either a reponse to an HTTP request or a references to a Saved Object. | object, optional | ## Example This example makes action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` send an email. The action plugin will load the saved object and find what action type to call with `params`. ```typescript +const request: KibanaRequest = { ... }; const actionsClient = await server.plugins.actions.getActionsClientWithRequest(request); await actionsClient.enqueueExecution({ id: '3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5', @@ -288,6 +290,7 @@ await actionsClient.enqueueExecution({ subject: 'My email subject', body: 'My email body', }, + source: asHttpRequestExecutionSource(request), }); ``` From 2a9694f22d12ed8a0e4ac20e82707c9a1c1f4ab5 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 9 Sep 2020 18:20:41 +0100 Subject: [PATCH 15/18] update meta field only when api key is updated --- x-pack/plugins/actions/README.md | 12 +++---- ...should_legacy_rbac_apply_by_source.test.ts | 4 +-- .../should_legacy_rbac_apply_by_source.ts | 4 +-- .../alerts/server/alerts_client.test.ts | 36 +++++++------------ x-pack/plugins/alerts/server/alerts_client.ts | 6 ++-- .../authorization/alerts_authorization.ts | 2 +- .../alerts/server/saved_objects/mappings.json | 2 +- .../server/saved_objects/migrations.test.ts | 8 ++--- .../alerts/server/saved_objects/migrations.ts | 22 ++++++------ x-pack/plugins/alerts/server/types.ts | 2 +- .../tests/alerting/rbac_legacy.ts | 4 ++- 11 files changed, 47 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index a1eaf7794d31d..222f0903baf99 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -280,7 +280,7 @@ The following table describes the properties of the `options` object. | params | The `params` value to give the action type executor. | object | | spaceId | The space id the action is within. | string | | apiKey | The Elasticsearch API key to use for context. (Note: only required and used when security is enabled). | string | -| source | The source of the execution, either a reponse to an HTTP request or a references to a Saved Object. | object, optional | +| source | The source of the execution, either an HTTP request or a reference to a Saved Object. | object, optional | ## Example @@ -308,11 +308,11 @@ This api runs the action and asynchronously returns the result of running the ac The following table describes the properties of the `options` object. -| Property | Description | Type | -| -------- | ---------------------------------------------------- | ------ | -| id | The id of the action you want to execute. | string | -| params | The `params` value to give the action type executor. | object | -| source | The source of the execution, either a reponse to an HTTP request or a references to a Saved Object. | object, optional | +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------ | ------ | +| id | The id of the action you want to execute. | string | +| params | The `params` value to give the action type executor. | object | +| source | The source of the execution, either an HTTP request or a reference to a Saved Object.| object, optional | ## Example diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts index 4d7e8990ed02b..03062994adeb6 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts +++ b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts @@ -44,7 +44,7 @@ describe(`#shouldLegacyRbacApplyBySource`, () => { test('should return true if source alert is marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( - mockAlert({ id, attributes: { meta: { versionLastmodified: 'pre-7.10.0' } } }) + mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: 'pre-7.10.0' } } }) ); expect( await shouldLegacyRbacApplyBySource( @@ -60,7 +60,7 @@ describe(`#shouldLegacyRbacApplyBySource`, () => { test('should return false if source alert is marked as modern', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( - mockAlert({ id, attributes: { meta: { versionLastmodified: '7.10.0' } } }) + mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: '7.10.0' } } }) ); expect( await shouldLegacyRbacApplyBySource( diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts index 58922a4301e0b..06d5776003ede 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts @@ -19,9 +19,9 @@ export async function shouldLegacyRbacApplyBySource( ? ( await unsecuredSavedObjectsClient.get<{ meta?: { - versionLastmodified?: string; + versionApiKeyLastmodified?: string; }; }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) - ).attributes.meta?.versionLastmodified === LEGACY_VERSION + ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION : false; } diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index acd59a43fe1f2..45859dbab5901 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -375,7 +375,7 @@ describe('create()', () => { "createdBy": "elastic", "enabled": true, "meta": Object { - "versionLastmodified": "v7.10.0", + "versionApiKeyLastmodified": "v7.10.0", }, "muteAll": false, "mutedInstanceIds": Array [], @@ -1003,7 +1003,7 @@ describe('create()', () => { updatedBy: 'elastic', enabled: true, meta: { - versionLastmodified: 'v7.10.0', + versionApiKeyLastmodified: 'v7.10.0', }, schedule: { interval: '10s' }, throttle: null, @@ -1119,7 +1119,7 @@ describe('create()', () => { updatedBy: 'elastic', enabled: false, meta: { - versionLastmodified: 'v7.10.0', + versionApiKeyLastmodified: 'v7.10.0', }, schedule: { interval: '10s' }, throttle: null, @@ -1247,7 +1247,7 @@ describe('enable()', () => { consumer: 'myApp', enabled: true, meta: { - versionLastmodified: kibanaVersion, + versionApiKeyLastmodified: kibanaVersion, }, updatedBy: 'elastic', apiKey: null, @@ -1335,7 +1335,7 @@ describe('enable()', () => { consumer: 'myApp', enabled: true, meta: { - versionLastmodified: kibanaVersion, + versionApiKeyLastmodified: kibanaVersion, }, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -1503,7 +1503,7 @@ describe('disable()', () => { apiKeyOwner: null, enabled: false, meta: { - versionLastmodified: kibanaVersion, + versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, updatedBy: 'elastic', @@ -1546,7 +1546,7 @@ describe('disable()', () => { apiKeyOwner: null, enabled: false, meta: { - versionLastmodified: kibanaVersion, + versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, updatedBy: 'elastic', @@ -1656,9 +1656,6 @@ describe('muteAll()', () => { await alertsClient.muteAll({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - meta: { - versionLastmodified: kibanaVersion, - }, muteAll: true, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1744,9 +1741,6 @@ describe('unmuteAll()', () => { await alertsClient.unmuteAll({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - meta: { - versionLastmodified: kibanaVersion, - }, muteAll: false, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1830,9 +1824,6 @@ describe('muteInstance()', () => { 'alert', '1', { - meta: { - versionLastmodified: kibanaVersion, - }, mutedInstanceIds: ['2'], updatedBy: 'elastic', }, @@ -1966,9 +1957,6 @@ describe('unmuteInstance()', () => { 'alert', '1', { - meta: { - versionLastmodified: kibanaVersion, - }, mutedInstanceIds: [], updatedBy: 'elastic', }, @@ -3235,7 +3223,7 @@ describe('update()', () => { "consumer": "myApp", "enabled": true, "meta": Object { - "versionLastmodified": "v7.10.0", + "versionApiKeyLastmodified": "v7.10.0", }, "name": "abc", "params": Object { @@ -3395,7 +3383,7 @@ describe('update()', () => { "consumer": "myApp", "enabled": true, "meta": Object { - "versionLastmodified": "v7.10.0", + "versionApiKeyLastmodified": "v7.10.0", }, "name": "abc", "params": Object { @@ -3549,7 +3537,7 @@ describe('update()', () => { "consumer": "myApp", "enabled": false, "meta": Object { - "versionLastmodified": "v7.10.0", + "versionApiKeyLastmodified": "v7.10.0", }, "name": "abc", "params": Object { @@ -4237,7 +4225,7 @@ describe('updateApiKey()', () => { }, ], meta: { - versionLastmodified: kibanaVersion, + versionApiKeyLastmodified: kibanaVersion, }, }, { version: '123' } @@ -4276,7 +4264,7 @@ describe('updateApiKey()', () => { }, ], meta: { - versionLastmodified: kibanaVersion, + versionApiKeyLastmodified: kibanaVersion, }, }, { version: '123' } diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index ac2063db1af93..519db3875b662 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -964,8 +964,10 @@ export class AlertsClient { } private updateMeta>(alertAttributes: T): T { - alertAttributes.meta = alertAttributes.meta ?? {}; - alertAttributes.meta.versionLastmodified = this.kibanaVersion; + if (alertAttributes.hasOwnProperty('apiKey') || alertAttributes.hasOwnProperty('apiKeyOwner')) { + alertAttributes.meta = alertAttributes.meta ?? {}; + alertAttributes.meta.versionApiKeyLastmodified = this.kibanaVersion; + } return alertAttributes; } } diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index c03c025e9dcee..7bc951c54ba58 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -111,7 +111,7 @@ export class AlertsAuthorization { } public shouldUseLegacyAuthorization(alert: RawAlert): boolean { - return alert.meta?.versionLastmodified === LEGACY_LAST_MODIFIED_VERSION; + return alert.meta?.versionApiKeyLastmodified === LEGACY_LAST_MODIFIED_VERSION; } private shouldCheckAuthorization(): boolean { diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json index 040a7f873c79c..8440b963975ff 100644 --- a/x-pack/plugins/alerts/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json @@ -79,7 +79,7 @@ }, "meta": { "properties": { - "versionLastmodified": { + "versionApiKeyLastmodified": { "type": "keyword" } } diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 34ddc32693b2a..84af646a6e2ee 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -29,7 +29,7 @@ describe('7.10.0', () => { attributes: { ...alert.attributes, meta: { - versionLastmodified: 'pre-7.10.0', + versionApiKeyLastmodified: 'pre-7.10.0', }, }, }); @@ -46,7 +46,7 @@ describe('7.10.0', () => { ...alert.attributes, consumer: 'infrastructure', meta: { - versionLastmodified: 'pre-7.10.0', + versionApiKeyLastmodified: 'pre-7.10.0', }, }, }); @@ -63,7 +63,7 @@ describe('7.10.0', () => { ...alert.attributes, consumer: 'alerts', meta: { - versionLastmodified: 'pre-7.10.0', + versionApiKeyLastmodified: 'pre-7.10.0', }, }, }); @@ -91,7 +91,7 @@ describe('7.10.0 migrates with failure', () => { }, }); expect(log.error).toHaveBeenCalledWith( - `encryptedSavedObject migration failed for alert ${alert.id} with error: Can't migrate!`, + `encryptedSavedObject 7.10.0 migration failed for alert ${alert.id} with error: Can't migrate!`, { alertDocument: { ...alert, diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 18e167dbfadcb..c877f955468c3 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -20,19 +20,20 @@ export function getMigrations( const migrationWhenRBACWasIntroduced = markAsLegacyAndChangeConsumer(encryptedSavedObjects); return { - '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced), + '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), }; } function executeMigrationWithErrorHandling( - migrationFunc: SavedObjectMigrationFn + migrationFunc: SavedObjectMigrationFn, + version: string ) { return (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => { try { return migrationFunc(doc, context); } catch (ex) { context.log.error( - `encryptedSavedObject migration failed for alert ${doc.id} with error: ${ex.message}`, + `encryptedSavedObject ${version} migration failed for alert ${doc.id} with error: ${ex.message}`, { alertDocument: doc } ); } @@ -40,16 +41,15 @@ function executeMigrationWithErrorHandling( }; } +const consumersToChange: Map = new Map( + Object.entries({ + alerting: 'alerts', + metrics: 'infrastructure', + }) +); function markAsLegacyAndChangeConsumer( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationFn { - const consumersToChange: Map = new Map( - Object.entries({ - alerting: 'alerts', - metrics: 'infrastructure', - }) - ); - return encryptedSavedObjects.createMigration( function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { // migrate all documents in 7.10 in order to add the "meta" RBAC field @@ -66,7 +66,7 @@ function markAsLegacyAndChangeConsumer( consumer: consumersToChange.get(consumer) ?? consumer, // mark any alert predating 7.10 as a legacy alert meta: { - versionLastmodified: LEGACY_LAST_MODIFIED_VERSION, + versionApiKeyLastmodified: LEGACY_LAST_MODIFIED_VERSION, }, }, }; diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index b1fbd3b8a6ae7..8d568e8b7ecd1 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -112,7 +112,7 @@ export interface RawAlertAction extends SavedObjectAttributes { } export interface AlertMeta extends SavedObjectAttributes { - versionLastmodified?: string; + versionApiKeyLastmodified?: string; } export type PartialAlert = Pick & Partial>; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 3315255885ca5..513b7fc449065 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -156,7 +156,9 @@ export default function alertTests({ getService }: FtrProviderContext) { // this is important as proper update *should* update the legacy status of the alert // and we want to ensure we don't accidentally introduce a change that might break our support of legacy alerts expect(swapResponse.body.id).to.eql(alertId); - expect(swapResponse.body.attributes.meta.versionLastmodified).to.eql('pre-7.10.0'); + expect(swapResponse.body.attributes.meta.versionApiKeyLastmodified).to.eql( + 'pre-7.10.0' + ); // loading the archive likely caused the task to fail so ensure it's rescheduled to run in 2 seconds, // otherwise this test will stall for 5 minutes From 332d14acf771f645a53f4a68dd9aeb1d828d1137 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 14 Sep 2020 18:43:30 +0100 Subject: [PATCH 16/18] fixed merge issue --- .../actions/server/authorization/actions_authorization.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index f3db6e78a1c8b..577c3a340301c 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -77,7 +77,6 @@ export class ActionsAuthorization { }); if (hasAllRequested) { this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); - this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); } else { throw Boom.forbidden( this.auditLogger.actionsAuthorizationFailure(username, operation, actionTypeId) From b32e62e3fe9739ae289a7e58e01677c1687eacae Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 15 Sep 2020 17:11:38 +0100 Subject: [PATCH 17/18] migrate siem consumer --- .../server/saved_objects/migrations.test.ts | 17 +++++++++++++++++ .../alerts/server/saved_objects/migrations.ts | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 84af646a6e2ee..1c1261ae3fa08 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -52,6 +52,23 @@ describe('7.10.0', () => { }); }); + test('migrates the consumer for siem', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + consumer: 'securitySolution', + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'siem', + meta: { + versionApiKeyLastmodified: 'pre-7.10.0', + }, + }, + }); + }); + test('migrates the consumer for alerting', () => { const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const alert = getMockData({ diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index c877f955468c3..c88f4d786c212 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -11,6 +11,10 @@ import { } from '../../../../../src/core/server'; import { RawAlert } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +import { + APP_ID as SIEM_APP_ID, + SERVER_APP_ID as SIEM_SERVER_APP_ID, +} from '../../../security_solution/common/constants'; export const LEGACY_LAST_MODIFIED_VERSION = 'pre-7.10.0'; @@ -45,6 +49,7 @@ const consumersToChange: Map = new Map( Object.entries({ alerting: 'alerts', metrics: 'infrastructure', + [SIEM_APP_ID]: SIEM_SERVER_APP_ID, }) ); function markAsLegacyAndChangeConsumer( From b486243b07f4a4bf4492e290a04421160db0911f Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 16 Sep 2020 09:16:47 +0100 Subject: [PATCH 18/18] typo Co-authored-by: Patrick Mueller --- .../actions/server/authorization/actions_authorization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index 577c3a340301c..bd6e355c2cf9d 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -15,7 +15,7 @@ export interface ConstructorOptions { auditLogger: ActionsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; authentication?: SecurityPluginSetup['authc']; - // In order to support legacy Alerts which predate the introduciton of the + // In order to support legacy Alerts which predate the introduction of the // Actions feature in Kibana we need a way of "dialing down" the level of // authorization for certain opearations. // Specifically, we want to allow these old alerts and their scheduled