From 73830cc33c67a9d974b8827e979905ca98957112 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Thu, 22 Jul 2021 22:55:52 -0400 Subject: [PATCH 1/7] WIP --- .../plugins/actions/server/actions_client.ts | 41 +- .../get_authorization_mode_by_source.ts | 5 +- .../actions/server/create_execute_function.ts | 28 +- .../actions/server/lib/action_executor.ts | 6 +- .../actions/server/lib/task_runner_factory.ts | 3 + .../actions/server/saved_objects/index.ts | 8 +- .../invalidate_pending_api_keys/task.ts | 1 + .../server/rules_client/rules_client.ts | 98 +- .../alerting/server/saved_objects/index.ts | 4 +- .../server/task_runner/task_runner.ts | 10 +- .../server/services/attachments/index.ts | 22 +- .../cases/server/services/cases/index.ts | 31 +- .../cases/server/services/configure/index.ts | 2 +- .../crypto/encrypted_saved_objects_service.ts | 1 - .../server/saved_objects/index.ts | 17 +- .../server/lib/log_health_metrics.ts | 1 + .../tests/alerting/get_existing.ts | 47 + .../spaces_only/tests/alerting/index.ts | 1 + .../alerts_in_multiple_spaces/data.json.gz | Bin 0 -> 1684 bytes .../alerts_in_multiple_spaces/mappings.json | 2949 +++++++++++++++++ 20 files changed, 3212 insertions(+), 63 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts create mode 100644 x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz create mode 100644 x-pack/test/functional/es_archives/alerts_in_multiple_spaces/mappings.json diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 66032a7c411ba..3f9bc0d2894e2 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -17,6 +17,7 @@ import { SavedObject, KibanaRequest, SavedObjectsUtils, + SavedObjectsResolveResponse, } from '../../../../src/core/server'; import { AuditLogger } from '../../security/server'; import { ActionType } from '../common'; @@ -209,10 +210,10 @@ export class ActionsClient { throw error; } const { - attributes, - references, - version, - } = await this.unsecuredSavedObjectsClient.get('action', id); + saved_object: { attributes, references, version }, + /* resolvedResponse */ + } = await this.unsecuredSavedObjectsClient.resolve('action', id); + // TODO: How to handle `resolveResponse` here const { actionTypeId } = attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); @@ -263,7 +264,13 @@ export class ActionsClient { /** * Get an action */ - public async get({ id }: { id: string }): Promise { + public async get({ + id, + }: { + id: string; + }): Promise< + ActionResult & { resolveResponse?: Omit } + > { try { await this.authorization.ensureAuthorized('get'); } catch (error) { @@ -296,7 +303,12 @@ export class ActionsClient { }; } - const result = await this.unsecuredSavedObjectsClient.get('action', id); + const { + saved_object: result, + ...resolveResponse + } = await this.unsecuredSavedObjectsClient.resolve('action', id); + + // TODO: How to handle `resolveResponse` here this.auditLogger?.log( connectorAuditEvent({ @@ -312,6 +324,7 @@ export class ActionsClient { name: result.attributes.name, config: result.attributes.config, isPreconfigured: false, + resolveResponse, }; } @@ -331,12 +344,24 @@ export class ActionsClient { throw error; } - const savedObjectsActions = ( + const discoveredSavedObjects = ( await this.unsecuredSavedObjectsClient.find({ perPage: MAX_ACTIONS_RETURNED, type: 'action', }) - ).saved_objects.map(actionFromSavedObject); + ).saved_objects; + const savedObjectsActions = ( + await Promise.all( + discoveredSavedObjects.map(({ id }) => + this.unsecuredSavedObjectsClient.resolve('action', id) + ) + ) + ).map(({ saved_object: action, ...resolveResponse }) => { + return { + ...actionFromSavedObject(action), + resolveResponse, + }; + }); savedObjectsActions.forEach(({ id }) => this.auditLogger?.log( diff --git a/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts index 659a51df7fbbd..a3ac31e4a5538 100644 --- a/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts @@ -20,15 +20,16 @@ export async function getAuthorizationModeBySource( unsecuredSavedObjectsClient: SavedObjectsClientContract, executionSource?: ActionExecutionSource ): Promise { + // TODO: How to handle `resolveResponse` here return isSavedObjectExecutionSource(executionSource) && executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE && ( - await unsecuredSavedObjectsClient.get<{ + await unsecuredSavedObjectsClient.resolve<{ meta?: { versionApiKeyLastmodified?: string; }; }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) - ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION + ).saved_object.attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION ? AuthorizationMode.Legacy : AuthorizationMode.RBAC; } diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index bcad5f20d9ba7..43f20a8c78880 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { SavedObjectsClientContract } from '../../../../src/core/server'; +import { + SavedObjectsClientContract, + SavedObjectsResolveResponse, +} from '../../../../src/core/server'; import { RunNowResult, TaskManagerStartContract } from '../../task_manager/server'; import { RawAction, @@ -53,9 +56,11 @@ export function createExecutionEnqueuerFunction({ ); } - const action = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id); + const { action } = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id); validateCanActionBeUsed(action); + // TODO: How to handle `resolveResponse` here + const { actionTypeId } = action; if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); @@ -93,7 +98,7 @@ export function createEphemeralExecutionEnqueuerFunction({ unsecuredSavedObjectsClient: SavedObjectsClientContract, { id, params, spaceId, source, apiKey }: ExecuteOptions ): Promise { - const action = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id); + const { action } = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id); validateCanActionBeUsed(action); const { actionTypeId } = action; @@ -148,12 +153,21 @@ async function getAction( unsecuredSavedObjectsClient: SavedObjectsClientContract, preconfiguredActions: PreConfiguredAction[], actionId: string -): Promise { +): Promise<{ + action: PreConfiguredAction | RawAction; + resolveResponse?: Omit; +}> { const pcAction = preconfiguredActions.find((action) => action.id === actionId); if (pcAction) { - return pcAction; + return { action: pcAction }; } - const { attributes } = await unsecuredSavedObjectsClient.get('action', actionId); - return attributes; + const { + saved_object: { attributes }, + ...resolveResponse + } = await unsecuredSavedObjectsClient.resolve('action', actionId); + return { + action: attributes, + resolveResponse, + }; } diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 5dfe56cff5016..6b3157878c465 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger, KibanaRequest } from 'src/core/server'; +import { Logger, KibanaRequest, SavedObjectsResolveResponse } from 'src/core/server'; import { cloneDeep } from 'lodash'; import { withSpan } from '@kbn/apm-utils'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; @@ -288,7 +288,7 @@ async function getActionInfo( preconfiguredActions: PreConfiguredAction[], actionId: string, namespace: string | undefined -): Promise { +): Promise }> { // check to see if it's a pre-configured action first const pcAction = preconfiguredActions.find( (preconfiguredAction) => preconfiguredAction.id === actionId @@ -308,6 +308,7 @@ async function getActionInfo( const { attributes: { secrets }, + resolveResponse, } = await encryptedSavedObjectsClient.getDecryptedAsInternalUser('action', actionId, { namespace: namespace === 'default' ? undefined : namespace, }); @@ -317,5 +318,6 @@ async function getActionInfo( name, config, secrets, + resolveResponse, }; } 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 2354ea55eded6..c91474618111b 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -87,12 +87,15 @@ export class TaskRunnerFactory { const { attributes: { actionId, params, apiKey, relatedSavedObjects }, references, + /* resolveResponse, */ } = await getActionTaskParams( actionTaskExecutorParams, encryptedSavedObjectsClient, spaceIdToNamespace ); + // TODO: How to handle `resolveResponse` here? + const requestHeaders: Record = {}; if (apiKey) { requestHeaders.authorization = `ApiKey ${apiKey}`; diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 9d8c364007959..369a92701af08 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -31,7 +31,9 @@ export function setupSavedObjects( savedObjects.registerType({ name: ACTION_SAVED_OBJECT_TYPE, hidden: true, - namespaceType: 'single', + // namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', mappings: mappings.action as SavedObjectsTypeMappingDefinition, migrations: getMigrations(encryptedSavedObjects), management: { @@ -67,7 +69,9 @@ export function setupSavedObjects( savedObjects.registerType({ name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, hidden: true, - namespaceType: 'single', + // namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', mappings: mappings.action_task_params as SavedObjectsTypeMappingDefinition, }); encryptedSavedObjects.registerType({ diff --git a/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts index 4ef0e8d25c574..15bdcc6b4e605 100644 --- a/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts +++ b/x-pack/plugins/alerting/server/invalidate_pending_api_keys/task.ts @@ -201,6 +201,7 @@ async function invalidateApiKeys( 'api_key_pending_invalidation', apiKeyObj.id ); + // TODO: How to handle `resolveResponse` here? return decryptedApiKey.attributes.apiKeyId; }) ); diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 11db0fd8ec6cd..96baf6d11dbc6 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -17,6 +17,7 @@ import { PluginInitializerContext, SavedObjectsUtils, SavedObjectAttributes, + SavedObjectsResolveResponse, } from '../../../../../src/core/server'; import { esKuery } from '../../../../../src/plugins/data/server'; import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; @@ -379,7 +380,10 @@ export class RulesClient { }: { id: string; }): Promise> { - const result = await this.unsecuredSavedObjectsClient.get('alert', id); + const { + saved_object: result, + ...resolveResponse + } = await this.unsecuredSavedObjectsClient.resolve('alert', id); try { await this.authorization.ensureAuthorized({ ruleTypeId: result.attributes.alertTypeId, @@ -407,7 +411,8 @@ export class RulesClient { result.id, result.attributes.alertTypeId, result.attributes, - result.references + result.references, + resolveResponse ); } @@ -617,6 +622,7 @@ export class RulesClient { id, { namespace: this.namespace } ); + // TODO: How to handle `resolveResponse` here? apiKeyToInvalidate = decryptedAlert.attributes.apiKey; taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; attributes = decryptedAlert.attributes; @@ -626,7 +632,11 @@ export class RulesClient { `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the scheduledTaskId using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + const { saved_object: alert } = await this.unsecuredSavedObjectsClient.resolve( + 'alert', + id + ); + // TODO: How to handle `resolveResponse` here? taskIdToRemove = alert.attributes.scheduledTaskId; attributes = alert.attributes; } @@ -696,13 +706,18 @@ export class RulesClient { id, { namespace: this.namespace } ); + // TODO: How to handle `resolveResponse` here? } catch (e) { // We'll skip invalidating the API key since we failed to load the decrypted saved object this.logger.error( `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the object using SOC - alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); + const { + saved_object: savedObject, + } = await this.unsecuredSavedObjectsClient.resolve('alert', id); + // TODO: How to handle `resolveResponse` here? + alertSavedObject = savedObject; } try { @@ -884,7 +899,11 @@ export class RulesClient { `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + const { saved_object: alert } = await this.unsecuredSavedObjectsClient.resolve( + 'alert', + id + ); + // TODO: How to handle `resolveResponse` here? attributes = alert.attributes; version = alert.version; } @@ -989,9 +1008,13 @@ export class RulesClient { `enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + const { saved_object: alert } = await this.unsecuredSavedObjectsClient.resolve( + 'alert', + id + ); attributes = alert.attributes; version = alert.version; + // TODO: How to handle `resolveResponse` here } try { @@ -1098,6 +1121,7 @@ export class RulesClient { id, { namespace: this.namespace } ); + // TODO: How to handle `resolveResponse` here? apiKeyToInvalidate = decryptedAlert.attributes.apiKey; attributes = decryptedAlert.attributes; version = decryptedAlert.version; @@ -1107,7 +1131,11 @@ export class RulesClient { `disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.unsecuredSavedObjectsClient.get('alert', id); + const { saved_object: alert } = await this.unsecuredSavedObjectsClient.resolve( + 'alert', + id + ); + // TODO: How to handle `resolveResponse` here? attributes = alert.attributes; version = alert.version; } @@ -1180,10 +1208,10 @@ export class RulesClient { } private async muteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); + const { + saved_object: { attributes, version }, + } = await this.unsecuredSavedObjectsClient.resolve('alert', id); + // TODO: How to handle `resolveResponse` here? try { await this.authorization.ensureAuthorized({ @@ -1242,10 +1270,10 @@ export class RulesClient { } private async unmuteAllWithOCC({ id }: { id: string }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - id - ); + const { + saved_object: { attributes, version }, + } = await this.unsecuredSavedObjectsClient.resolve('alert', id); + // TODO: How to handle `resolveResponse` here try { await this.authorization.ensureAuthorized({ @@ -1304,10 +1332,10 @@ export class RulesClient { } private async muteInstanceWithOCC({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); + const { + saved_object: { attributes, version }, + } = await this.unsecuredSavedObjectsClient.resolve('alert', alertId); + // TODO: How to handle `resolveResponse` here? try { await this.authorization.ensureAuthorized({ @@ -1372,10 +1400,11 @@ export class RulesClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.unsecuredSavedObjectsClient.get( - 'alert', - alertId - ); + const { + saved_object: { attributes, version }, + } = await this.unsecuredSavedObjectsClient.resolve('alert', alertId); + + // TODO: How to handle `resolveResponse` here try { await this.authorization.ensureAuthorized({ @@ -1469,13 +1498,22 @@ export class RulesClient { id: string, ruleTypeId: string, rawAlert: RawAlert, - references: SavedObjectReference[] | undefined - ): Alert { + references: SavedObjectReference[] | undefined, + resolveResponse?: Omit + ): Alert & { resolveResponse?: Omit } { const ruleType = this.alertTypeRegistry.get(ruleTypeId); // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawAlert, it is safe // to cast the result to an Alert - return this.getPartialAlertFromRaw(id, ruleType, rawAlert, references) as Alert; + return this.getPartialAlertFromRaw( + id, + ruleType, + rawAlert, + references, + resolveResponse + ) as Alert & { + resolveResponse: Omit; + }; } private getPartialAlertFromRaw( @@ -1490,8 +1528,11 @@ export class RulesClient { params, ...rawAlert }: Partial, - references: SavedObjectReference[] | undefined - ): PartialAlert { + references: SavedObjectReference[] | undefined, + resolveResponse?: Omit + ): PartialAlert & { + resolveResponse?: Omit; + } { // Not the prettiest code here, but if we want to use most of the // alert fields from the rawAlert using `...rawAlert` kind of access, we // need to specifically delete the executionStatus as it's a different type @@ -1506,6 +1547,7 @@ export class RulesClient { return { id, notifyWhen, + resolveResponse, ...rawAlertWithoutExecutionStatus, // we currently only support the Interval Schedule type // Once we support additional types, this type signature will likely change diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index 88ee3179ab3d8..2c5b58e1e0d3e 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -53,7 +53,9 @@ export function setupSavedObjects( savedObjects.registerType({ name: 'alert', hidden: true, - namespaceType: 'single', + // namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert as SavedObjectsTypeMappingDefinition, management: { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 8abfbf82f960f..b4f3d9105c982 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -115,13 +115,19 @@ export class TaskRunner< const namespace = this.context.spaceIdToNamespace(spaceId); // Only fetch encrypted attributes here, we'll create a saved objects client // scoped with the API key to fetch the remaining data. + + // TODO: Change when https://github.com/elastic/kibana/issues/106567 is resolved + const { attributes: { apiKey }, } = await this.context.encryptedSavedObjectsClient.getDecryptedAsInternalUser( 'alert', alertId, - { namespace } + { + namespace, + } ); + // TODO: How to handle `resolveResponse` here? return apiKey; } @@ -456,6 +462,7 @@ export class TaskRunner< const { params: { alertId, spaceId }, } = this.taskInstance; + // TODO: Change when https://github.com/elastic/kibana/issues/106567 is resolved let apiKey: string | null; try { apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId); @@ -478,6 +485,7 @@ export class TaskRunner< } catch (err) { throw new ErrorWithReason(AlertExecutionStatusErrorReasons.License, err); } + return { state: await promiseResult( this.validateAndExecuteAlert(services, apiKey, alert, event) diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index c2d9b4826fc14..3d2905f5aeb64 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { Logger, SavedObject, SavedObjectReference } from 'kibana/server'; +import { + Logger, + SavedObject, + SavedObjectReference, + SavedObjectsResolveResponse, +} from 'kibana/server'; import { KueryNode } from '../../../../../../src/plugins/data/common'; import { @@ -93,13 +98,24 @@ export class AttachmentService { public async get({ unsecuredSavedObjectsClient, attachmentId, - }: GetAttachmentArgs): Promise> { + }: GetAttachmentArgs): Promise< + SavedObject & { + resolveResponse?: Omit; + } + > { try { this.log.debug(`Attempting to GET attachment ${attachmentId}`); - return await unsecuredSavedObjectsClient.get( + const { + saved_object: savedObject, + ...resolveResponse + } = await unsecuredSavedObjectsClient.resolve( CASE_COMMENT_SAVED_OBJECT, attachmentId ); + return { + ...savedObject, + resolveResponse, + }; } catch (error) { this.log.error(`Error on GET attachment ${attachmentId}: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index a0e4380f95640..97fdf56e16c0a 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -14,6 +14,7 @@ import { SavedObjectsFindResponse, SavedObjectsBulkResponse, SavedObjectsFindResult, + SavedObjectsResolveResponse, } from 'kibana/server'; import type { estypes } from '@elastic/elasticsearch'; @@ -713,10 +714,21 @@ export class CasesService { public async getCase({ unsecuredSavedObjectsClient, id: caseId, - }: GetCaseArgs): Promise> { + }: GetCaseArgs): Promise< + SavedObject & { + resolveResponse?: Omit; + } + > { try { this.log.debug(`Attempting to GET case ${caseId}`); - return await unsecuredSavedObjectsClient.get(CASE_SAVED_OBJECT, caseId); + const { + saved_object: savedObject, + ...resolveResponse + } = await unsecuredSavedObjectsClient.resolve(CASE_SAVED_OBJECT, caseId); + return { + ...savedObject, + resolveResponse, + }; } catch (error) { this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; @@ -725,10 +737,21 @@ export class CasesService { public async getSubCase({ unsecuredSavedObjectsClient, id, - }: GetCaseArgs): Promise> { + }: GetCaseArgs): Promise< + SavedObject & { + resolveResponse?: Omit; + } + > { try { this.log.debug(`Attempting to GET sub case ${id}`); - return await unsecuredSavedObjectsClient.get(SUB_CASE_SAVED_OBJECT, id); + const { + saved_object: savedObject, + ...resolveResponse + } = await unsecuredSavedObjectsClient.resolve(SUB_CASE_SAVED_OBJECT, id); + return { + ...savedObject, + resolveResponse, + }; } catch (error) { this.log.error(`Error on GET sub case ${id}: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 348bff954b73e..3397ba0ec1ed2 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -47,7 +47,7 @@ export class CaseConfigureService { public async get({ unsecuredSavedObjectsClient, configurationId }: GetCaseConfigureArgs) { try { this.log.debug(`Attempting to GET case configuration ${configurationId}`); - return await unsecuredSavedObjectsClient.get( + return await unsecuredSavedObjectsClient.resolve( CASE_CONFIGURE_SAVED_OBJECT, configurationId ); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 652a2c8b6870e..a8b53ff47c471 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -498,7 +498,6 @@ export class EncryptedSavedObjectsService { `Failed to decrypt "${attributeName}" attribute: ${err.message || err}` ); this.options.audit.decryptAttributeFailure(attributeName, descriptor, params?.user); - throw new EncryptionError( `Unable to decrypt attribute "${attributeName}"`, attributeName, diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts index 1268113eb19bb..c33451a9dcedd 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts @@ -11,6 +11,7 @@ import type { ISavedObjectTypeRegistry, SavedObject, SavedObjectsBaseOptions, + SavedObjectsResolveResponse, SavedObjectsServiceSetup, StartServicesAccessor, } from 'src/core/server'; @@ -42,7 +43,9 @@ export interface EncryptedSavedObjectsClient { type: string, id: string, options?: SavedObjectsBaseOptions - ) => Promise>; + ) => Promise< + SavedObject & { resolveResponse?: Omit } + >; } export function setupSavedObjects({ @@ -81,11 +84,19 @@ export function setupSavedObjects({ type: string, id: string, options?: SavedObjectsBaseOptions - ): Promise> => { + ): Promise< + SavedObject & { resolveResponse?: Omit } + > => { const [internalRepository, typeRegistry] = await internalRepositoryAndTypeRegistryPromise; - const savedObject = await internalRepository.get(type, id, options); + const { saved_object: savedObject, ...resolveResponse } = await internalRepository.resolve( + type, + id, + options + ); + return { ...savedObject, + resolveResponse, attributes: (await service.decryptAttributes( { type, diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts index e8511b1e8c71d..45e5361b7ddb8 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts @@ -27,6 +27,7 @@ export function logHealthMetrics( logger: Logger, config: TaskManagerConfig ) { + return; let logLevel: LogLevel = LogLevel.Debug; const enabled = config.monitored_stats_health_verbose_log.enabled; const healthWithoutCapacity: MonitoredHealth = { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts new file mode 100644 index 0000000000000..880bdb5106053 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +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 createGetExistingTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get existing', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/alerts_in_multiple_spaces'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/alerts_in_multiple_spaces'); + }); + + it('should show the alias resolve match for the non default space rule', async () => { + const response = await supertest.get( + `${getUrlPrefix(`chrisspace`)}/api/alerting/rule/100ff690-eaf9-11eb-9cd0-ed3f761f7f83` + ); + + expect(response.body.resolveResponse).to.eql({ + outcome: 'aliasMatch', + aliasTargetId: 'fdc84d89-1f92-5511-b4a4-65034392a07a', + }); + }); + + it('should show the absolute resolve match for the default space rule', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerting/rule/efb7a0f0-eaf8-11eb-9cd0-ed3f761f7f83` + ); + + expect(response.body.resolveResponse).to.eql({ + outcome: 'exactMatch', + }); + }); + }); +} 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 3a4cc62c2550f..d520349b19567 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 @@ -21,6 +21,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./enable')); loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_existing')); loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./get_alert_instance_summary')); loadTestFile(require.resolve('./rule_types')); diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ae51e10c7cc8237d817a720bac73613df3314e31 GIT binary patch literal 1684 zcmV;F25b2riwFP!000026YW`PbE?Q1{+?gqb-vw<1_42=nyCveF^M~(2j@(sT4)4i z(WXK1RQ~%kxCD%;Np2;xoSCYW3H>hp_Pad&1ixM7a(NuBV7{8m>zKSb6)BkedG>d#AaNPBUT5&)5#h^10v{Z&?id299DlliI3(fkBtE+EsZw;WuS*SuyC@~D63J@5TLa7921wqrvqs|yS zS2B2`!u&stvv;2@+J0?B!n;dCC?+LD1>2kFGX-=FYo0NEN{KDMWBI=2&Sj_}h~sJ) z!1m$mb&%t1Kg!8w?d)Fu$w!`!_;9Q#B}Or$Mptp*7)<;-eY5twWre2H2_;Ed4?|(6 zw4LRwH9_8v>$IZf7&!quDsutkJ7%R34sTGnG^TFd3I*s>WADyTRnr0%> z0;w7LE->xgFjI@ukTmzlAPrw-*rFiNA6&ENYE=)QLjjKE%2Wb6!uJL`&sF(d$Wiz@ z)4XN}3q?kqa+g)`ylbg?%Ln(0!=br6x|zB*{z89|!k`nWBBa*_W2kT_&}~U(M&-IX zq|_VrhM8JAUk;=QmgRYUtuD&px{kI`oY<5;TV8|Oi0R!ZA#i+U!_{iNc5VJ=LQb5l z(*(U3=TE|v@7m{exlL%JU1B_xZg?Vk#36{<2z=3^n30gg_r-$CE0oQ1CZSZh0=F7mh<7@RW&^gj zlC(PP)!}e!v~L@`K5r~QX{>&cn-A8eK@TOV-xEW1JeS&)(W1^rHe5k6#{jLbSCLwq z;g-svll0M z^D|Ne#1ecccG*L*d3j2Dube+0JuHJ>BaPw_hWc*LP!1p)&u zjsd3d-}HF@nyrwvi_c0hHM5LO!jMeQ)?c`%UUhY{sd-L(a=iaP@#k#N*m7Xgf{Ew+ z|KcZ75NGvLX+npG7_nw`6R_d>r@4ofRm__dAH} z+2~%phR5;U7nO(r+z3m-dXK z>2@Gr%l>L1!Vulkz{2+x6Ufp7JMV!&Hk-OWyH>n)LoE$$d(*$!Z1K2UnfJDL!?`ft zb=|?#?pk8!X6UvaIK?wPy05zW+9>0wrrq45p?QBhYJ?9{QBZt2RJJ2~Bb%ZM Date: Mon, 26 Jul 2021 15:55:50 -0400 Subject: [PATCH 2/7] Add failing test for migration issue --- .../server/lib/log_health_metrics.ts | 1 - .../tests/alerting/get_existing.ts | 18 ++++++++++++++---- .../alerts_in_multiple_spaces/data.json.gz | Bin 1684 -> 1698 bytes .../alerts_in_multiple_spaces/mappings.json | 9 +++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts index 45e5361b7ddb8..e8511b1e8c71d 100644 --- a/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts +++ b/x-pack/plugins/task_manager/server/lib/log_health_metrics.ts @@ -27,7 +27,6 @@ export function logHealthMetrics( logger: Logger, config: TaskManagerConfig ) { - return; let logLevel: LogLevel = LogLevel.Debug; const enabled = config.monitored_stats_health_verbose_log.enabled; const healthWithoutCapacity: MonitoredHealth = { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts index 880bdb5106053..1c32f257a26c8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts @@ -14,6 +14,17 @@ export default function createGetExistingTests({ getService }: FtrProviderContex const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + async function registerWithTaskManager(spaceId: string, id: string) { + await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule/${id}/_disable`) + .set('kbn-xsrf', 'foo') + .expect(204); + await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerting/rule/${id}/_enable`) + .set('kbn-xsrf', 'foo') + .expect(204); + } + describe('get existing', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/alerts_in_multiple_spaces'); @@ -24,10 +35,9 @@ export default function createGetExistingTests({ getService }: FtrProviderContex }); it('should show the alias resolve match for the non default space rule', async () => { - const response = await supertest.get( - `${getUrlPrefix(`chrisspace`)}/api/alerting/rule/100ff690-eaf9-11eb-9cd0-ed3f761f7f83` - ); - + const id = '100ff690-eaf9-11eb-9cd0-ed3f761f7f83'; + await registerWithTaskManager('chrisspace', id); + const response = await supertest.get(`${getUrlPrefix(`chrisspace`)}/api/alerting/rule/${id}`); expect(response.body.resolveResponse).to.eql({ outcome: 'aliasMatch', aliasTargetId: 'fdc84d89-1f92-5511-b4a4-65034392a07a', diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz index ae51e10c7cc8237d817a720bac73613df3314e31..2715060f56d66f69510aec20a802a2fc7a82b1a9 100644 GIT binary patch literal 1698 zcmV;T23`3diwFo?1^-|G17u-zVJ>QOZ*BnXS!;8uNE-g0U*S66HlyJth*h(-;3X#U zj_AQTQ>hjjMY(9xpm-|(eHy$3G?`7dJUhE*HkFE@-+teIuTMWA-)?fbJdRc{U&-YS zPtX4xFNDAj;+1dYfjnA9EcI7FhZO^wAh2<=FBZ85+{V)4(wYJnJkv*s?*&MIH9}T& z55cxILjYTzJA%k3AF--Xq`4w>&?Ebt%RNUMxq}8_73rSsq4=<$C~9g>*?dZ7Kwn4J3Ll~v&MiMjz9SNM z?vSNia@yZAT9yx{HZ&R#-~eTuOu0!my1+@otQ``55$LgM`-o1Qzc*`n>&hQz&x#D!u~LR7H5Sw2%h*R*CC)t8jm^4pg0 zTkcGOI)XT^h6!vRzF!9=&i1pC9M;O7<)3WinMepnx=OR^JgRq82adqle=s&{&s&xm zN*hzMtamXKcWTR7PMc%o?f7;xT8@wtaKjQGK%qS^*Tcab3YX@@ty!T6eQM-An3~c= z<~^sasy<}yWpT$39!+l|uBl~jy$ob=^UE)n-Ey7bJ5}PS_u26~_S3=8$GTP^ja%f) z{FS?Q4`})vkrqhJ&<}xWpN5%goQ9;iuY)u~h2@H($XvN*uhps?K!*Z6$Cs!Cbi^MG zbY83SFCj-2YHZ_{>n~IVwM!jN#j}p38BHHNst%84%J6RD+V~6eMGk{@q)Cujo2;q9 zfylIEg&me^+JMsTv^#cU8A2(LBUn;qwY9b=h3gvHLTPMM#&mfLszbJWr-s1s)eT>* zc3aiip9wi}vQ86pG0xu!lgbsEDhjmt9sApc2$NGN)8xhLw|8v)txRX3BA42b+BAUX zYsQ|bR7-whPLi!NVcC7#+-Ij6pAhBTx?O5M(P47%C?k(v1 zVD=8e9UAH8t>4oandB-J4fG3%x9Ld)E9(hHv6#YwK5hrg#-#7Sp0otbF5fjmZ&inD zw7N9~qpMDL19>s0bZEP>WQt&E3hM>vG@E?LIa8Hp7`fB$&uYzEzBTpP3EwN*^VR}4 z>wJiJ28X77uDg=;8tm5KU~9JS>${#{E&x5!z9@|+Yg1OtIv3MMz;dF$jM zLJ6^V1KUnFO+G%Ne$=gzvooIZLt3a%VudahS&^G$`YrDtPfGefOto1W{T+zGxi{e+ z_@pGeE#iUW8J1}!ah<{5nXv#v$+q9PL1KyV;1Zt*?mZZvApnEGhUpx;9=6PAJckJi z6E~CSqy#`x+uoefJoNj?s1VI>U)@~p>weI`<}1l#ypYWNGqCNi`5e)HiPy=Gmuz;A z;Pz`484gfaATZ$K7+?y2PmlL+*$P>^I97tGnPogD3~BD!#)Wz6T~jBWs^`R! z|HwLxEeAF%m{`vLGkzikaaNa`#w0v%qwM`Fy(3m#D;XT;?Pl;}hO23;d5H^TK4w|P-djx<3A}36sT+S`#tG_=2QTbdHoj&e>u{V zerxf4Y0oH{Z2QWs;;$AG4ADIUEc{rW14Vw~W?c{{^M+weZ&hzy*XV(5Z+dr|EgqH1 zv+nj`FcU|+j@zHu9ZPE84cz7vuX=Nj>1nR9HcL3F>UWQ5Fn_!s*2AZXB&xm=s@tKx zQRb2cl-jtpm8p#&)zr;&B)QuCVEu&WElKDvb{rbE#*4+MXGRq2;KrsG-48~=;vt-J zW2!tCIxSR>%Is}}3+2*Mnm79YIocys7``a5-2V*iVSgyvlOB-&6Q9%9UzzxrR7n@v s@<(o;y`R@7K5v4;e--ihIIsD{e+3SDe?G6_KJu>lcd=B+{5%@~04W1e{Qv*} literal 1684 zcmV;F25b2riwFP!000026YW`PbE?Q1{+?gqb-vw<1_42=nyCveF^M~(2j@(sT4)4i z(WXK1RQ~%kxCD%;Np2;xoSCYW3H>hp_Pad&1ixM7a(NuBV7{8m>zKSb6)BkedG>d#AaNPBUT5&)5#h^10v{Z&?id299DlliI3(fkBtE+EsZw;WuS*SuyC@~D63J@5TLa7921wqrvqs|yS zS2B2`!u&stvv;2@+J0?B!n;dCC?+LD1>2kFGX-=FYo0NEN{KDMWBI=2&Sj_}h~sJ) z!1m$mb&%t1Kg!8w?d)Fu$w!`!_;9Q#B}Or$Mptp*7)<;-eY5twWre2H2_;Ed4?|(6 zw4LRwH9_8v>$IZf7&!quDsutkJ7%R34sTGnG^TFd3I*s>WADyTRnr0%> z0;w7LE->xgFjI@ukTmzlAPrw-*rFiNA6&ENYE=)QLjjKE%2Wb6!uJL`&sF(d$Wiz@ z)4XN}3q?kqa+g)`ylbg?%Ln(0!=br6x|zB*{z89|!k`nWBBa*_W2kT_&}~U(M&-IX zq|_VrhM8JAUk;=QmgRYUtuD&px{kI`oY<5;TV8|Oi0R!ZA#i+U!_{iNc5VJ=LQb5l z(*(U3=TE|v@7m{exlL%JU1B_xZg?Vk#36{<2z=3^n30gg_r-$CE0oQ1CZSZh0=F7mh<7@RW&^gj zlC(PP)!}e!v~L@`K5r~QX{>&cn-A8eK@TOV-xEW1JeS&)(W1^rHe5k6#{jLbSCLwq z;g-svll0M z^D|Ne#1ecccG*L*d3j2Dube+0JuHJ>BaPw_hWc*LP!1p)&u zjsd3d-}HF@nyrwvi_c0hHM5LO!jMeQ)?c`%UUhY{sd-L(a=iaP@#k#N*m7Xgf{Ew+ z|KcZ75NGvLX+npG7_nw`6R_d>r@4ofRm__dAH} z+2~%phR5;U7nO(r+z3m-dXK z>2@Gr%l>L1!Vulkz{2+x6Ufp7JMV!&Hk-OWyH>n)LoE$$d(*$!Z1K2UnfJDL!?`ft zb=|?#?pk8!X6UvaIK?wPy05zW+9>0wrrq45p?QBhYJ?9{QBZt2RJJ2~Bb%ZM Date: Mon, 26 Jul 2021 23:28:25 -0400 Subject: [PATCH 3/7] Extract data.json.gz --- .../alerts_in_multiple_spaces/data.json | 266 ++++++++++++++++++ .../alerts_in_multiple_spaces/data.json.gz | Bin 1698 -> 0 bytes 2 files changed, 266 insertions(+) create mode 100644 x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json delete mode 100644 x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json new file mode 100644 index 0000000000000..6a3c19a4e3e40 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json @@ -0,0 +1,266 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "_reserved": true, + "color": "#00bfb3", + "description": "This is your default space!", + "disabledFeatures": [ + ], + "name": "Default" + }, + "type": "space", + "updated_at": "2021-07-22T14:20:59.665Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "action:ec9133a0-eaf8-11eb-9cd0-ed3f761f7f83", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".server-log", + "config": { + }, + "isMissingSecrets": false, + "name": "DefaultSpace_ServerLog", + "secrets": "2y7vVcU13UhyENUmeVaWsIdwqook820YW0BBcOte9zULmkbHWroz6MHykVrmu4T56ue7Mh8EvR/rvkfZnDiv9es0VoIfYSHrfG4YpCse2GB9z6RJfoZ9q0kPqkuB9w==" + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "action": "7.14.0" + }, + "references": [ + ], + "type": "action", + "updated_at": "2021-07-22T14:27:20.686Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "space:chrisspace", + "index": ".kibana_1", + "source": { + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "disabledFeatures": [ + ], + "name": "ChrisSpace" + }, + "type": "space", + "updated_at": "2021-07-22T14:27:34.992Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "chrisspace:action:0a646500-eaf9-11eb-9cd0-ed3f761f7f83", + "index": ".kibana_1", + "source": { + "action": { + "actionTypeId": ".server-log", + "config": { + }, + "isMissingSecrets": false, + "name": "ChrisSpace_ServerLog", + "secrets": "U7D3F+4QjUSrM5N4UtgNiYdHsaJUm6rgST/ZnltX2XBvuMyYAe2qf3fYeR92LBS3T5DYR0Y/Y/3Zid75uBye5SgDqYj5vqDrxeAWl0dbk+aCT3O/UvamsUw6CCOLCg==" + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "action": "7.14.0" + }, + "namespace": "chrisspace", + "references": [ + ], + "type": "action", + "updated_at": "2021-07-22T14:28:10.719Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "chrisspace:alert:100ff690-eaf9-11eb-9cd0-ed3f761f7f83", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": ".server-log", + "group": "query matched", + "params": { + "level": "info", + "message": "ChrisSpace" + } + } + ], + "alertTypeId": ".es-query", + "apiKey": "0t7cWMukFZQmePAkaHO6OdvopEeqrp+f7dOUbzRBjh0celnBAf9akf7qjaNHH6v4mbU122BNQQgDH+6Lbs3Z6P8lhLjtHE6vtzd4rbQ4OpBcDeODeRxfLGEzP7fja1VYXSFKiwE2RBBPOAvYVgBL8TjD7yleperxearq+pyYCbtH9Fot9MqW9WbIdZVBnmAuYFlgQsn5WTCWug==", + "apiKeyOwner": "elastic", + "consumer": "alerts", + "createdAt": "2021-07-22T14:28:20.925Z", + "createdBy": "elastic", + "enabled": true, + "executionStatus": { + "error": null, + "lastExecutionDate": "2021-07-26T17:09:15.394Z", + "status": "active" + }, + "meta": { + "versionApiKeyLastmodified": "7.14.0" + }, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "ChrisSpaceRule", + "notifyWhen": "onActiveAlert", + "params": { + "esQuery": "{\n \"query\":{\n \"match_all\" : {}\n }\n}", + "index": [ + ".kibana-event-log-*" + ], + "size": 100, + "threshold": [ + 0 + ], + "thresholdComparator": ">", + "timeField": "@timestamp", + "timeWindowSize": 5, + "timeWindowUnit": "m" + }, + "schedule": { + "interval": "1s" + }, + "scheduledTaskId": "10ced9c0-eaf9-11eb-9cd0-ed3f761f7f83", + "tags": [ + ], + "throttle": null, + "updatedAt": "2021-07-22T14:28:20.925Z", + "updatedBy": "elastic" + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "alert": "7.13.0" + }, + "namespace": "chrisspace", + "references": [ + { + "id": "0a646500-eaf9-11eb-9cd0-ed3f761f7f83", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2021-07-26T17:09:15.702Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "alert:efb7a0f0-eaf8-11eb-9cd0-ed3f761f7f83", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + { + "actionRef": "action_0", + "actionTypeId": ".server-log", + "group": "query matched", + "params": { + "level": "info", + "message": "DefaultSpace" + } + } + ], + "alertTypeId": ".es-query", + "apiKey": "VyZxQS+SspjAevrG2aitJ8haSBK4gOauShFddb+UoqEY1RllwP/wxtV58gOxIRg9VzNnQZlNiAM/RnHK6Uoho2PYndqf5tyCc/JrRhJGTEvKZA9UsSvUxTlwShAYaSDWLxB0w7ADUwbVAnYGRqKthLA7Qjz4rTLWjjVPfy0rNtFwPyGRVujIvb4W08h7NLrEy83+F4vB5kAhFQ==", + "apiKeyOwner": "elastic", + "consumer": "alerts", + "createdAt": "2021-07-22T14:27:26.734Z", + "createdBy": "elastic", + "enabled": true, + "executionStatus": { + "error": null, + "lastExecutionDate": "2021-07-26T17:09:15.393Z", + "status": "active" + }, + "meta": { + "versionApiKeyLastmodified": "7.14.0" + }, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "DefaultSpaceRule", + "notifyWhen": "onActiveAlert", + "params": { + "esQuery": "{\n \"query\":{\n \"match_all\" : {}\n }\n}", + "index": [ + ".kibana-event-log-*" + ], + "size": 100, + "threshold": [ + 0 + ], + "thresholdComparator": ">", + "timeField": "@timestamp", + "timeWindowSize": 5, + "timeWindowUnit": "m" + }, + "schedule": { + "interval": "1s" + }, + "scheduledTaskId": "f05c9380-eaf8-11eb-9cd0-ed3f761f7f83", + "tags": [ + ], + "throttle": null, + "updatedAt": "2021-07-22T14:27:26.734Z", + "updatedBy": "elastic" + }, + "coreMigrationVersion": "7.14.0", + "migrationVersion": { + "alert": "7.13.0" + }, + "references": [ + { + "id": "ec9133a0-eaf8-11eb-9cd0-ed3f761f7f83", + "name": "action_0", + "type": "action" + } + ], + "type": "alert", + "updated_at": "2021-07-26T17:09:15.746Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz deleted file mode 100644 index 2715060f56d66f69510aec20a802a2fc7a82b1a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1698 zcmV;T23`3diwFo?1^-|G17u-zVJ>QOZ*BnXS!;8uNE-g0U*S66HlyJth*h(-;3X#U zj_AQTQ>hjjMY(9xpm-|(eHy$3G?`7dJUhE*HkFE@-+teIuTMWA-)?fbJdRc{U&-YS zPtX4xFNDAj;+1dYfjnA9EcI7FhZO^wAh2<=FBZ85+{V)4(wYJnJkv*s?*&MIH9}T& z55cxILjYTzJA%k3AF--Xq`4w>&?Ebt%RNUMxq}8_73rSsq4=<$C~9g>*?dZ7Kwn4J3Ll~v&MiMjz9SNM z?vSNia@yZAT9yx{HZ&R#-~eTuOu0!my1+@otQ``55$LgM`-o1Qzc*`n>&hQz&x#D!u~LR7H5Sw2%h*R*CC)t8jm^4pg0 zTkcGOI)XT^h6!vRzF!9=&i1pC9M;O7<)3WinMepnx=OR^JgRq82adqle=s&{&s&xm zN*hzMtamXKcWTR7PMc%o?f7;xT8@wtaKjQGK%qS^*Tcab3YX@@ty!T6eQM-An3~c= z<~^sasy<}yWpT$39!+l|uBl~jy$ob=^UE)n-Ey7bJ5}PS_u26~_S3=8$GTP^ja%f) z{FS?Q4`})vkrqhJ&<}xWpN5%goQ9;iuY)u~h2@H($XvN*uhps?K!*Z6$Cs!Cbi^MG zbY83SFCj-2YHZ_{>n~IVwM!jN#j}p38BHHNst%84%J6RD+V~6eMGk{@q)Cujo2;q9 zfylIEg&me^+JMsTv^#cU8A2(LBUn;qwY9b=h3gvHLTPMM#&mfLszbJWr-s1s)eT>* zc3aiip9wi}vQ86pG0xu!lgbsEDhjmt9sApc2$NGN)8xhLw|8v)txRX3BA42b+BAUX zYsQ|bR7-whPLi!NVcC7#+-Ij6pAhBTx?O5M(P47%C?k(v1 zVD=8e9UAH8t>4oandB-J4fG3%x9Ld)E9(hHv6#YwK5hrg#-#7Sp0otbF5fjmZ&inD zw7N9~qpMDL19>s0bZEP>WQt&E3hM>vG@E?LIa8Hp7`fB$&uYzEzBTpP3EwN*^VR}4 z>wJiJ28X77uDg=;8tm5KU~9JS>${#{E&x5!z9@|+Yg1OtIv3MMz;dF$jM zLJ6^V1KUnFO+G%Ne$=gzvooIZLt3a%VudahS&^G$`YrDtPfGefOto1W{T+zGxi{e+ z_@pGeE#iUW8J1}!ah<{5nXv#v$+q9PL1KyV;1Zt*?mZZvApnEGhUpx;9=6PAJckJi z6E~CSqy#`x+uoefJoNj?s1VI>U)@~p>weI`<}1l#ypYWNGqCNi`5e)HiPy=Gmuz;A z;Pz`484gfaATZ$K7+?y2PmlL+*$P>^I97tGnPogD3~BD!#)Wz6T~jBWs^`R! z|HwLxEeAF%m{`vLGkzikaaNa`#w0v%qwM`Fy(3m#D;XT;?Pl;}hO23;d5H^TK4w|P-djx<3A}36sT+S`#tG_=2QTbdHoj&e>u{V zerxf4Y0oH{Z2QWs;;$AG4ADIUEc{rW14Vw~W?c{{^M+weZ&hzy*XV(5Z+dr|EgqH1 zv+nj`FcU|+j@zHu9ZPE84cz7vuX=Nj>1nR9HcL3F>UWQ5Fn_!s*2AZXB&xm=s@tKx zQRb2cl-jtpm8p#&)zr;&B)QuCVEu&WElKDvb{rbE#*4+MXGRq2;KrsG-48~=;vt-J zW2!tCIxSR>%Is}}3+2*Mnm79YIocys7``a5-2V*iVSgyvlOB-&6Q9%9UzzxrR7n@v s@<(o;y`R@7K5v4;e--ihIIsD{e+3SDe?G6_KJu>lcd=B+{5%@~04W1e{Qv*} From 6bbfa82a744dbc5d50743eda2f7e098e293a89fa Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 27 Jul 2021 00:22:33 -0400 Subject: [PATCH 4/7] Fix tests * The existing test data archive did not decrypt properly without the migration additions; it appears that was not encrypted with the same key that is used by the functional test server. I replaced that test data with fresh data that does decrypt properly. * I added empty ESO migration functions for the alert and action saved object types so they will be decrypted and re-encrypted during the index migration process. Note that the alias match test will not pass until the associated ESO service bugfix is applied. --- .../server/saved_objects/migrations.ts | 9 + .../server/saved_objects/migrations.ts | 9 + .../tests/alerting/get_existing.ts | 29 +- .../alerts_in_multiple_spaces/data.json | 347 ++++++++---------- 4 files changed, 192 insertions(+), 202 deletions(-) diff --git a/x-pack/plugins/actions/server/saved_objects/migrations.ts b/x-pack/plugins/actions/server/saved_objects/migrations.ts index 17932b6b90f97..4b14b3efbfcbf 100644 --- a/x-pack/plugins/actions/server/saved_objects/migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/migrations.ts @@ -46,10 +46,19 @@ export function getMigrations( pipeMigrations(addisMissingSecretsField) ); + // This empty migration is necessary to ensure that the saved object is decrypted with its old descriptor/ and re-encrypted with its new + // descriptor, if necessary. This is included because the saved object is being converted to `namespaceType: 'multiple-isolated'` in 8.0 + // (see the `convertToMultiNamespaceTypeVersion` field in the saved object type registration process). + const migrationActions800 = encryptedSavedObjects.createMigration( + (doc): doc is SavedObjectUnsanitizedDoc => true, + (doc) => doc // no-op + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 8969e3ad0fdef..1c23311d507c9 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -73,11 +73,20 @@ export function getMigrations( pipeMigrations(removeNullsFromSecurityRules) ); + // This empty migration is necessary to ensure that the saved object is decrypted with its old descriptor/ and re-encrypted with its new + // descriptor, if necessary. This is included because the saved object is being converted to `namespaceType: 'multiple-isolated'` in 8.0 + // (see the `convertToMultiNamespaceTypeVersion` field in the saved object type registration process). + const migrationActions800 = encryptedSavedObjects.createMigration( + (doc): doc is SavedObjectUnsanitizedDoc => true, + (doc) => doc // no-op + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts index 1c32f257a26c8..0f30db4d2f72f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts @@ -14,7 +14,8 @@ export default function createGetExistingTests({ getService }: FtrProviderContex const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - async function registerWithTaskManager(spaceId: string, id: string) { + /** Disables and enables the alerting rule (effectively asserting that decryption works properly at runtime). */ + async function registerWithTaskManager(id: string, spaceId: string = '') { await supertest .post(`${getUrlPrefix(spaceId)}/api/alerting/rule/${id}/_disable`) .set('kbn-xsrf', 'foo') @@ -25,6 +26,11 @@ export default function createGetExistingTests({ getService }: FtrProviderContex .expect(204); } + /** Resolves the alerting rule with a given ID in a given space. */ + async function resolveAlertingRule(id: string, spaceId: string = '') { + return supertest.get(`${getUrlPrefix(spaceId)}/api/alerting/rule/${id}`); + } + describe('get existing', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/alerts_in_multiple_spaces'); @@ -34,21 +40,24 @@ export default function createGetExistingTests({ getService }: FtrProviderContex await esArchiver.unload('x-pack/test/functional/es_archives/alerts_in_multiple_spaces'); }); - it('should show the alias resolve match for the non default space rule', async () => { - const id = '100ff690-eaf9-11eb-9cd0-ed3f761f7f83'; - await registerWithTaskManager('chrisspace', id); - const response = await supertest.get(`${getUrlPrefix(`chrisspace`)}/api/alerting/rule/${id}`); + it('should resolve to an alias match for the non default space rule', async () => { + const oldObjectId = '8855c6f7-8a73-52a1-bd36-3c929ce3ecb6'; + const newObjectId = '80827100-ee87-11eb-8dcd-cb59df51587b'; // This ID is not found in the test data archive; it is deterministically generated when the alerting rule is migrated to 8.0 + const spaceId = 'customspace'; + + await registerWithTaskManager(oldObjectId, spaceId); + const response = await resolveAlertingRule(newObjectId, spaceId); expect(response.body.resolveResponse).to.eql({ outcome: 'aliasMatch', - aliasTargetId: 'fdc84d89-1f92-5511-b4a4-65034392a07a', + aliasTargetId: oldObjectId, }); }); - it('should show the absolute resolve match for the default space rule', async () => { - const response = await supertest.get( - `${getUrlPrefix(``)}/api/alerting/rule/efb7a0f0-eaf8-11eb-9cd0-ed3f761f7f83` - ); + it('should resolve to an exact match for the default space rule', async () => { + const objectId = '4c23c3a0-ee87-11eb-8dcd-cb59df51587b'; + await registerWithTaskManager(objectId); + const response = await resolveAlertingRule(objectId); expect(response.body.resolveResponse).to.eql({ outcome: 'exactMatch', }); diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json index 6a3c19a4e3e40..126340481a379 100644 --- a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json +++ b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json @@ -1,25 +1,20 @@ { "type": "doc", "value": { - "id": "space:default", + "id": "space:customspace", "index": ".kibana_1", "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "space": "6.6.0" + "space" : { + "name" : "CustomSpace", + "disabledFeatures" : [ ] }, - "references": [ - ], - "space": { - "_reserved": true, - "color": "#00bfb3", - "description": "This is your default space!", - "disabledFeatures": [ - ], - "name": "Default" + "type" : "space", + "references" : [ ], + "migrationVersion" : { + "space" : "6.6.0" }, - "type": "space", - "updated_at": "2021-07-22T14:20:59.665Z" + "coreMigrationVersion" : "7.14.0", + "updated_at" : "2021-07-27T03:04:22.349Z" }, "type": "_doc" } @@ -28,25 +23,23 @@ { "type": "doc", "value": { - "id": "action:ec9133a0-eaf8-11eb-9cd0-ed3f761f7f83", + "id": "action:3fd484e0-ee87-11eb-8dcd-cb59df51587b", "index": ".kibana_1", "source": { - "action": { - "actionTypeId": ".server-log", - "config": { - }, - "isMissingSecrets": false, - "name": "DefaultSpace_ServerLog", - "secrets": "2y7vVcU13UhyENUmeVaWsIdwqook820YW0BBcOte9zULmkbHWroz6MHykVrmu4T56ue7Mh8EvR/rvkfZnDiv9es0VoIfYSHrfG4YpCse2GB9z6RJfoZ9q0kPqkuB9w==" + "action" : { + "actionTypeId" : ".server-log", + "name" : "DefaultSpace_Connector", + "isMissingSecrets" : false, + "config" : { }, + "secrets" : "AEN6aQfiii4lJPfABP3lWh0wk2DITczt6hUqeK90mzMIJlEy8FVkRT2vHP4UIdVggLB3HWcKKgENmOk2fSQG2HRbQw6rpluhhSL+lxdlIYv9Sc3VsBUlAC7MqvEeNA==" }, - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "action": "7.14.0" + "type" : "action", + "references" : [ ], + "migrationVersion" : { + "action" : "7.14.0" }, - "references": [ - ], - "type": "action", - "updated_at": "2021-07-22T14:27:20.686Z" + "coreMigrationVersion" : "7.14.0", + "updated_at" : "2021-07-27T03:03:42.396Z" }, "type": "_doc" } @@ -55,22 +48,24 @@ { "type": "doc", "value": { - "id": "space:chrisspace", + "id": "customspace:action:7c6fcef0-ee87-11eb-8dcd-cb59df51587b", "index": ".kibana_1", "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "space": "6.6.0" + "action" : { + "actionTypeId" : ".server-log", + "name" : "CustomSpace_Connector", + "isMissingSecrets" : false, + "config" : { }, + "secrets" : "Iwyym3VrXIBW4CxA/RdJUDjXjC4G498PqyjqdaMNFo5GlbnZX0Ynp1rpM95eG9WclGBnXTc4viwDs3QZPquU8Smv7Izvx53tt7npIyJ9qtGaAaQZ681f5gNtJJA/bg==" }, - "references": [ - ], - "space": { - "disabledFeatures": [ - ], - "name": "ChrisSpace" + "type" : "action", + "references" : [ ], + "namespace" : "customspace", + "migrationVersion" : { + "action" : "7.14.0" }, - "type": "space", - "updated_at": "2021-07-22T14:27:34.992Z" + "coreMigrationVersion" : "7.14.0", + "updated_at" : "2021-07-27T03:05:24.072Z" }, "type": "_doc" } @@ -79,107 +74,76 @@ { "type": "doc", "value": { - "id": "chrisspace:action:0a646500-eaf9-11eb-9cd0-ed3f761f7f83", + "id": "alert:4c23c3a0-ee87-11eb-8dcd-cb59df51587b", "index": ".kibana_1", "source": { - "action": { - "actionTypeId": ".server-log", - "config": { + "alert" : { + "params" : { + "esQuery" : "{\n \"query\":{\n \"match_all\" : {}\n }\n}", + "size" : 100, + "timeWindowSize" : 5, + "timeWindowUnit" : "m", + "threshold" : [ + 0 + ], + "thresholdComparator" : ">", + "index" : [ + ".kibana-event-log-*" + ], + "timeField" : "@timestamp" }, - "isMissingSecrets": false, - "name": "ChrisSpace_ServerLog", - "secrets": "U7D3F+4QjUSrM5N4UtgNiYdHsaJUm6rgST/ZnltX2XBvuMyYAe2qf3fYeR92LBS3T5DYR0Y/Y/3Zid75uBye5SgDqYj5vqDrxeAWl0dbk+aCT3O/UvamsUw6CCOLCg==" - }, - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "action": "7.14.0" - }, - "namespace": "chrisspace", - "references": [ - ], - "type": "action", - "updated_at": "2021-07-22T14:28:10.719Z" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "chrisspace:alert:100ff690-eaf9-11eb-9cd0-ed3f761f7f83", - "index": ".kibana_1", - "source": { - "alert": { - "actions": [ + "consumer" : "alerts", + "schedule" : { + "interval" : "1m" + }, + "tags" : [ ], + "name" : "DefaultSpace_Rule", + "throttle" : null, + "actions" : [ { - "actionRef": "action_0", - "actionTypeId": ".server-log", - "group": "query matched", - "params": { - "level": "info", - "message": "ChrisSpace" - } + "group" : "query matched", + "params" : { + "level" : "info", + "message" : "DefaultSpace_Message" + }, + "actionRef" : "action_0", + "actionTypeId" : ".server-log" } ], - "alertTypeId": ".es-query", - "apiKey": "0t7cWMukFZQmePAkaHO6OdvopEeqrp+f7dOUbzRBjh0celnBAf9akf7qjaNHH6v4mbU122BNQQgDH+6Lbs3Z6P8lhLjtHE6vtzd4rbQ4OpBcDeODeRxfLGEzP7fja1VYXSFKiwE2RBBPOAvYVgBL8TjD7yleperxearq+pyYCbtH9Fot9MqW9WbIdZVBnmAuYFlgQsn5WTCWug==", - "apiKeyOwner": "elastic", - "consumer": "alerts", - "createdAt": "2021-07-22T14:28:20.925Z", - "createdBy": "elastic", - "enabled": true, - "executionStatus": { - "error": null, - "lastExecutionDate": "2021-07-26T17:09:15.394Z", - "status": "active" - }, - "meta": { - "versionApiKeyLastmodified": "7.14.0" - }, - "muteAll": false, - "mutedInstanceIds": [ - ], - "name": "ChrisSpaceRule", - "notifyWhen": "onActiveAlert", - "params": { - "esQuery": "{\n \"query\":{\n \"match_all\" : {}\n }\n}", - "index": [ - ".kibana-event-log-*" - ], - "size": 100, - "threshold": [ - 0 - ], - "thresholdComparator": ">", - "timeField": "@timestamp", - "timeWindowSize": 5, - "timeWindowUnit": "m" + "enabled" : true, + "alertTypeId" : ".es-query", + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : "elastic", + "apiKey" : "SQxINbeYowvAYUoULov4NCv3ff5xfbP0m7tmGbjwwzqLmzVswUctuFX3MzJI5knrFjXF4+xQbXwbwcu7ZmO/VW1IRli2klXlx/GfczHraMD5wmN+OQlvJfrubgZ9Pfm0iSyoHYMyM6MsYqVuE/cQvTNB0LPzmjFAC71gQlBNV52GiMGxSq82BJMn/ogPoX4Vy1UZBoTTh0AK5Q==", + "createdBy" : "elastic", + "updatedBy" : "elastic", + "createdAt" : "2021-07-27T03:04:03.758Z", + "updatedAt" : "2021-07-27T03:04:03.758Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "active", + "lastExecutionDate" : "2021-07-27T03:07:16.475Z", + "error" : null }, - "schedule": { - "interval": "1s" + "meta" : { + "versionApiKeyLastmodified" : "7.14.0" }, - "scheduledTaskId": "10ced9c0-eaf9-11eb-9cd0-ed3f761f7f83", - "tags": [ - ], - "throttle": null, - "updatedAt": "2021-07-22T14:28:20.925Z", - "updatedBy": "elastic" + "scheduledTaskId" : "4d350c90-ee87-11eb-8dcd-cb59df51587b" }, - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "alert": "7.13.0" - }, - "namespace": "chrisspace", - "references": [ + "type" : "alert", + "references" : [ { - "id": "0a646500-eaf9-11eb-9cd0-ed3f761f7f83", - "name": "action_0", - "type": "action" + "id" : "3fd484e0-ee87-11eb-8dcd-cb59df51587b", + "name" : "action_0", + "type" : "action" } ], - "type": "alert", - "updated_at": "2021-07-26T17:09:15.702Z" + "migrationVersion" : { + "alert" : "7.13.0" + }, + "coreMigrationVersion" : "7.14.0", + "updated_at" : "2021-07-27T03:07:17.288Z" }, "type": "_doc" } @@ -188,78 +152,77 @@ { "type": "doc", "value": { - "id": "alert:efb7a0f0-eaf8-11eb-9cd0-ed3f761f7f83", + "id": "customspace:alert:80827100-ee87-11eb-8dcd-cb59df51587b", "index": ".kibana_1", "source": { - "alert": { - "actions": [ - { - "actionRef": "action_0", - "actionTypeId": ".server-log", - "group": "query matched", - "params": { - "level": "info", - "message": "DefaultSpace" - } - } - ], - "alertTypeId": ".es-query", - "apiKey": "VyZxQS+SspjAevrG2aitJ8haSBK4gOauShFddb+UoqEY1RllwP/wxtV58gOxIRg9VzNnQZlNiAM/RnHK6Uoho2PYndqf5tyCc/JrRhJGTEvKZA9UsSvUxTlwShAYaSDWLxB0w7ADUwbVAnYGRqKthLA7Qjz4rTLWjjVPfy0rNtFwPyGRVujIvb4W08h7NLrEy83+F4vB5kAhFQ==", - "apiKeyOwner": "elastic", - "consumer": "alerts", - "createdAt": "2021-07-22T14:27:26.734Z", - "createdBy": "elastic", - "enabled": true, - "executionStatus": { - "error": null, - "lastExecutionDate": "2021-07-26T17:09:15.393Z", - "status": "active" - }, - "meta": { - "versionApiKeyLastmodified": "7.14.0" - }, - "muteAll": false, - "mutedInstanceIds": [ - ], - "name": "DefaultSpaceRule", - "notifyWhen": "onActiveAlert", - "params": { - "esQuery": "{\n \"query\":{\n \"match_all\" : {}\n }\n}", - "index": [ - ".kibana-event-log-*" - ], - "size": 100, - "threshold": [ + "alert" : { + "params" : { + "esQuery" : "{\n \"query\":{\n \"match_all\" : {}\n }\n}", + "size" : 100, + "timeWindowSize" : 5, + "timeWindowUnit" : "m", + "threshold" : [ 0 ], - "thresholdComparator": ">", - "timeField": "@timestamp", - "timeWindowSize": 5, - "timeWindowUnit": "m" + "thresholdComparator" : ">", + "index" : [ + ".kibana-event-log-*" + ], + "timeField" : "@timestamp" }, - "schedule": { - "interval": "1s" + "consumer" : "alerts", + "schedule" : { + "interval" : "1m" }, - "scheduledTaskId": "f05c9380-eaf8-11eb-9cd0-ed3f761f7f83", - "tags": [ + "tags" : [ ], + "name" : "CustomSpace_Rule", + "throttle" : null, + "actions" : [ + { + "group" : "query matched", + "params" : { + "level" : "info", + "message" : "CustomSpace_Message" + }, + "actionRef" : "action_0", + "actionTypeId" : ".server-log" + } ], - "throttle": null, - "updatedAt": "2021-07-22T14:27:26.734Z", - "updatedBy": "elastic" - }, - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "alert": "7.13.0" + "enabled" : true, + "alertTypeId" : ".es-query", + "notifyWhen" : "onActiveAlert", + "apiKeyOwner" : "elastic", + "apiKey" : "cMYA0GuObN+7rwAAJn+/gKpj5GeVTxeXq9CcrueMvyIS2vplmvDyMaZflnnsp8t8L5BnVsAQak6TYl3T7PI5AVbmYROa+2zqam7CffJ3x9gDZ+Kd8+wY++wcIuZymVppom94iRhxhYQvy+6yHeNullBdcCl8kvOJPEWEbwAPiiofLDwW/rY1AdbZxyJ5DNr5Ay7GnnAZQBW+eg==", + "createdBy" : "elastic", + "updatedBy" : "elastic", + "createdAt" : "2021-07-27T03:05:30.977Z", + "updatedAt" : "2021-07-27T03:05:30.977Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "executionStatus" : { + "status" : "active", + "lastExecutionDate" : "2021-07-27T03:07:40.462Z", + "error" : null + }, + "meta" : { + "versionApiKeyLastmodified" : "7.14.0" + }, + "scheduledTaskId" : "80c8c8d0-ee87-11eb-8dcd-cb59df51587b" }, - "references": [ + "type" : "alert", + "references" : [ { - "id": "ec9133a0-eaf8-11eb-9cd0-ed3f761f7f83", - "name": "action_0", - "type": "action" + "id" : "7c6fcef0-ee87-11eb-8dcd-cb59df51587b", + "name" : "action_0", + "type" : "action" } ], - "type": "alert", - "updated_at": "2021-07-26T17:09:15.746Z" + "namespace" : "customspace", + "migrationVersion" : { + "alert" : "7.13.0" + }, + "coreMigrationVersion" : "7.14.0", + "updated_at" : "2021-07-27T03:07:41.410Z" }, "type": "_doc" } From 15e71aa88e3275367ac648c619319116737a7a12 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 27 Jul 2021 14:37:24 -0400 Subject: [PATCH 5/7] Fix ESO migration decryption for converted saved object types A bug in the algorithm neglected to differentiate when a converted object may have had its ID changed (this happens when an object exists in a non-default space and then it is converted). This commit fixes the bug and adds several more unit tests and integration tests to exercise different permutations of migrations. --- .../server/create_migration.test.ts | 124 +++++++----- .../server/create_migration.ts | 33 ++- .../encrypted_saved_objects_service.test.ts | 190 ++++++++++++++---- .../crypto/encrypted_saved_objects_service.ts | 26 ++- .../api_consumer_plugin/server/index.ts | 17 +- .../encrypted_saved_objects/data.json | 66 +++++- .../tests/encrypted_saved_objects_api.ts | 109 ++++++++-- 7 files changed, 452 insertions(+), 113 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts index 548340fbb6463..7301624f65094 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts @@ -170,10 +170,14 @@ describe('createMigration()', () => { describe('uses the object `namespaces` field to populate the descriptor when the migration context indicates this type is being converted', () => { const doTest = ({ objectNamespace, + objectNamespaces, decryptDescriptorNamespace, + encryptDescriptorNamespace, }: { objectNamespace: string | undefined; + objectNamespaces: string[] | undefined; decryptDescriptorNamespace: string | undefined; + encryptDescriptorNamespace: string | undefined; }) => { const instantiateServiceWithLegacyType = jest.fn(() => encryptedSavedObjectsServiceMock.create() @@ -189,56 +193,82 @@ describe('createMigration()', () => { }, (doc) => doc ); - - const attributes = { - firstAttr: 'first_attr', - }; - - encryptionSavedObjectService.decryptAttributesSync.mockReturnValueOnce(attributes); - encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); - - noopMigration( - { - id: '123', - type: 'known-type-1', - namespaces: objectNamespace ? [objectNamespace] : [], + const attributes = { firstAttr: 'first_attr' }; + + ['7.99.99', '8.0.0'].forEach((migrationVersion, i) => { + encryptionSavedObjectService.decryptAttributesSync.mockReturnValueOnce(attributes); + encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); + noopMigration( + { + id: '123', + originId: 'some-origin-id', + type: 'known-type-1', + namespace: objectNamespace, + namespaces: objectNamespaces, + attributes, + }, + migrationMocks.createContext({ + migrationVersion, // test works with any version <= 8.0.0 + convertToMultiNamespaceTypeVersion: '8.0.0', + }) + ); + expect(encryptionSavedObjectService.decryptAttributesSync).toHaveBeenNthCalledWith( + i + 1, + { id: '123', type: 'known-type-1', namespace: decryptDescriptorNamespace }, attributes, - }, - migrationMocks.createContext({ - migrationVersion: '8.0.0', - convertToMultiNamespaceTypeVersion: '8.0.0', - }) - ); - - expect(encryptionSavedObjectService.decryptAttributesSync).toHaveBeenCalledWith( - { - id: '123', - type: 'known-type-1', - namespace: decryptDescriptorNamespace, - }, - attributes, - { convertToMultiNamespaceType: true } - ); - - expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( - { - id: '123', - type: 'known-type-1', - }, - attributes - ); + { convertToMultiNamespaceType: true, originId: 'some-origin-id' } + ); + expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenNthCalledWith( + i + 1, + { id: '123', type: 'known-type-1', namespace: encryptDescriptorNamespace }, + attributes + ); + }); }; - it('when namespaces is an empty array', () => { - doTest({ objectNamespace: undefined, decryptDescriptorNamespace: undefined }); - }); - - it('when the first namespace element is "default"', () => { - doTest({ objectNamespace: 'default', decryptDescriptorNamespace: undefined }); - }); - - it('when the first namespace element is another string', () => { - doTest({ objectNamespace: 'foo', decryptDescriptorNamespace: 'foo' }); + [undefined, 'foo'].forEach((objectNamespace) => { + // In the test cases below, we test what will happen if an object has both `namespace` and `namespaces` fields. This will not happen + // in normal operation, as when Kibana converts an object from a single- to a multi-namespace type, it removes the `namespace` field + // and adds the `namespaces` field. The tests below are for completeness. + const namespaceDescription = objectNamespace ? 'defined' : undefined; + + it(`when namespaces is undefined and namespace is ${namespaceDescription}`, () => { + doTest({ + objectNamespace, + objectNamespaces: undefined, + decryptDescriptorNamespace: objectNamespace, + encryptDescriptorNamespace: objectNamespace, + }); + }); + + it(`when namespaces is an empty array and namespace is ${namespaceDescription}`, () => { + // The `namespaces` field should never be an empty array, but we test for it anyway. In this case, we fall back to attempting to + // decrypt with the `namespace` field; if that doesn't work, the ESO service will try again without it. + doTest({ + objectNamespace, + objectNamespaces: [], + decryptDescriptorNamespace: objectNamespace, + encryptDescriptorNamespace: objectNamespace, + }); + }); + + it(`when namespaces is a non-empty array (default space) and namespace is ${namespaceDescription}`, () => { + doTest({ + objectNamespace, + objectNamespaces: ['default', 'additional-spaces-are-ignored'], + decryptDescriptorNamespace: undefined, + encryptDescriptorNamespace: objectNamespace, + }); + }); + + it(`when namespaces is a non-empty array (custom space) and namespace is ${namespaceDescription}`, () => { + doTest({ + objectNamespace, + objectNamespaces: ['custom', 'additional-spaces-are-ignored'], + decryptDescriptorNamespace: 'custom', + encryptDescriptorNamespace: objectNamespace, + }); + }); }); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts index beace2b17fe08..e4e79d8e84604 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts @@ -5,6 +5,8 @@ * 2.0. */ +import Semver from 'semver'; + import type { SavedObjectMigrationContext, SavedObjectMigrationFn, @@ -65,19 +67,37 @@ export const getCreateMigration = ( return encryptedDoc; } + // After it is converted, the object's old ID is stored in the `originId` field. In addition, objects that are imported a certain way + // may have this field set, but it would not be necessary to use this to decrypt saved object attributes. + const originId = encryptedDoc.originId; + + // If an object is slated to be converted, it should be decrypted flexibly: + // * If this is an index migration: + // a. If there is one or more pending migration _before_ the conversion, the object will be decrypted and re-encrypted with its + // namespace in the descriptor. Then, after the conversion, the object will be decrypted with its namespace and old ID in the + // descriptor and re-encrypted with its new ID (without a namespace) in the descriptor. + // b. If there are no pending migrations before the conversion, then after the conversion the object will be decrypted with its + // namespace and old ID in the descriptor and re-encrypted with its new ID (without a namespace) in the descriptor. + // * If this is *not* an index migration, then it is a single document migration. In that case, the object will be decrypted and + // re-encrypted without a namespace in the descriptor. + // To account for these different scenarios, when this field is set, the ESO service will attempt several different permutations of + // the descriptor when decrypting the object. + const convertToMultiNamespaceType = + !!context.convertToMultiNamespaceTypeVersion && + Semver.lte(context.migrationVersion, context.convertToMultiNamespaceTypeVersion); + // If an object has been converted right before this migration function is called, it will no longer have a `namespace` field, but it // will have a `namespaces` field; in that case, the first/only element in that array should be used as the namespace in the descriptor // during decryption. - const convertToMultiNamespaceType = - context.convertToMultiNamespaceTypeVersion === context.migrationVersion; - const decryptDescriptorNamespace = convertToMultiNamespaceType - ? normalizeNamespace(encryptedDoc.namespaces?.[0]) // `namespaces` contains string values, but we need to normalize this to the namespace ID representation - : encryptedDoc.namespace; + const decryptDescriptorNamespace = + convertToMultiNamespaceType && encryptedDoc.namespaces?.length + ? normalizeNamespace(encryptedDoc.namespaces[0]) // `namespaces` contains string values, but we need to normalize this to the namespace ID representation + : encryptedDoc.namespace; const { id, type } = encryptedDoc; // These descriptors might have a `namespace` that is undefined. That is expected for multi-namespace and namespace-agnostic types. const decryptDescriptor = { id, type, namespace: decryptDescriptorNamespace }; - const encryptDescriptor = { id, type, namespace: encryptedDoc.namespace }; + const encryptDescriptor = { id, type, namespace: encryptedDoc.namespace }; // It would be preferable to rely on getDescriptorNamespace() here, but that requires the SO type registry which can only be retrieved from a promise, and this is not an async function // decrypt the attributes using the input type definition // then migrate the document @@ -87,6 +107,7 @@ export const getCreateMigration = ( mapAttributes(encryptedDoc, (inputAttributes) => inputService.decryptAttributesSync(decryptDescriptor, inputAttributes, { convertToMultiNamespaceType, + originId, }) ), context diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index 5ac6467e8d78b..5bd1d6a5b355e 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -820,53 +820,163 @@ describe('#decryptAttributes', () => { ); }); - it('retries decryption without namespace if incorrect namespace is provided and convertToMultiNamespaceType option is enabled', async () => { - const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + describe('with convertToMultiNamespaceType option', () => { + it('retries decryption without namespace', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; - service.registerType({ - type: 'known-type-1', - attributesToEncrypt: new Set(['attrThree']), + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // namespace was not included in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes, + { user: mockUser, convertToMultiNamespaceType: true } + ) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt without the namespace in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + mockUser + ); }); - const encryptedAttributes = service.encryptAttributesSync( - { type: 'known-type-1', id: 'object-id' }, // namespace was not included in descriptor during encryption - attributes - ); - expect(encryptedAttributes).toEqual({ - attrOne: 'one', - attrTwo: 'two', - attrThree: expect.not.stringMatching(/^three$/), + it('retries decryption without originId (old object ID)', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // old object ID was not used in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes( + { type: 'known-type-1', id: 'object-id', namespace: undefined }, + encryptedAttributes, + { user: mockUser, convertToMultiNamespaceType: true, originId: 'old-object-id' } + ) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the old object ID in the descriptor (fail) + expect.anything(), + `["known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt with the current object ID in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], + { type: 'known-type-1', id: 'object-id', namespace: undefined }, + mockUser + ); }); - const mockUser = mockAuthenticatedUser(); - await expect( - service.decryptAttributes( + it('retries decryption without namespace *and* without originId (old object ID)', async () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, // namespace and old object ID were not used in descriptor during encryption + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const mockUser = mockAuthenticatedUser(); + await expect( + service.decryptAttributes( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes, + { user: mockUser, convertToMultiNamespaceType: true, originId: 'old-object-id' } + ) + ).resolves.toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(4); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 1, // first attempted to decrypt with the old object ID and namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 2, // then attempted to decrypt with the old object ID and no namespace in the descriptor (fail) + expect.anything(), + `["known-type-1","old-object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 3, // first attempted to decrypt with the current object ID and namespace in the descriptor (fail) + expect.anything(), + `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( + 4, // then attempted to decrypt with the current object ID and no namespace in the descriptor (success) + expect.anything(), + `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` + ); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( + ['attrThree'], { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, - encryptedAttributes, - { user: mockUser, convertToMultiNamespaceType: true } - ) - ).resolves.toEqual({ - attrOne: 'one', - attrTwo: 'two', - attrThree: 'three', + mockUser + ); }); - expect(mockNodeCrypto.decrypt).toHaveBeenCalledTimes(2); - expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( - 1, // first attempted to decrypt with the namespace in the descriptor (fail) - expect.anything(), - `["object-ns","known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` - ); - expect(mockNodeCrypto.decrypt).toHaveBeenNthCalledWith( - 2, // then attempted to decrypt without the namespace in the descriptor (success) - expect.anything(), - `["known-type-1","object-id",{"attrOne":"one","attrTwo":"two"}]` - ); - expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1); - expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith( - ['attrThree'], - { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, - mockUser - ); }); it('decrypts even if no attributes are included into AAD', async () => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index a8b53ff47c471..9829046bbddcd 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -71,6 +71,11 @@ interface DecryptParameters extends CommonParameters { * decrypted during object migration, the object was never encrypted with its namespace in the descriptor portion of the AAD. */ convertToMultiNamespaceType?: boolean; + /** + * If the originId (old object ID) is present and the object is being converted from a single-namespace type to a multi-namespace type, + * we will attempt to decrypt with both the old object ID and the current object ID. + */ + originId?: string; } interface EncryptedSavedObjectsServiceOptions { @@ -484,11 +489,22 @@ export class EncryptedSavedObjectsService { ); } if (!encryptionAADs.length) { - encryptionAADs.push(this.getAAD(typeDefinition, descriptor, attributes)); - if (params?.convertToMultiNamespaceType && descriptor.namespace) { - // This is happening during a migration; create an alternate AAD for decrypting the object attributes by stripping out the namespace from the descriptor. - const { namespace, ...alternateDescriptor } = descriptor; - encryptionAADs.push(this.getAAD(typeDefinition, alternateDescriptor, attributes)); + if (params?.convertToMultiNamespaceType) { + // The object is either pending conversion to a multi-namespace type, or it was just converted. We may need to attempt to decrypt + // it with several different descriptors depending upon how the migrations are structured, and whether this is a full index + // migration or a single document migration. Note that the originId is set either when the document is converted _or_ when it is + // imported with "createNewCopies: false", so we have to try with and without it. + const ids = params.originId ? [params.originId, descriptor.id] : [descriptor.id]; + for (const id of ids) { + const decryptDescriptor = { ...descriptor, id }; + encryptionAADs.push(this.getAAD(typeDefinition, decryptDescriptor, attributes)); + if (descriptor.namespace) { + const { namespace, ...alternateDescriptor } = decryptDescriptor; + encryptionAADs.push(this.getAAD(typeDefinition, alternateDescriptor, attributes)); + } + } + } else { + encryptionAADs.push(this.getAAD(typeDefinition, descriptor, attributes)); } } try { diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts index 96a0a3b2fa427..b68ddf46a3d39 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -134,7 +134,8 @@ function defineTypeWithMigration(core: CoreSetup, deps: PluginsSet core.savedObjects.registerType({ name: SAVED_OBJECT_WITH_MIGRATION_TYPE, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', // in data.json, we simulate that existing objects were created with `namespaceType: 'single'` + convertToMultiNamespaceTypeVersion: '8.0.0', // in this version we convert from a single-namespace type to a "share-capable" multi-namespace isolated type mappings: { properties: { nonEncryptedAttribute: { @@ -195,6 +196,20 @@ function defineTypeWithMigration(core: CoreSetup, deps: PluginsSet }, typePriorTo790 ), + + // NOTE FOR MAINTAINERS: do not add any more migrations before 8.0.0 unless you regenerate the test data for two of the objects in + // data.json: '362828f0-eef2-11eb-9073-11359682300a' and '36448a90-eef2-11eb-9073-11359682300a. These are used in the test cases 'for + // a saved object that does not need to be migrated before it is converted'. + + // This empty migration is necessary to ensure that the saved object is decrypted with its old descriptor/ and re-encrypted with its + // new descriptor, if necessary. This is included because the saved object is being converted to `namespaceType: 'multiple-isolated'` + // in 8.0.0 (see the `convertToMultiNamespaceTypeVersion` field in the saved object type registration process). + '8.0.0': deps.encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc // no-op + ), }, }); } diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json index 88ec54cdf3a54..71ac4dfc974d4 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json @@ -223,6 +223,70 @@ } } +{ + "type": "doc", + "value": { + "id": "custom-space:saved-object-with-migration:a67c6950-eed8-11eb-9a62-032b4e4049d1", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "BIOBsx5SjLq3ZQdOJv06XeCAMY9ZrYj8K5bcGa5+wpd3TeT2sqln1+9AGblnfxT7LXRI3sLWQ900+wRQzBhJYx8PNKH+Yw+GdeESpu73PFHdWt/52cJKr+b4EPALFc00tIMEDHdT9FyQhqJ7nV8UpwtjcuTp9SA=", + "nonEncryptedAttribute": "elastic" + }, + "type": "saved-object-with-migration", + "references": [], + "namespace": "custom-space", + "migrationVersion": { + "saved-object-with-migration": "7.7.0" + }, + "updated_at": "2021-07-27T12:46:23.881Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "saved-object-with-migration:362828f0-eef2-11eb-9073-11359682300a", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "wWDAtF/5PkCb5BxjfWyRxoIoHbJXlb5cGAKg9ztZ1Bz9Zwo0/xf2yTa3Gq/CbYrvey/F9FZkZOUk03USPaqa5mfFO8FhORkfmNLQaPhgCIDNd6SbIhN8RYkqWVTYSVgcZrwes+VwiTUZ29mCJprVSHwXdyAOy4g=", + "nonEncryptedAttribute": "elastic-migrated", + "additionalEncryptedAttribute": "mszSQj0+Wv7G6kZJQsqf7CWwjJwwyriMlBcUjSHTLlj+tljbLTb7PI7gR07S9l7BXd3Lquc5PeOJifl2HvnTh8s871d/WdtIvt2K/ggwA2ae9NH6ui8A15cuPlXiGO612qccsIyBzhsftFyWJNuLBApmqeEy7HFe" + }, + "type": "saved-object-with-migration", + "references": [], + "migrationVersion": { + "saved-object-with-migration": "7.9.0" + }, + "updated_at": "2021-07-27T15:49:22.324Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "custom-space:saved-object-with-migration:36448a90-eef2-11eb-9073-11359682300a", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "33lfpnBI136UfkdcQLzovzBXdUaeDouN0Z32qkVutgZJ5SU60hMtaHWXNkaU9DGy9jtr0ptwm6FCYmZbyDrlGMwyZP2n0PzMhwW9fRcBh7he12Cm1mImWTrxgYoRtc1MX20/orbINx5VnuNl1Ide7htAm1oPRjM=", + "nonEncryptedAttribute": "elastic-migrated", + "additionalEncryptedAttribute": "e2rsxBijtMGcdw7A+WAWJNlLOhQCZnEP1sdcHxVO5aQouiUVeI1OTFcOY3h/+iZBlSGvZdGRURgimrSNc0HRicemZx3o4v1gVw0JX3RRatzdl02v3GJoFzBWfQGyf3xhNNWmkweGJrFQqr2kfdKjIHbdVmMt4LZj" + }, + "type": "saved-object-with-migration", + "references": [], + "namespace": "custom-space", + "migrationVersion": { + "saved-object-with-migration": "7.9.0" + }, + "updated_at": "2021-07-27T15:49:22.509Z" + } + } +} + { "type": "doc", "value": { @@ -367,4 +431,4 @@ "updated_at": "2020-06-17T16:29:27.563Z" } } -} \ No newline at end of file +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 0b01f4f385da6..311228424afe3 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -527,20 +527,103 @@ export default function ({ getService }: FtrProviderContext) { ); }); - it('migrates unencrypted fields on saved objects', async () => { - const { body: decryptedResponse } = await supertest - .get( - `/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` - ) - .expect(200); + function getGetApiUrl({ objectId, spaceId }: { objectId: string; spaceId?: string }) { + const spacePrefix = spaceId ? `/s/${spaceId}` : ''; + return `${spacePrefix}/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/${objectId}`; + } + + // For brevity, each encrypted saved object has the same decrypted attributes after migrations/conversion. + // An assertion based on this ensures all encrypted fields can still be decrypted after migrations/conversion have been applied. + const expectedDecryptedAttributes = { + encryptedAttribute: 'this is my secret api key', + nonEncryptedAttribute: 'elastic-migrated', // this field was migrated in 7.8.0 + additionalEncryptedAttribute: 'elastic-migrated-encrypted', // this field was added in 7.9.0 + }; + + // In these test cases, we simulate a scenario where some existing objects that are migrated when Kibana starts up. Note that when a + // document migration is triggered, the saved object "convert" transform is also applied by the Core migration algorithm. + describe('handles index migration correctly', () => { + describe('in the default space', () => { + it('for a saved object that needs to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ objectId: '74f3e6d7-b7bb-477d-ac28-92ee22728e6e' }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('for a saved object that does not need to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ objectId: '362828f0-eef2-11eb-9073-11359682300a' }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + }); + + describe('in a custom space', () => { + const spaceId = 'custom-space'; + + it('for a saved object that needs to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ + objectId: 'a98e22f8-530e-5d69-baf7-97526796f3a6', // This ID is not found in the data.json file, it is dynamically generated when the object is converted; the original ID is a67c6950-eed8-11eb-9a62-032b4e4049d1 + spaceId, + }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('for a saved object that does not need to be migrated before it is converted', async () => { + const getApiUrl = getGetApiUrl({ + objectId: '41395c74-da7a-5679-9535-412d550a6cf7', // This ID is not found in the data.json file, it is dynamically generated when the object is converted; the original ID is 36448a90-eef2-11eb-9073-11359682300a + spaceId, + }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + }); + }); + + // In these test cases, we simulate a scenario where new objects are migrated upon creation. This happens because an outdated + // `migrationVersion` field is included below. Note that when a document migration is triggered, the saved object "convert" transform + // is *not* applied by the Core migration algorithm. + describe('handles document migration correctly', () => { + function getCreateApiUrl({ spaceId }: { spaceId?: string } = {}) { + const spacePrefix = spaceId ? `/s/${spaceId}` : ''; + return `${spacePrefix}/api/saved_objects/saved-object-with-migration`; + } + + const objectToCreate = { + attributes: { + encryptedAttribute: 'this is my secret api key', + nonEncryptedAttribute: 'elastic', + }, + migrationVersion: { 'saved-object-with-migration': '7.7.0' }, + }; + + it('in the default space', async () => { + const createApiUrl = getCreateApiUrl(); + const { body: savedObject } = await supertest + .post(createApiUrl) + .set('kbn-xsrf', 'xxx') + .send(objectToCreate) + .expect(200); + const { id: objectId } = savedObject; + + const getApiUrl = getGetApiUrl({ objectId }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); + }); + + it('in a custom space', async () => { + const spaceId = 'custom-space'; + const createApiUrl = getCreateApiUrl({ spaceId }); + const { body: savedObject } = await supertest + .post(createApiUrl) + .set('kbn-xsrf', 'xxx') + .send(objectToCreate) + .expect(200); + const { id: objectId } = savedObject; - expect(decryptedResponse.attributes).to.eql({ - // ensures the encrypted field can still be decrypted after the migration - encryptedAttribute: 'this is my secret api key', - // ensures the non-encrypted field has been migrated in 7.8.0 - nonEncryptedAttribute: 'elastic-migrated', - // ensures the non-encrypted field has been migrated into a new encrypted field in 7.9.0 - additionalEncryptedAttribute: 'elastic-migrated-encrypted', + const getApiUrl = getGetApiUrl({ objectId, spaceId }); + const { body: decryptedResponse } = await supertest.get(getApiUrl).expect(200); + expect(decryptedResponse.attributes).to.eql(expectedDecryptedAttributes); }); }); }); From 677abd67bcfaff58714d163606dee5eb5109058d Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 27 Jul 2021 23:14:29 -0400 Subject: [PATCH 6/7] Add more resolve in the task manager --- .../server/task_runner/task_runner.ts | 52 +++- .../tests/alerting/get_existing.ts | 21 +- .../alerts_in_multiple_spaces/data.json | 229 ------------------ .../alerts_in_multiple_spaces/data.json.gz | Bin 0 -> 1678 bytes .../alerts_in_multiple_spaces/mappings.json | 9 - 5 files changed, 58 insertions(+), 253 deletions(-) delete mode 100644 x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json create mode 100644 x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index b4f3d9105c982..e7901e2b7660d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -247,12 +247,22 @@ export class TaskRunner< actions, } = alert; const { - params: { alertId }, + params: { alertId: potentiallyLegacyAlertId }, state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt }, } = this.taskInstance; const namespace = this.context.spaceIdToNamespace(spaceId); const alertType = this.alertTypeRegistry.get(alertTypeId); + // We need to ensure the `alertId` resolves to an actual saved object due to https://github.com/elastic/kibana/issues/100067 + // It might be an old id that needs aliased + const { + saved_object: { id: alertId }, + } = await this.context.internalSavedObjectsRepository.resolve( + 'alert', + potentiallyLegacyAlertId, + { namespace: this.context.spaceIdToNamespace(spaceId) } + ); + const alertInstances = mapValues< Record, AlertInstance @@ -433,9 +443,19 @@ export class TaskRunner< event: Event ) { const { - params: { alertId, spaceId }, + params: { alertId: potentiallyLegacyAlertId, spaceId }, } = this.taskInstance; + // We need to ensure the `alertId` resolves to an actual saved object due to https://github.com/elastic/kibana/issues/100067 + // It might be an old id that needs aliased + const { + saved_object: { id: alertId }, + } = await this.context.internalSavedObjectsRepository.resolve( + 'alert', + potentiallyLegacyAlertId, + { namespace: this.context.spaceIdToNamespace(spaceId) } + ); + // Validate const validatedParams = validateAlertTypeParams(alert.params, this.alertType.validate?.params); const executionHandler = this.getExecutionHandler( @@ -460,8 +480,19 @@ export class TaskRunner< async loadAlertAttributesAndRun(event: Event): Promise> { const { - params: { alertId, spaceId }, + params: { alertId: potentiallyLegacyAlertId, spaceId }, } = this.taskInstance; + + // We need to ensure the `alertId` resolves to an actual saved object due to https://github.com/elastic/kibana/issues/100067 + // It might be an old id that needs aliased + const { + saved_object: { id: alertId }, + } = await this.context.internalSavedObjectsRepository.resolve( + 'alert', + potentiallyLegacyAlertId, + { namespace: this.context.spaceIdToNamespace(spaceId) } + ); + // TODO: Change when https://github.com/elastic/kibana/issues/106567 is resolved let apiKey: string | null; try { @@ -500,14 +531,27 @@ export class TaskRunner< async run(): Promise { const { - params: { alertId, spaceId }, + params: { alertId: potentiallyLegacyAlertId, spaceId }, startedAt, state: originalState, schedule: taskSchedule, } = this.taskInstance; + // We need to ensure the `alertId` resolves to an actual saved object due to https://github.com/elastic/kibana/issues/100067 + // It might be an old id that needs aliased + const { + saved_object: { id: alertId }, + } = await this.context.internalSavedObjectsRepository.resolve( + 'alert', + potentiallyLegacyAlertId, + { namespace: this.context.spaceIdToNamespace(spaceId) } + ); + + // TODO: handle resolve here + const runDate = new Date(); const runDateString = runDate.toISOString(); + this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDateString}`); const namespace = this.context.spaceIdToNamespace(spaceId); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts index 0f30db4d2f72f..4fa769f4712ab 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_existing.ts @@ -41,23 +41,22 @@ export default function createGetExistingTests({ getService }: FtrProviderContex }); it('should resolve to an alias match for the non default space rule', async () => { - const oldObjectId = '8855c6f7-8a73-52a1-bd36-3c929ce3ecb6'; - const newObjectId = '80827100-ee87-11eb-8dcd-cb59df51587b'; // This ID is not found in the test data archive; it is deterministically generated when the alerting rule is migrated to 8.0 - const spaceId = 'customspace'; - - await registerWithTaskManager(oldObjectId, spaceId); - const response = await resolveAlertingRule(newObjectId, spaceId); + const oldObjectId = '58f37000-ef16-11eb-8970-13ff1ce2b1b6'; + const newObjectId = '8d7d8746-5863-5fd4-990e-027292e83a9a'; // This ID is not found in the test data archive; it is deterministically generated when the alerting rule is migrated to 8.0 + const spaceId = 'chrisspace'; + await registerWithTaskManager(newObjectId, spaceId); + const response = await resolveAlertingRule(oldObjectId, spaceId); expect(response.body.resolveResponse).to.eql({ outcome: 'aliasMatch', - aliasTargetId: oldObjectId, + aliasTargetId: newObjectId, }); }); it('should resolve to an exact match for the default space rule', async () => { - const objectId = '4c23c3a0-ee87-11eb-8dcd-cb59df51587b'; - - await registerWithTaskManager(objectId); - const response = await resolveAlertingRule(objectId); + const oldObjectId = '41a31d60-ef16-11eb-8970-13ff1ce2b1b6'; + const newObjectId = '41a31d60-ef16-11eb-8970-13ff1ce2b1b6'; // This ID is not found in the test data archive; it is deterministically generated when the alerting rule is migrated to 8.0 + await registerWithTaskManager(newObjectId); + const response = await resolveAlertingRule(oldObjectId); expect(response.body.resolveResponse).to.eql({ outcome: 'exactMatch', }); diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json deleted file mode 100644 index 126340481a379..0000000000000 --- a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json +++ /dev/null @@ -1,229 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "space:customspace", - "index": ".kibana_1", - "source": { - "space" : { - "name" : "CustomSpace", - "disabledFeatures" : [ ] - }, - "type" : "space", - "references" : [ ], - "migrationVersion" : { - "space" : "6.6.0" - }, - "coreMigrationVersion" : "7.14.0", - "updated_at" : "2021-07-27T03:04:22.349Z" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "action:3fd484e0-ee87-11eb-8dcd-cb59df51587b", - "index": ".kibana_1", - "source": { - "action" : { - "actionTypeId" : ".server-log", - "name" : "DefaultSpace_Connector", - "isMissingSecrets" : false, - "config" : { }, - "secrets" : "AEN6aQfiii4lJPfABP3lWh0wk2DITczt6hUqeK90mzMIJlEy8FVkRT2vHP4UIdVggLB3HWcKKgENmOk2fSQG2HRbQw6rpluhhSL+lxdlIYv9Sc3VsBUlAC7MqvEeNA==" - }, - "type" : "action", - "references" : [ ], - "migrationVersion" : { - "action" : "7.14.0" - }, - "coreMigrationVersion" : "7.14.0", - "updated_at" : "2021-07-27T03:03:42.396Z" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "customspace:action:7c6fcef0-ee87-11eb-8dcd-cb59df51587b", - "index": ".kibana_1", - "source": { - "action" : { - "actionTypeId" : ".server-log", - "name" : "CustomSpace_Connector", - "isMissingSecrets" : false, - "config" : { }, - "secrets" : "Iwyym3VrXIBW4CxA/RdJUDjXjC4G498PqyjqdaMNFo5GlbnZX0Ynp1rpM95eG9WclGBnXTc4viwDs3QZPquU8Smv7Izvx53tt7npIyJ9qtGaAaQZ681f5gNtJJA/bg==" - }, - "type" : "action", - "references" : [ ], - "namespace" : "customspace", - "migrationVersion" : { - "action" : "7.14.0" - }, - "coreMigrationVersion" : "7.14.0", - "updated_at" : "2021-07-27T03:05:24.072Z" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "alert:4c23c3a0-ee87-11eb-8dcd-cb59df51587b", - "index": ".kibana_1", - "source": { - "alert" : { - "params" : { - "esQuery" : "{\n \"query\":{\n \"match_all\" : {}\n }\n}", - "size" : 100, - "timeWindowSize" : 5, - "timeWindowUnit" : "m", - "threshold" : [ - 0 - ], - "thresholdComparator" : ">", - "index" : [ - ".kibana-event-log-*" - ], - "timeField" : "@timestamp" - }, - "consumer" : "alerts", - "schedule" : { - "interval" : "1m" - }, - "tags" : [ ], - "name" : "DefaultSpace_Rule", - "throttle" : null, - "actions" : [ - { - "group" : "query matched", - "params" : { - "level" : "info", - "message" : "DefaultSpace_Message" - }, - "actionRef" : "action_0", - "actionTypeId" : ".server-log" - } - ], - "enabled" : true, - "alertTypeId" : ".es-query", - "notifyWhen" : "onActiveAlert", - "apiKeyOwner" : "elastic", - "apiKey" : "SQxINbeYowvAYUoULov4NCv3ff5xfbP0m7tmGbjwwzqLmzVswUctuFX3MzJI5knrFjXF4+xQbXwbwcu7ZmO/VW1IRli2klXlx/GfczHraMD5wmN+OQlvJfrubgZ9Pfm0iSyoHYMyM6MsYqVuE/cQvTNB0LPzmjFAC71gQlBNV52GiMGxSq82BJMn/ogPoX4Vy1UZBoTTh0AK5Q==", - "createdBy" : "elastic", - "updatedBy" : "elastic", - "createdAt" : "2021-07-27T03:04:03.758Z", - "updatedAt" : "2021-07-27T03:04:03.758Z", - "muteAll" : false, - "mutedInstanceIds" : [ ], - "executionStatus" : { - "status" : "active", - "lastExecutionDate" : "2021-07-27T03:07:16.475Z", - "error" : null - }, - "meta" : { - "versionApiKeyLastmodified" : "7.14.0" - }, - "scheduledTaskId" : "4d350c90-ee87-11eb-8dcd-cb59df51587b" - }, - "type" : "alert", - "references" : [ - { - "id" : "3fd484e0-ee87-11eb-8dcd-cb59df51587b", - "name" : "action_0", - "type" : "action" - } - ], - "migrationVersion" : { - "alert" : "7.13.0" - }, - "coreMigrationVersion" : "7.14.0", - "updated_at" : "2021-07-27T03:07:17.288Z" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "customspace:alert:80827100-ee87-11eb-8dcd-cb59df51587b", - "index": ".kibana_1", - "source": { - "alert" : { - "params" : { - "esQuery" : "{\n \"query\":{\n \"match_all\" : {}\n }\n}", - "size" : 100, - "timeWindowSize" : 5, - "timeWindowUnit" : "m", - "threshold" : [ - 0 - ], - "thresholdComparator" : ">", - "index" : [ - ".kibana-event-log-*" - ], - "timeField" : "@timestamp" - }, - "consumer" : "alerts", - "schedule" : { - "interval" : "1m" - }, - "tags" : [ ], - "name" : "CustomSpace_Rule", - "throttle" : null, - "actions" : [ - { - "group" : "query matched", - "params" : { - "level" : "info", - "message" : "CustomSpace_Message" - }, - "actionRef" : "action_0", - "actionTypeId" : ".server-log" - } - ], - "enabled" : true, - "alertTypeId" : ".es-query", - "notifyWhen" : "onActiveAlert", - "apiKeyOwner" : "elastic", - "apiKey" : "cMYA0GuObN+7rwAAJn+/gKpj5GeVTxeXq9CcrueMvyIS2vplmvDyMaZflnnsp8t8L5BnVsAQak6TYl3T7PI5AVbmYROa+2zqam7CffJ3x9gDZ+Kd8+wY++wcIuZymVppom94iRhxhYQvy+6yHeNullBdcCl8kvOJPEWEbwAPiiofLDwW/rY1AdbZxyJ5DNr5Ay7GnnAZQBW+eg==", - "createdBy" : "elastic", - "updatedBy" : "elastic", - "createdAt" : "2021-07-27T03:05:30.977Z", - "updatedAt" : "2021-07-27T03:05:30.977Z", - "muteAll" : false, - "mutedInstanceIds" : [ ], - "executionStatus" : { - "status" : "active", - "lastExecutionDate" : "2021-07-27T03:07:40.462Z", - "error" : null - }, - "meta" : { - "versionApiKeyLastmodified" : "7.14.0" - }, - "scheduledTaskId" : "80c8c8d0-ee87-11eb-8dcd-cb59df51587b" - }, - "type" : "alert", - "references" : [ - { - "id" : "7c6fcef0-ee87-11eb-8dcd-cb59df51587b", - "name" : "action_0", - "type" : "action" - } - ], - "namespace" : "customspace", - "migrationVersion" : { - "alert" : "7.13.0" - }, - "coreMigrationVersion" : "7.14.0", - "updated_at" : "2021-07-27T03:07:41.410Z" - }, - "type": "_doc" - } -} diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c913817516d1866cbef4513586e468843a59860f GIT binary patch literal 1678 zcmV;9266cxiwFP!000026YW@ObE4Q9{+?gq^?th$RqDIAtW_l`hfCG}S zC>#*&%74EH)HE)cPUa@Fbaj;#<-E&z_xB-Rt_p=B+ALwQRwx?2Ui>LNh=CKOCtvUb zezc908ZLnjYX&qyltDI)Ek&pr^wSGyS?h{q3225YK764`2v` z7^YsLAc8-ACF#E72kB-1CJ4>c=wgm$z)%nDCE9y2S=eC#e>T|f!ahoc?6!X$N!cOL z9B8y5Kv8gHWX?>?=m9r#v$+rWnWKlS?L2xuj+O>M(6|REB~k=MmI%H?@e_)u5p0dY z(XJAAJ9T?y&#Laz3cyECS5H@0U*1|8`XaEyyfxoNb-X!YjWi5Ors!RA#?=_EOtaiO z=j>~?YCuopSz|dur=UQTphqzt|N4CR0_QCcn1?mArX>Ls5gz$H>((pYG>InCBjxBsvP^><< z(^@|w)r+ahjC(DS7&-Wn|Tbmtyg*jscA9Y86jHkR&AhZm0oQUpP$IWK-229 zY%R8l(A}=a&=H~vvvn!)laqWR=dE5}=7*260-BFq4xq8Rx9wM zWy-UPV6Z8;0O<>i!%n!CAOLQ7WbU5gz!*BIyzQC(iC9`XFbsk9!dBkb$E@aFtRu4)JdE-gWsca;9kRQHD5ikQw79-u~Nat3VN002>0!omp5wZodspvx3P-CYolixrkGyQ zI>q55w)(}~2RNf1ZJXn|4v!gq`_^~8&+o@E-on|h9RZ;8Jg8;v(1Y}+u?T>NkPgcIT} z&zV&0p1)Rh_OUOJZ#_F}9QSfDgz4l=b9a&g+rxtaZ<|z<{D!PC0pa6r%&~$_a*%t? z=GoZ^te2|{hs#GOv+cO$4qNv1BVjLaJQ6+%TkN-FDn`E_U@n{_XczC-zkHpv?!`5H z3Tv)TciaU4bkx$nkAHA#@ar{Azc*;lKo3I#nl!i{py&SvJ^2@ekA*!AZhrfT)I?2Q z1_4?vyR5a1d#=QeKCV* z6nDu-k!6Ph-GtJexZ28QV;Id8Fe4ab&10#WNEHq5c7~ypZLh93=Il)SWUV=Cx*^$q z7xds=7lMf(cNBGCHWIj|^vDHj-J(PegrM@fJlW&k8z~=|>!K@60-|GqLuLM39hr-_q_`Dw1yx|{k Ye+yhg6J=81zm04D2Y*D7wmBOB0D-(jD*ylh literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/mappings.json b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/mappings.json index 7408de6ae3413..03f744f63bc58 100644 --- a/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/mappings.json +++ b/x-pack/test/functional/es_archives/alerts_in_multiple_spaces/mappings.json @@ -1817,15 +1817,6 @@ }, "type": "text" }, - "search-telemetry": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, "space": { "fields": { "keyword": { From 34dcc33455d4e6b502e0ac822ef7252184cdad13 Mon Sep 17 00:00:00 2001 From: chrisronline Date: Fri, 30 Jul 2021 00:20:02 -0400 Subject: [PATCH 7/7] WIP for adjusting the find_by_ids event log route --- x-pack/plugins/actions/server/plugin.ts | 23 ++++--- x-pack/plugins/alerting/server/plugin.ts | 18 +++-- .../event_log/server/event_log_client.ts | 27 ++++++-- x-pack/plugins/event_log/server/plugin.ts | 5 +- .../server/saved_object_provider_registry.ts | 69 ++++++++++++++----- 5 files changed, 106 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 2f4b1325e3df3..b752651e59607 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -18,6 +18,7 @@ import { ElasticsearchServiceStart, SavedObjectsClientContract, SavedObjectsBulkGetObject, + SavedObjectsBaseOptions, } from '../../../../src/core/server'; import { @@ -356,20 +357,24 @@ export class ActionsPlugin implements Plugin getActionsClientWithRequest(request); + const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) => + core.savedObjects.getScopedClient(request); this.eventLogService!.registerSavedObjectProvider('action', (request) => { const client = secureGetActionsClientWithRequest(request); - return (objects?: SavedObjectsBulkGetObject[]) => - objects - ? Promise.all( - objects.map(async (objectItem) => await (await client).get({ id: objectItem.id })) - ) - : Promise.resolve([]); + const soClient = getScopedSavedObjectsClientWithoutAccessToActions(request); + return { + bulkGetter: (objects?: SavedObjectsBulkGetObject[]) => + objects + ? Promise.all( + objects.map(async (objectItem) => await (await client).get({ id: objectItem.id })) + ) + : Promise.resolve([]), + resolver: (type: string, id: string, options?: SavedObjectsBaseOptions) => + soClient.resolve(type, id, options), + }; }); - const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) => - core.savedObjects.getScopedClient(request); - actionExecutor!.initialize({ logger, eventLogger: this.eventLogger!, diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 1407f622fabec..6a59b3f3e5941 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -35,6 +35,7 @@ import { ServiceStatus, SavedObjectsBulkGetObject, ServiceStatusLevels, + SavedObjectsBaseOptions, } from '../../../../src/core/server'; import type { AlertingRequestHandlerContext } from './types'; import { defineRoutes } from './routes'; @@ -368,6 +369,7 @@ export class AlertingPlugin { return alertingAuthorizationClientFactory!.create(request); }; + const internalSavedObjectsRepository = core.savedObjects.createInternalRepository(['alert']); taskRunnerFactory.initialize({ logger, getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), @@ -377,7 +379,7 @@ export class AlertingPlugin { encryptedSavedObjectsClient, basePathService: core.http.basePath, eventLogger: this.eventLogger!, - internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), + internalSavedObjectsRepository, alertTypeRegistry: this.alertTypeRegistry!, kibanaBaseUrl: this.kibanaBaseUrl, supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), @@ -386,10 +388,16 @@ export class AlertingPlugin { this.eventLogService!.registerSavedObjectProvider('alert', (request) => { const client = getRulesClientWithRequest(request); - return (objects?: SavedObjectsBulkGetObject[]) => - objects - ? Promise.all(objects.map(async (objectItem) => await client.get({ id: objectItem.id }))) - : Promise.resolve([]); + return { + bulkGetter: (objects?: SavedObjectsBulkGetObject[]) => + objects + ? Promise.all( + objects.map(async (objectItem) => await client.get({ id: objectItem.id })) + ) + : Promise.resolve([]), + resolver: (type: string, id: string, options?: SavedObjectsBaseOptions) => + internalSavedObjectsRepository.resolve(type, id, options), + }; }); scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager); diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 5214db1169deb..a3d80698f9918 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -13,7 +13,10 @@ import { SpacesServiceStart } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClient } from './types'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; -import { SavedObjectBulkGetterResult } from './saved_object_provider_registry'; +import { + SavedObjectBulkGetterResult, + SavedObjectIdResolverResult, +} from './saved_object_provider_registry'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; @@ -61,7 +64,10 @@ export type FindOptionsType = Pick< interface EventLogServiceCtorParams { esContext: EsContext; - savedObjectGetter: SavedObjectBulkGetterResult; + savedObjectGetter: { + bulkGetter: SavedObjectBulkGetterResult; + resolver: SavedObjectIdResolverResult; + }; spacesService?: SpacesServiceStart; request: KibanaRequest; } @@ -69,7 +75,10 @@ interface EventLogServiceCtorParams { // note that clusterClient may be null, indicating we can't write to ES export class EventLogClient implements IEventLogClient { private esContext: EsContext; - private savedObjectGetter: SavedObjectBulkGetterResult; + private savedObjectGetter: { + bulkGetter: SavedObjectBulkGetterResult; + resolver: SavedObjectIdResolverResult; + }; private spacesService?: SpacesServiceStart; private request: KibanaRequest; @@ -90,8 +99,18 @@ export class EventLogClient implements IEventLogClient { const space = await this.spacesService?.getActiveSpace(this.request); const namespace = space && this.spacesService?.spaceIdToNamespace(space.id); + // Pass ids through the SO resolve API to ensure they point to the right SO + let resolvedIds = ids; + try { + resolvedIds = (await Promise.all( + ids.map((id) => this.savedObjectGetter.resolver(type, id, { namespace })) + )) as string[]; + } catch (err) { + throw err; + } + // verify the user has the required permissions to view this saved objects - await this.savedObjectGetter(type, ids); + await this.savedObjectGetter.bulkGetter(type, resolvedIds); return await this.esContext.esAdapter.queryEventsBySavedObjects( this.esContext.esNames.indexPattern, diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index 9cc874735cc0e..f835542b78523 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -138,7 +138,10 @@ export class Plugin implements CorePlugin { const client = core.savedObjects.getScopedClient(request); - return client.bulkGet.bind(client); + return { + bulkGetter: client.bulkGet.bind(client), + resolver: client.resolve.bind(client), + }; }); this.eventLogClientService = new EventLogClientService({ diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts index 916757859e6df..7187948f55eda 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts @@ -6,7 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { + KibanaRequest, + SavedObjectsBaseOptions, + SavedObjectsClientContract, +} from 'src/core/server'; import { fromNullable, getOrElse } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -19,9 +23,23 @@ export type SavedObjectBulkGetter = ( ...params: Parameters ) => Promise; +export type SavedObjectResolver = ( + ...params: Parameters +) => Promise; + export type SavedObjectBulkGetterResult = (type: string, ids: string[]) => Promise; +export type SavedObjectIdResolverResult = ( + type: string, + id: string, + options?: SavedObjectsBaseOptions | undefined +) => Promise | undefined; -export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectBulkGetter; +interface SavedObjectCombinedApi { + bulkGetter: SavedObjectBulkGetter; + resolver: SavedObjectResolver; +} + +export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectCombinedApi; export class SavedObjectProviderRegistry { private providers = new Map(); @@ -42,7 +60,9 @@ export class SavedObjectProviderRegistry { this.providers.set(type, provider); } - public getProvidersClient(request: KibanaRequest): SavedObjectBulkGetterResult { + public getProvidersClient( + request: KibanaRequest + ): { bulkGetter: SavedObjectBulkGetterResult; resolver: SavedObjectIdResolverResult } { if (!this.defaultProvider) { throw new Error( i18n.translate( @@ -60,21 +80,36 @@ export class SavedObjectProviderRegistry { // would be nice to have a simple version support in API: // curl -X GET "localhost:9200/my-index-000001/_mget?pretty" -H 'Content-Type: application/json' -d' { "ids" : ["1", "2"] } ' - const scopedProviders = new Map(); + const scopedProviders = new Map(); const defaultGetter = this.defaultProvider(request); - return (type: string, ids: string[]) => { - const objects = ids.map((id: string) => ({ type, id })); - const getter = pipe( - fromNullable(scopedProviders.get(type)), - getOrElse(() => { - const client = this.providers.has(type) - ? this.providers.get(type)!(request) - : defaultGetter; - scopedProviders.set(type, client); - return client; - }) - ); - return getter(objects); + return { + bulkGetter: (type: string, ids: string[]) => { + const objects = ids.map((id: string) => ({ type, id })); + const getter = pipe( + fromNullable(scopedProviders.get(type)), + getOrElse(() => { + const client = this.providers.has(type) + ? this.providers.get(type)!(request) + : defaultGetter; + scopedProviders.set(type, client); + return client; + }) + ); + return getter.bulkGetter(objects); + }, + resolver: (type: string, id: string, options?: SavedObjectsBaseOptions) => { + const getter = pipe( + fromNullable(scopedProviders.get(type)), + getOrElse(() => { + const client = this.providers.has(type) + ? this.providers.get(type)!(request) + : defaultGetter; + scopedProviders.set(type, client); + return client; + }) + ); + return getter.resolver(type, id, options); + }, }; } }