From 1170c6e70a0e92ba688ce34c71d8b4aa0988574f Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 30 Apr 2021 09:21:32 -0400 Subject: [PATCH 001/186] WIP - creating alerting authorization client factory and exposing authorization client on plugin start contract --- .../alerting_authorization_client_factory.ts | 59 +++++++++++++++++++ .../alerting/server/alerts_client_factory.ts | 29 ++------- .../authorization/alerts_authorization.ts | 59 ++++++++++++++----- .../alerts_authorization_kuery.ts | 1 + x-pack/plugins/alerting/server/plugin.ts | 27 +++++++-- .../server/authorization/actions/index.ts | 1 + .../security/server/authorization/index.ts | 2 +- x-pack/plugins/security/server/index.ts | 2 +- 8 files changed, 136 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts new file mode 100644 index 0000000000000..8b8707ac96107 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -0,0 +1,59 @@ +/* + * 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 { KibanaRequest } from 'src/core/server'; +import { ALERTS_FEATURE_ID } from '../common'; +import { AlertTypeRegistry } from './types'; +import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; +import { Space } from '../../spaces/server'; + +export interface AlertingAuthorizationClientFactoryOpts { + alertTypeRegistry: AlertTypeRegistry; + securityPluginSetup?: SecurityPluginSetup; + securityPluginStart?: SecurityPluginStart; + getSpace: (request: KibanaRequest) => Promise; + features: FeaturesPluginStart; +} + +export class AlertingAuthorizationClientFactory { + private isInitialized = false; + private alertTypeRegistry!: AlertTypeRegistry; + private securityPluginStart?: SecurityPluginStart; + private securityPluginSetup?: SecurityPluginSetup; + private features!: FeaturesPluginStart; + private getSpace!: (request: KibanaRequest) => Promise; + + public initialize(options: AlertingAuthorizationClientFactoryOpts) { + if (this.isInitialized) { + throw new Error('AlertingAuthorizationClientFactory already initialized'); + } + this.isInitialized = true; + this.getSpace = options.getSpace; + this.alertTypeRegistry = options.alertTypeRegistry; + this.securityPluginSetup = options.securityPluginSetup; + this.securityPluginStart = options.securityPluginStart; + this.features = options.features; + } + + public create(request: KibanaRequest, privilegeName?: string): AlertsAuthorization { + const { securityPluginSetup, securityPluginStart, features } = this; + return new AlertsAuthorization({ + authorization: securityPluginStart?.authz, + request, + getSpace: this.getSpace, + alertTypeRegistry: this.alertTypeRegistry, + features: features!, + auditLogger: new AlertsAuthorizationAuditLogger( + securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) + ), + privilegeName, + }); + } +} diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 05e50346f56cf..455781e067ce0 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -13,16 +13,12 @@ import { } from 'src/core/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; -import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; -import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; -import { Space } from '../../spaces/server'; import { IEventLogClientService } from '../../../plugins/event_log/server'; +import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -31,13 +27,12 @@ export interface AlertsClientFactoryOpts { securityPluginSetup?: SecurityPluginSetup; securityPluginStart?: SecurityPluginStart; getSpaceId: (request: KibanaRequest) => string | undefined; - getSpace: (request: KibanaRequest) => Promise; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actions: ActionsPluginStartContract; - features: FeaturesPluginStart; eventLog: IEventLogClientService; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; + authorization: AlertingAuthorizationClientFactory; } export class AlertsClientFactory { @@ -48,13 +43,12 @@ export class AlertsClientFactory { private securityPluginSetup?: SecurityPluginSetup; private securityPluginStart?: SecurityPluginStart; private getSpaceId!: (request: KibanaRequest) => string | undefined; - private getSpace!: (request: KibanaRequest) => Promise; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; private actions!: ActionsPluginStartContract; - private features!: FeaturesPluginStart; private eventLog!: IEventLogClientService; private kibanaVersion!: PluginInitializerContext['env']['packageInfo']['version']; + private authorization!: AlertingAuthorizationClientFactory; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -63,7 +57,6 @@ export class AlertsClientFactory { this.isInitialized = true; this.logger = options.logger; this.getSpaceId = options.getSpaceId; - this.getSpace = options.getSpace; this.taskManager = options.taskManager; this.alertTypeRegistry = options.alertTypeRegistry; this.securityPluginSetup = options.securityPluginSetup; @@ -71,24 +64,14 @@ export class AlertsClientFactory { this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; this.actions = options.actions; - this.features = options.features; this.eventLog = options.eventLog; this.kibanaVersion = options.kibanaVersion; + this.authorization = options.authorization; } public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { - const { securityPluginSetup, securityPluginStart, actions, eventLog, features } = this; + const { securityPluginSetup, securityPluginStart, actions, eventLog } = this; const spaceId = this.getSpaceId(request); - const authorization = new AlertsAuthorization({ - authorization: securityPluginStart?.authz, - request, - getSpace: this.getSpace, - alertTypeRegistry: this.alertTypeRegistry, - features: features!, - auditLogger: new AlertsAuthorizationAuditLogger( - securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) - ), - }); return new AlertsClient({ spaceId, @@ -100,7 +83,7 @@ export class AlertsClientFactory { excludedWrappers: ['security'], includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), - authorization, + authorization: this.authorization!.create(request), actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index b2ead9bf1b4e6..ea39b635f5045 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -6,11 +6,11 @@ */ import Boom from '@hapi/boom'; -import { map, mapValues, fromPairs, has } from 'lodash'; +import { map, mapValues, fromPairs, has, get } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; -import { SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup, AlertingActions } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; @@ -49,6 +49,7 @@ export interface RegistryAlertTypeWithAuth extends RegistryAlertType { type IsAuthorizedAtProducerLevel = boolean; +const DEFAULT_PRIVILEGE_NAME = 'alerting'; export interface ConstructorOptions { alertTypeRegistry: AlertTypeRegistry; request: KibanaRequest; @@ -56,6 +57,7 @@ export interface ConstructorOptions { getSpace: (request: KibanaRequest) => Promise; auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; + privilegeName?: string; } export class AlertsAuthorization { @@ -65,6 +67,12 @@ export class AlertsAuthorization { private readonly auditLogger: AlertsAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; + private readonly privilegeName: string; + private readonly getAuthorizationString: ( + alertTypeId: string, + consumer: string, + operation: string + ) => string; constructor({ alertTypeRegistry, @@ -73,11 +81,19 @@ export class AlertsAuthorization { features, auditLogger, getSpace, + privilegeName, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; + this.privilegeName = privilegeName ?? DEFAULT_PRIVILEGE_NAME; + + const authorizationAction = get( + this.authorization!.actions, + this.privilegeName + ) as AlertingActions; + this.getAuthorizationString = authorizationAction.get; this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) @@ -86,13 +102,14 @@ export class AlertsAuthorization { new Set( features .getKibanaFeatures() - .filter( - ({ id, alerting }) => - // ignore features which are disabled in the user's space - !disabledFeatures.has(id) && + .filter((feature) => { + // ignore features which are disabled in the user's space + return ( + !disabledFeatures.has(feature.id) && // ignore features which don't grant privileges to alerting - (alerting?.length ?? 0 > 0) - ) + (get(feature, this.privilegeName, undefined)?.length ?? 0 > 0) + ); + }) .map((feature) => feature.id) ) ) @@ -102,6 +119,9 @@ export class AlertsAuthorization { return new Set(); }); + // TODO + // This adds { alerts: { read: true, all: true }} to the list of consumers + // Do we need a flag to skip this??? this.allPossibleConsumers = this.featuresIds.then((featuresIds) => featuresIds.size ? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], { @@ -116,6 +136,10 @@ export class AlertsAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } + // alertTypeId determines the producer + // consumer determines the consumer/owner + // operation enum needs to be passed in in the constructor + // also pass in a type rule/alert to pass into the .get function??? public async ensureAuthorized( alertTypeId: string, consumer: string, @@ -127,10 +151,13 @@ export class AlertsAuthorization { if (authorization && this.shouldCheckAuthorization()) { const alertType = this.alertTypeRegistry.get(alertTypeId); const requiredPrivilegesByScope = { + // authorization.actions.alerting.get needs to be able to change consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), }; + // This needs to be feature flagged + // We special case the Alerts Management `consumer` as we don't want to have to // manually authorize each alert type in the management UI const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; @@ -160,6 +187,7 @@ export class AlertsAuthorization { * as Privileged. * This check will ensure we don't accidentally let these through */ + // This should also log the type they're trying to access rule/alert throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( username, @@ -223,12 +251,13 @@ export class AlertsAuthorization { logSuccessfulAuthorization: () => void; }> { if (this.authorization && this.shouldCheckAuthorization()) { - const { - username, - authorizedAlertTypes, - } = await this.augmentAlertTypesWithAuthorization(this.alertTypeRegistry.list(), [ - ReadOperations.Find, - ]); + const { username, authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( + this.alertTypeRegistry.list(), + [ + // maybe pass in this operation? or require it to be 'find' + ReadOperations.Find, + ] + ); if (!authorizedAlertTypes.size) { throw Boom.forbidden( @@ -333,6 +362,7 @@ export class AlertsAuthorization { for (const feature of featuresIds) { for (const operation of operations) { privilegeToAlertType.set( + // this function needs to be swappable this.authorization!.actions.alerting.get(alertType.id, feature, operation), [ alertType, @@ -369,6 +399,7 @@ export class AlertsAuthorization { alertType.authorizedConsumers[feature] ); + // this needs to be feature flagged if (isAuthorizedAtProducerLevel) { // granting privileges under the producer automatically authorized the Alerts Management UI as well alertType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts index b7a9efd1d964f..3c5ff4345095b 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts @@ -10,6 +10,7 @@ import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { KueryNode } from '../../../../../src/plugins/data/server'; import { RegistryAlertTypeWithAuth } from './alerts_authorization'; +// pass in the field name instead of hardcoding `alert.attributes.alertTypeId` and `alertTypeId` export function asFiltersByAlertTypeAndConsumer( alertTypes: Set ): KueryNode { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 1155cfa93337d..e8a2b9dbece9b 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -36,7 +36,6 @@ import { SavedObjectsBulkGetObject, } from '../../../../src/core/server'; import type { AlertingRequestHandlerContext } from './types'; - import { defineRoutes } from './routes'; import { LICENSE_TYPE, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { @@ -68,6 +67,8 @@ import { } from './health'; import { AlertsConfig } from './config'; import { getHealth } from './health/get_health'; +import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; +import { AlertsAuthorization } from './authorization'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -104,6 +105,7 @@ export interface PluginSetupContract { export interface PluginStartContract { listTypes: AlertTypeRegistry['list']; getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf; + getAlertingAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; getFrameworkHealth: () => Promise; } @@ -137,6 +139,7 @@ export class AlertingPlugin { private isESOCanEncrypt?: boolean; private security?: SecurityPluginSetup; private readonly alertsClientFactory: AlertsClientFactory; + private readonly alertingAuthorizationClientFactory: AlertingAuthorizationClientFactory; private readonly telemetryLogger: Logger; private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private eventLogService?: IEventLogService; @@ -149,6 +152,7 @@ export class AlertingPlugin { this.logger = initializerContext.logger.get('plugins', 'alerting'); this.taskRunnerFactory = new TaskRunnerFactory(); this.alertsClientFactory = new AlertsClientFactory(); + this.alertingAuthorizationClientFactory = new AlertingAuthorizationClientFactory(); this.telemetryLogger = initializerContext.logger.get('usage'); this.kibanaIndexConfig = initializerContext.config.legacy.globalConfig$; this.kibanaVersion = initializerContext.env.packageInfo.version; @@ -283,6 +287,7 @@ export class AlertingPlugin { taskRunnerFactory, alertTypeRegistry, alertsClientFactory, + alertingAuthorizationClientFactory, security, licenseState, } = this; @@ -299,6 +304,16 @@ export class AlertingPlugin { : undefined; }; + alertingAuthorizationClientFactory.initialize({ + alertTypeRegistry: alertTypeRegistry!, + securityPluginSetup: security, + securityPluginStart: plugins.security, + async getSpace(request: KibanaRequest) { + return plugins.spaces?.spacesService.getActiveSpace(request); + }, + features: plugins.features, + }); + alertsClientFactory.initialize({ alertTypeRegistry: alertTypeRegistry!, logger, @@ -310,13 +325,10 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return plugins.spaces?.spacesService.getSpaceId(request); }, - async getSpace(request: KibanaRequest) { - return plugins.spaces?.spacesService.getActiveSpace(request); - }, actions: plugins.actions, - features: plugins.features, eventLog: plugins.eventLog, kibanaVersion: this.kibanaVersion, + authorization: alertingAuthorizationClientFactory, }); const getAlertsClientWithRequest = (request: KibanaRequest) => { @@ -328,6 +340,10 @@ export class AlertingPlugin { return alertsClientFactory!.create(request, core.savedObjects); }; + const getAlertingAuthorizationWithRequest = (request: KibanaRequest) => { + return alertingAuthorizationClientFactory!.create(request); + }; + taskRunnerFactory.initialize({ logger, getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), @@ -357,6 +373,7 @@ export class AlertingPlugin { return { listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!), + getAlertingAuthorizationWithRequest, getAlertsClientWithRequest, getFrameworkHealth: async () => await getHealth(core.savedObjects.createInternalRepository(['alert'])), diff --git a/x-pack/plugins/security/server/authorization/actions/index.ts b/x-pack/plugins/security/server/authorization/actions/index.ts index f6c649bf8cda9..b8eab8fb0b0ff 100644 --- a/x-pack/plugins/security/server/authorization/actions/index.ts +++ b/x-pack/plugins/security/server/authorization/actions/index.ts @@ -6,3 +6,4 @@ */ export { Actions } from './actions'; +export type { AlertingActions } from './alerting'; diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 6cbb4d10c75e4..65cfc36376f96 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { Actions } from './actions'; +export { Actions, AlertingActions } from './actions'; export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { featurePrivilegeIterator } from './privileges'; diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 087cf8f4f8ee8..c7710f90511f4 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -26,7 +26,7 @@ export type { InvalidateAPIKeyResult, GrantAPIKeyResult, } from './authentication'; -export type { CheckPrivilegesPayload } from './authorization'; +export type { CheckPrivilegesPayload, AlertingActions } from './authorization'; export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; From 5a1c2c8fae893d101baed4e3231874ef74b39d1e Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 30 Apr 2021 15:06:30 -0400 Subject: [PATCH 002/186] Updating alerting feature privilege builder to handle different alerting types --- .../server/authorization/actions/alerting.ts | 17 +++- .../server/authorization/actions/index.ts | 1 - .../security/server/authorization/index.ts | 2 +- .../alerting.test.ts | 82 +++++++++++-------- .../feature_privilege_builder/alerting.ts | 53 ++++++++---- x-pack/plugins/security/server/index.ts | 2 +- 6 files changed, 97 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts index 0dd30e7f26b32..47d575994d31c 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.ts @@ -14,9 +14,14 @@ export class AlertingActions { this.prefix = `alerting:${versionNumber}:`; } - public get(alertTypeId: string, consumer: string, operation: string): string { - if (!alertTypeId || !isString(alertTypeId)) { - throw new Error('alertTypeId is required and must be a string'); + public get( + ruleTypeId: string, + consumer: string, + alertingType: string, + operation: string + ): string { + if (!ruleTypeId || !isString(ruleTypeId)) { + throw new Error('ruleTypeId is required and must be a string'); } if (!operation || !isString(operation)) { @@ -27,6 +32,10 @@ export class AlertingActions { throw new Error('consumer is required and must be a string'); } - return `${this.prefix}${alertTypeId}/${consumer}/${operation}`; + if (!alertingType || !isString(alertingType)) { + throw new Error('alertingType is required and must be a string'); + } + + return `${this.prefix}${ruleTypeId}/${consumer}/${alertingType}/${operation}`; } } diff --git a/x-pack/plugins/security/server/authorization/actions/index.ts b/x-pack/plugins/security/server/authorization/actions/index.ts index b8eab8fb0b0ff..f6c649bf8cda9 100644 --- a/x-pack/plugins/security/server/authorization/actions/index.ts +++ b/x-pack/plugins/security/server/authorization/actions/index.ts @@ -6,4 +6,3 @@ */ export { Actions } from './actions'; -export type { AlertingActions } from './alerting'; diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 65cfc36376f96..6cbb4d10c75e4 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { Actions, AlertingActions } from './actions'; +export { Actions } from './actions'; export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { featurePrivilegeIterator } from './privileges'; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 34bfb113ab0ea..1226489c6026c 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -76,10 +76,12 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary", - "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertInstanceSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", ] `); }); @@ -114,20 +116,23 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary", - "alerting:1.0.0-zeta1:alert-type/my-feature/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/create", - "alerting:1.0.0-zeta1:alert-type/my-feature/delete", - "alerting:1.0.0-zeta1:alert-type/my-feature/update", - "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/my-feature/enable", - "alerting:1.0.0-zeta1:alert-type/my-feature/disable", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertInstanceSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", ] `); }); @@ -162,24 +167,29 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary", - "alerting:1.0.0-zeta1:alert-type/my-feature/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/create", - "alerting:1.0.0-zeta1:alert-type/my-feature/delete", - "alerting:1.0.0-zeta1:alert-type/my-feature/update", - "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/my-feature/enable", - "alerting:1.0.0-zeta1:alert-type/my-feature/disable", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertInstanceSummary", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertInstanceSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertInstanceSummary", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index c813f0f935cf5..c238d061c1b52 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -10,20 +10,35 @@ import { uniq } from 'lodash'; import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; -const readOperations: string[] = ['get', 'getAlertState', 'getAlertInstanceSummary', 'find']; -const writeOperations: string[] = [ - 'create', - 'delete', - 'update', - 'updateApiKey', - 'enable', - 'disable', - 'muteAll', - 'unmuteAll', - 'muteInstance', - 'unmuteInstance', -]; -const allOperations: string[] = [...readOperations, ...writeOperations]; +enum AlertingType { + RULE = 'rule', + ALERT = 'alert', +} + +const readOperations: Record = { + rule: ['get', 'getAlertState', 'getAlertInstanceSummary', 'find'], + alert: ['get', 'find'], +}; + +const writeOperations: Record = { + rule: [ + 'create', + 'delete', + 'update', + 'updateApiKey', + 'enable', + 'disable', + 'muteAll', + 'unmuteAll', + 'muteInstance', + 'unmuteInstance', + ], + alert: ['update'], +}; +const allOperations: Record = { + rule: [...readOperations.rule, ...writeOperations.rule], + alert: [...readOperations.alert, ...writeOperations.alert], +}; export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder { public getActions( @@ -31,12 +46,16 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder feature: KibanaFeature ): string[] { const getAlertingPrivilege = ( - operations: string[], + operations: Record, privilegedTypes: readonly string[], consumer: string ) => - privilegedTypes.flatMap((type) => - operations.map((operation) => this.actions.alerting.get(type, consumer, operation)) + privilegedTypes.flatMap((privilegedType) => + Object.values(AlertingType).flatMap((alertingType) => + operations[alertingType].map((operation) => + this.actions.alerting.get(privilegedType, consumer, alertingType, operation) + ) + ) ); return uniq([ diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index c7710f90511f4..087cf8f4f8ee8 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -26,7 +26,7 @@ export type { InvalidateAPIKeyResult, GrantAPIKeyResult, } from './authentication'; -export type { CheckPrivilegesPayload, AlertingActions } from './authorization'; +export type { CheckPrivilegesPayload } from './authorization'; export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; From cbde0ccceab6c143e6fcdd3f633d43679be0b49b Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 30 Apr 2021 15:21:45 -0400 Subject: [PATCH 003/186] Passing in alerting authorization type to AlertingActions class string builder --- .../server/alerts_client/alerts_client.ts | 137 +++++++++--------- .../authorization/alerts_authorization.ts | 70 +++++---- 2 files changed, 113 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 1db990edef2a9..504d6b806049c 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -46,7 +46,12 @@ import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/se import { TaskManagerStartContract } from '../../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; import { RegistryAlertType, UntypedNormalizedAlertType } from '../alert_type_registry'; -import { AlertsAuthorization, WriteOperations, ReadOperations } from '../authorization'; +import { + AlertsAuthorization, + WriteOperations, + ReadOperations, + AlertingAuthorizationTypes, +} from '../authorization'; import { IEventLogClient } from '../../../../plugins/event_log/server'; import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; @@ -233,11 +238,11 @@ export class AlertsClient { const id = options?.id || SavedObjectsUtils.generateId(); try { - await this.authorization.ensureAuthorized( - data.alertTypeId, - data.consumer, - WriteOperations.Create - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: data.alertTypeId, + consumer: data.consumer, + operation: WriteOperations.Create, + }); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -358,11 +363,11 @@ export class AlertsClient { }): Promise> { const result = await this.unsecuredSavedObjectsClient.get('alert', id); try { - await this.authorization.ensureAuthorized( - result.attributes.alertTypeId, - result.attributes.consumer, - ReadOperations.Get - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: result.attributes.alertTypeId, + consumer: result.attributes.consumer, + operation: ReadOperations.Get, + }); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -384,11 +389,11 @@ export class AlertsClient { public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); - await this.authorization.ensureAuthorized( - alert.alertTypeId, - alert.consumer, - ReadOperations.GetAlertState - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: alert.alertTypeId, + consumer: alert.consumer, + operation: ReadOperations.GetAlertState, + }); if (alert.scheduledTaskId) { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(alert.scheduledTaskId), @@ -404,11 +409,11 @@ export class AlertsClient { }: GetAlertInstanceSummaryParams): Promise { this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`); const alert = await this.get({ id }); - await this.authorization.ensureAuthorized( - alert.alertTypeId, - alert.consumer, - ReadOperations.GetAlertInstanceSummary - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: alert.alertTypeId, + consumer: alert.consumer, + operation: ReadOperations.GetAlertInstanceSummary, + }); // default duration of instance summary is 60 * alert interval const dateNow = new Date(); @@ -584,11 +589,11 @@ export class AlertsClient { } try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.Delete - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Delete, + }); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -657,11 +662,11 @@ export class AlertsClient { } try { - await this.authorization.ensureAuthorized( - alertSavedObject.attributes.alertTypeId, - alertSavedObject.attributes.consumer, - WriteOperations.Update - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: alertSavedObject.attributes.alertTypeId, + consumer: alertSavedObject.attributes.consumer, + operation: WriteOperations.Update, + }); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -829,11 +834,11 @@ export class AlertsClient { } try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.UpdateApiKey - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.UpdateApiKey, + }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); } @@ -933,11 +938,11 @@ export class AlertsClient { } try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.Enable - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Enable, + }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -1045,11 +1050,11 @@ export class AlertsClient { } try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.Disable - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.Disable, + }); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -1117,11 +1122,11 @@ export class AlertsClient { ); try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.MuteAll - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.MuteAll, + }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -1178,11 +1183,11 @@ export class AlertsClient { ); try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.UnmuteAll - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.UnmuteAll, + }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -1239,11 +1244,11 @@ export class AlertsClient { ); try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.MuteInstance - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.MuteInstance, + }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -1306,11 +1311,11 @@ export class AlertsClient { ); try { - await this.authorization.ensureAuthorized( - attributes.alertTypeId, - attributes.consumer, - WriteOperations.UnmuteInstance - ); + await this.authorization.ensureAuthorized({ + ruleTypeId: attributes.alertTypeId, + consumer: attributes.consumer, + operation: WriteOperations.UnmuteInstance, + }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); } diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index ea39b635f5045..786cee2a1363c 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -10,7 +10,7 @@ import { map, mapValues, fromPairs, has, get } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; -import { SecurityPluginSetup, AlertingActions } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; @@ -18,6 +18,11 @@ import { Space } from '../../../spaces/server'; import { asFiltersByAlertTypeAndConsumer } from './alerts_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; +export enum AlertingAuthorizationTypes { + Rule = 'rule', + Alert = 'alert', +} + export enum ReadOperations { Get = 'get', GetAlertState = 'getAlertState', @@ -38,6 +43,12 @@ export enum WriteOperations { UnmuteInstance = 'unmuteInstance', } +export interface EnsureAuthorizedOpts { + ruleTypeId: string; + consumer: string; + operation: ReadOperations | WriteOperations; +} + interface HasPrivileges { read: boolean; all: boolean; @@ -58,6 +69,7 @@ export interface ConstructorOptions { auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; privilegeName?: string; + authorizationType?: AlertingAuthorizationTypes; } export class AlertsAuthorization { @@ -68,11 +80,7 @@ export class AlertsAuthorization { private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly privilegeName: string; - private readonly getAuthorizationString: ( - alertTypeId: string, - consumer: string, - operation: string - ) => string; + private readonly alertingAuthorizationType: AlertingAuthorizationTypes; constructor({ alertTypeRegistry, @@ -82,18 +90,14 @@ export class AlertsAuthorization { auditLogger, getSpace, privilegeName, + authorizationType, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; this.privilegeName = privilegeName ?? DEFAULT_PRIVILEGE_NAME; - - const authorizationAction = get( - this.authorization!.actions, - this.privilegeName - ) as AlertingActions; - this.getAuthorizationString = authorizationAction.get; + this.alertingAuthorizationType = authorizationType ?? AlertingAuthorizationTypes.Rule; this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) @@ -140,20 +144,25 @@ export class AlertsAuthorization { // consumer determines the consumer/owner // operation enum needs to be passed in in the constructor // also pass in a type rule/alert to pass into the .get function??? - public async ensureAuthorized( - alertTypeId: string, - consumer: string, - operation: ReadOperations | WriteOperations - ) { + public async ensureAuthorized({ ruleTypeId, consumer, operation }: EnsureAuthorizedOpts) { const { authorization } = this; const isAvailableConsumer = has(await this.allPossibleConsumers, consumer); if (authorization && this.shouldCheckAuthorization()) { - const alertType = this.alertTypeRegistry.get(alertTypeId); + const ruleType = this.alertTypeRegistry.get(ruleTypeId); const requiredPrivilegesByScope = { - // authorization.actions.alerting.get needs to be able to change - consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), - producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), + consumer: authorization.actions.alerting.get( + ruleTypeId, + consumer, + this.alertingAuthorizationType, + operation + ), + producer: authorization.actions.alerting.get( + ruleTypeId, + ruleType.producer, + this.alertingAuthorizationType, + operation + ), }; // This needs to be feature flagged @@ -165,7 +174,7 @@ export class AlertsAuthorization { const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: - shouldAuthorizeConsumer && consumer !== alertType.producer + shouldAuthorizeConsumer && consumer !== ruleType.producer ? [ // check for access at consumer level requiredPrivilegesByScope.consumer, @@ -191,7 +200,7 @@ export class AlertsAuthorization { throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( username, - alertTypeId, + ruleTypeId, ScopeType.Consumer, consumer, operation @@ -202,7 +211,7 @@ export class AlertsAuthorization { if (hasAllRequested) { this.auditLogger.alertsAuthorizationSuccess( username, - alertTypeId, + ruleTypeId, ScopeType.Consumer, consumer, operation @@ -220,12 +229,12 @@ export class AlertsAuthorization { const [unauthorizedScopeType, unauthorizedScope] = shouldAuthorizeConsumer && unauthorizedScopes.consumer ? [ScopeType.Consumer, consumer] - : [ScopeType.Producer, alertType.producer]; + : [ScopeType.Producer, ruleType.producer]; throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( username, - alertTypeId, + ruleTypeId, unauthorizedScopeType, unauthorizedScope, operation @@ -236,7 +245,7 @@ export class AlertsAuthorization { throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( '', - alertTypeId, + ruleTypeId, ScopeType.Consumer, consumer, operation @@ -363,7 +372,12 @@ export class AlertsAuthorization { for (const operation of operations) { privilegeToAlertType.set( // this function needs to be swappable - this.authorization!.actions.alerting.get(alertType.id, feature, operation), + this.authorization!.actions.alerting.get( + alertType.id, + feature, + this.alertingAuthorizationType, + operation + ), [ alertType, feature, From fd1a28b177ea409ace5f387862127d4ab487dc2d Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 07:46:26 -0400 Subject: [PATCH 004/186] Passing in authorization type in each function call --- .../server/alerts_client/alerts_client.ts | 28 +++++++++++---- .../authorization/alerts_authorization.ts | 34 ++++++++++++------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 504d6b806049c..e352615569a84 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -242,6 +242,7 @@ export class AlertsClient { ruleTypeId: data.alertTypeId, consumer: data.consumer, operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, }); } catch (error) { this.auditLogger?.log( @@ -367,6 +368,7 @@ export class AlertsClient { ruleTypeId: result.attributes.alertTypeId, consumer: result.attributes.consumer, operation: ReadOperations.Get, + authorizationType: AlertingAuthorizationTypes.Rule, }); } catch (error) { this.auditLogger?.log( @@ -393,6 +395,7 @@ export class AlertsClient { ruleTypeId: alert.alertTypeId, consumer: alert.consumer, operation: ReadOperations.GetAlertState, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (alert.scheduledTaskId) { const { state } = taskInstanceToAlertTaskInstance( @@ -413,6 +416,7 @@ export class AlertsClient { ruleTypeId: alert.alertTypeId, consumer: alert.consumer, operation: ReadOperations.GetAlertInstanceSummary, + authorizationType: AlertingAuthorizationTypes.Rule, }); // default duration of instance summary is 60 * alert interval @@ -454,7 +458,9 @@ export class AlertsClient { }: { options?: FindOptions } = {}): Promise> { let authorizationTuple; try { - authorizationTuple = await this.authorization.getFindAuthorizationFilter(); + authorizationTuple = await this.authorization.getFindAuthorizationFilter( + AlertingAuthorizationTypes.Rule + ); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -534,7 +540,7 @@ export class AlertsClient { const { filter: authorizationFilter, logSuccessfulAuthorization, - } = await this.authorization.getFindAuthorizationFilter(); + } = await this.authorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule); const filter = options.filter ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` : `alert.attributes.executionStatus.status:(${status})`; @@ -593,6 +599,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.Delete, + authorizationType: AlertingAuthorizationTypes.Rule, }); } catch (error) { this.auditLogger?.log( @@ -666,6 +673,7 @@ export class AlertsClient { ruleTypeId: alertSavedObject.attributes.alertTypeId, consumer: alertSavedObject.attributes.consumer, operation: WriteOperations.Update, + authorizationType: AlertingAuthorizationTypes.Rule, }); } catch (error) { this.auditLogger?.log( @@ -838,6 +846,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.UpdateApiKey, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -942,6 +951,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.Enable, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (attributes.actions.length) { @@ -1054,6 +1064,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.Disable, + authorizationType: AlertingAuthorizationTypes.Rule, }); } catch (error) { this.auditLogger?.log( @@ -1126,6 +1137,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.MuteAll, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (attributes.actions.length) { @@ -1187,6 +1199,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.UnmuteAll, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (attributes.actions.length) { @@ -1248,6 +1261,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.MuteInstance, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (attributes.actions.length) { @@ -1315,6 +1329,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.UnmuteInstance, + authorizationType: AlertingAuthorizationTypes.Rule, }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -1356,10 +1371,11 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [ - ReadOperations.Get, - WriteOperations.Create, - ]); + return await this.authorization.filterByAlertTypeAuthorization( + this.alertTypeRegistry.list(), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationTypes.Rule + ); } private async scheduleAlert(id: string, alertTypeId: string, schedule: IntervalSchedule) { diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 786cee2a1363c..19b928226dd73 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -47,6 +47,7 @@ export interface EnsureAuthorizedOpts { ruleTypeId: string; consumer: string; operation: ReadOperations | WriteOperations; + authorizationType: AlertingAuthorizationTypes; } interface HasPrivileges { @@ -69,7 +70,6 @@ export interface ConstructorOptions { auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; privilegeName?: string; - authorizationType?: AlertingAuthorizationTypes; } export class AlertsAuthorization { @@ -80,7 +80,6 @@ export class AlertsAuthorization { private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly privilegeName: string; - private readonly alertingAuthorizationType: AlertingAuthorizationTypes; constructor({ alertTypeRegistry, @@ -90,14 +89,12 @@ export class AlertsAuthorization { auditLogger, getSpace, privilegeName, - authorizationType, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; this.privilegeName = privilegeName ?? DEFAULT_PRIVILEGE_NAME; - this.alertingAuthorizationType = authorizationType ?? AlertingAuthorizationTypes.Rule; this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) @@ -144,7 +141,12 @@ export class AlertsAuthorization { // consumer determines the consumer/owner // operation enum needs to be passed in in the constructor // also pass in a type rule/alert to pass into the .get function??? - public async ensureAuthorized({ ruleTypeId, consumer, operation }: EnsureAuthorizedOpts) { + public async ensureAuthorized({ + ruleTypeId, + consumer, + operation, + authorizationType, + }: EnsureAuthorizedOpts) { const { authorization } = this; const isAvailableConsumer = has(await this.allPossibleConsumers, consumer); @@ -154,13 +156,13 @@ export class AlertsAuthorization { consumer: authorization.actions.alerting.get( ruleTypeId, consumer, - this.alertingAuthorizationType, + authorizationType, operation ), producer: authorization.actions.alerting.get( ruleTypeId, ruleType.producer, - this.alertingAuthorizationType, + authorizationType, operation ), }; @@ -254,7 +256,9 @@ export class AlertsAuthorization { } } - public async getFindAuthorizationFilter(): Promise<{ + public async getFindAuthorizationFilter( + authorizationType: AlertingAuthorizationTypes + ): Promise<{ filter?: KueryNode; ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; logSuccessfulAuthorization: () => void; @@ -265,7 +269,8 @@ export class AlertsAuthorization { [ // maybe pass in this operation? or require it to be 'find' ReadOperations.Find, - ] + ], + authorizationType ); if (!authorizedAlertTypes.size) { @@ -333,18 +338,21 @@ export class AlertsAuthorization { public async filterByAlertTypeAuthorization( alertTypes: Set, - operations: Array + operations: Array, + authorizationType: AlertingAuthorizationTypes ): Promise> { const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( alertTypes, - operations + operations, + authorizationType ); return authorizedAlertTypes; } private async augmentAlertTypesWithAuthorization( alertTypes: Set, - operations: Array + operations: Array, + authorizationType: AlertingAuthorizationTypes ): Promise<{ username?: string; hasAllRequested: boolean; @@ -375,7 +383,7 @@ export class AlertsAuthorization { this.authorization!.actions.alerting.get( alertType.id, feature, - this.alertingAuthorizationType, + authorizationType, operation ), [ From a97e064e6d9a5863f3269705f6665e83f08c273f Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 08:36:53 -0400 Subject: [PATCH 005/186] Passing in exempt consumer ids. Adding authorization type to audit logger --- .../alerting_authorization_client_factory.ts | 7 ++- .../alerting/server/alerts_client_factory.ts | 6 ++- .../authorization/alerts_authorization.ts | 49 +++++++++---------- .../server/authorization/audit_logger.ts | 25 +++++++--- x-pack/plugins/alerting/server/plugin.ts | 9 ++-- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts index 8b8707ac96107..cd1891e7a0f15 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -42,7 +42,11 @@ export class AlertingAuthorizationClientFactory { this.features = options.features; } - public create(request: KibanaRequest, privilegeName?: string): AlertsAuthorization { + public create( + request: KibanaRequest, + privilegeName: string, + exemptConsumerIds: string[] = [] + ): AlertsAuthorization { const { securityPluginSetup, securityPluginStart, features } = this; return new AlertsAuthorization({ authorization: securityPluginStart?.authz, @@ -54,6 +58,7 @@ export class AlertingAuthorizationClientFactory { securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) ), privilegeName, + exemptConsumerIds, }); } } diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 455781e067ce0..36eefc0ccaafd 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -19,7 +19,9 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve import { TaskManagerStartContract } from '../../task_manager/server'; import { IEventLogClientService } from '../../../plugins/event_log/server'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; +import { ALERTS_FEATURE_ID } from '../common'; +const FEATURE_PRIVILEGE_NAME = 'alerting'; export interface AlertsClientFactoryOpts { logger: Logger; taskManager: TaskManagerStartContract; @@ -83,7 +85,9 @@ export class AlertsClientFactory { excludedWrappers: ['security'], includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), - authorization: this.authorization!.create(request), + authorization: this.authorization!.create(request, FEATURE_PRIVILEGE_NAME, [ + ALERTS_FEATURE_ID, + ]), actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 19b928226dd73..816a370babec7 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -60,16 +60,15 @@ export interface RegistryAlertTypeWithAuth extends RegistryAlertType { } type IsAuthorizedAtProducerLevel = boolean; - -const DEFAULT_PRIVILEGE_NAME = 'alerting'; export interface ConstructorOptions { alertTypeRegistry: AlertTypeRegistry; request: KibanaRequest; features: FeaturesPluginStart; getSpace: (request: KibanaRequest) => Promise; auditLogger: AlertsAuthorizationAuditLogger; + privilegeName: string; + exemptConsumerIds: string[]; authorization?: SecurityPluginSetup['authz']; - privilegeName?: string; } export class AlertsAuthorization { @@ -80,6 +79,7 @@ export class AlertsAuthorization { private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly privilegeName: string; + private readonly exemptConsumerIds: string[]; constructor({ alertTypeRegistry, @@ -89,12 +89,18 @@ export class AlertsAuthorization { auditLogger, getSpace, privilegeName, + exemptConsumerIds, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; - this.privilegeName = privilegeName ?? DEFAULT_PRIVILEGE_NAME; + this.privilegeName = privilegeName; + + // List of consumer ids that are exempt from privilege check. This should be used sparingly. + // An example of this is the Rules Management `consumer` as we don't want to have to + // manually authorize each rule type in the management UI. + this.exemptConsumerIds = exemptConsumerIds; this.featuresIds = getSpace(request) .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) @@ -107,7 +113,7 @@ export class AlertsAuthorization { // ignore features which are disabled in the user's space return ( !disabledFeatures.has(feature.id) && - // ignore features which don't grant privileges to alerting + // ignore features which don't grant privileges to the specified privilege (get(feature, this.privilegeName, undefined)?.length ?? 0 > 0) ); }) @@ -120,12 +126,9 @@ export class AlertsAuthorization { return new Set(); }); - // TODO - // This adds { alerts: { read: true, all: true }} to the list of consumers - // Do we need a flag to skip this??? this.allPossibleConsumers = this.featuresIds.then((featuresIds) => featuresIds.size - ? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], { + ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { read: true, all: true, }) @@ -137,10 +140,6 @@ export class AlertsAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } - // alertTypeId determines the producer - // consumer determines the consumer/owner - // operation enum needs to be passed in in the constructor - // also pass in a type rule/alert to pass into the .get function??? public async ensureAuthorized({ ruleTypeId, consumer, @@ -167,11 +166,8 @@ export class AlertsAuthorization { ), }; - // This needs to be feature flagged - - // We special case the Alerts Management `consumer` as we don't want to have to - // manually authorize each alert type in the management UI - const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; + // Skip authorizing consumer if it is in the list of exempt consumer ids + const shouldAuthorizeConsumer = !this.exemptConsumerIds.includes(consumer); const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ @@ -184,8 +180,8 @@ export class AlertsAuthorization { requiredPrivilegesByScope.producer, ] : [ - // skip consumer privilege checks under `alerts` as all alert types can - // be created under `alerts` if you have producer level privileges + // skip consumer privilege checks for exempt consumer ids as all rule types can + // be created for exempt consumers if user has producer level privileges requiredPrivilegesByScope.producer, ], }); @@ -198,14 +194,14 @@ export class AlertsAuthorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - // This should also log the type they're trying to access rule/alert throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( username, ruleTypeId, ScopeType.Consumer, consumer, - operation + operation, + authorizationType ) ); } @@ -216,7 +212,8 @@ export class AlertsAuthorization { ruleTypeId, ScopeType.Consumer, consumer, - operation + operation, + authorizationType ); } else { const authorizedPrivileges = map( @@ -239,7 +236,8 @@ export class AlertsAuthorization { ruleTypeId, unauthorizedScopeType, unauthorizedScope, - operation + operation, + authorizationType ) ); } @@ -250,7 +248,8 @@ export class AlertsAuthorization { ruleTypeId, ScopeType.Consumer, consumer, - operation + operation, + authorizationType ) ); } diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.ts index 5137b3adf8b37..36312783ff689 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.ts @@ -29,9 +29,10 @@ export class AlertsAuthorizationAuditLogger { alertTypeId: string, scopeType: ScopeType, scope: string, - operation: string + operation: string, + authorizationType: string ): string { - return `${authorizationResult} to ${operation} a "${alertTypeId}" alert ${ + return `${authorizationResult} to ${operation} a "${alertTypeId}" ${authorizationType} ${ scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` }`; } @@ -41,14 +42,16 @@ export class AlertsAuthorizationAuditLogger { alertTypeId: string, scopeType: ScopeType, scope: string, - operation: string + operation: string, + authorizationType: string ): string { const message = this.getAuthorizationMessage( AuthorizationResult.Unauthorized, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); this.auditLogger.log('alerts_authorization_failure', `${username} ${message}`, { username, @@ -56,6 +59,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, scope, operation, + authorizationType, }); return message; } @@ -74,14 +78,16 @@ export class AlertsAuthorizationAuditLogger { alertTypeId: string, scopeType: ScopeType, scope: string, - operation: string + operation: string, + authorizationType: string ): string { const message = this.getAuthorizationMessage( AuthorizationResult.Authorized, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, { username, @@ -89,6 +95,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, scope, operation, + authorizationType, }); return message; } @@ -97,12 +104,13 @@ export class AlertsAuthorizationAuditLogger { username: string, authorizedEntries: Array<[string, string]>, scopeType: ScopeType, - operation: string + operation: string, + authorizationType: string ): string { const message = `${AuthorizationResult.Authorized} to ${operation}: ${authorizedEntries .map( ([alertTypeId, scope]) => - `"${alertTypeId}" alert ${ + `"${alertTypeId}" ${authorizationType}s ${ scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` }` ) @@ -112,6 +120,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, authorizedEntries, operation, + authorizationType, }); return message; } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index e8a2b9dbece9b..f6b7397eab2c9 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -105,7 +105,10 @@ export interface PluginSetupContract { export interface PluginStartContract { listTypes: AlertTypeRegistry['list']; getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf; - getAlertingAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; + getAlertingAuthorizationWithRequest( + request: KibanaRequest, + privilegeName: string + ): PublicMethodsOf; getFrameworkHealth: () => Promise; } @@ -340,8 +343,8 @@ export class AlertingPlugin { return alertsClientFactory!.create(request, core.savedObjects); }; - const getAlertingAuthorizationWithRequest = (request: KibanaRequest) => { - return alertingAuthorizationClientFactory!.create(request); + const getAlertingAuthorizationWithRequest = (request: KibanaRequest, privilegeName: string) => { + return alertingAuthorizationClientFactory!.create(request, privilegeName); }; taskRunnerFactory.initialize({ From b9ee8ac723c3e4289b3a235677bb007236f60678 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 09:03:49 -0400 Subject: [PATCH 006/186] Changing alertType to ruleType --- .../server/alerts_client/alerts_client.ts | 6 +- .../authorization/alerts_authorization.ts | 111 +++++++++--------- 2 files changed, 57 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index e352615569a84..9f9a18ab524c8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -472,7 +472,7 @@ export class AlertsClient { } const { filter: authorizationFilter, - ensureAlertTypeIsAuthorized, + ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, } = authorizationTuple; @@ -494,7 +494,7 @@ export class AlertsClient { const authorizedData = data.map(({ id, attributes, references }) => { try { - ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); + ensureRuleTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); } catch (error) { this.auditLogger?.log( alertAuditEvent({ @@ -1371,7 +1371,7 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.authorization.filterByAlertTypeAuthorization( + return await this.authorization.filterByRuleTypeAuthorization( this.alertTypeRegistry.list(), [ReadOperations.Get, WriteOperations.Create], AlertingAuthorizationTypes.Rule diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 816a370babec7..edbc0e74f2f7f 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -15,7 +15,7 @@ import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; -import { asFiltersByAlertTypeAndConsumer } from './alerts_authorization_kuery'; +import { asFiltersByRuleTypeAndConsumer } from './alerts_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; export enum AlertingAuthorizationTypes { @@ -259,11 +259,11 @@ export class AlertsAuthorization { authorizationType: AlertingAuthorizationTypes ): Promise<{ filter?: KueryNode; - ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; + ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string) => void; logSuccessfulAuthorization: () => void; }> { if (this.authorization && this.shouldCheckAuthorization()) { - const { username, authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( + const { username, authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( this.alertTypeRegistry.list(), [ // maybe pass in this operation? or require it to be 'find' @@ -272,40 +272,41 @@ export class AlertsAuthorization { authorizationType ); - if (!authorizedAlertTypes.size) { + if (!authorizedRuleTypes.size) { throw Boom.forbidden( this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find') ); } - const authorizedAlertTypeIdsToConsumers = new Set( - [...authorizedAlertTypes].reduce((alertTypeIdConsumerPairs, alertType) => { - for (const consumer of Object.keys(alertType.authorizedConsumers)) { - alertTypeIdConsumerPairs.push(`${alertType.id}/${consumer}`); + const authorizedRuleTypeIdsToConsumers = new Set( + [...authorizedRuleTypes].reduce((ruleTypeIdConsumerPairs, ruleType) => { + for (const consumer of Object.keys(ruleType.authorizedConsumers)) { + ruleTypeIdConsumerPairs.push(`${ruleType.id}/${consumer}`); } - return alertTypeIdConsumerPairs; + return ruleTypeIdConsumerPairs; }, []) ); const authorizedEntries: Map> = new Map(); return { - filter: asFiltersByAlertTypeAndConsumer(authorizedAlertTypes), - ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => { - if (!authorizedAlertTypeIdsToConsumers.has(`${alertTypeId}/${consumer}`)) { + filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes), + ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string) => { + if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}`)) { throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( username!, - alertTypeId, + ruleTypeId, ScopeType.Consumer, consumer, - 'find' + 'find', + authorizationType ) ); } else { - if (authorizedEntries.has(alertTypeId)) { - authorizedEntries.get(alertTypeId)!.add(consumer); + if (authorizedEntries.has(ruleTypeId)) { + authorizedEntries.get(ruleTypeId)!.add(consumer); } else { - authorizedEntries.set(alertTypeId, new Set([consumer])); + authorizedEntries.set(ruleTypeId, new Set([consumer])); } } }, @@ -323,39 +324,40 @@ export class AlertsAuthorization { [] ), ScopeType.Consumer, - 'find' + 'find', + authorizationType ); } }, }; } return { - ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {}, + ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string) => {}, logSuccessfulAuthorization: () => {}, }; } - public async filterByAlertTypeAuthorization( - alertTypes: Set, + public async filterByRuleTypeAuthorization( + ruleTypes: Set, operations: Array, authorizationType: AlertingAuthorizationTypes ): Promise> { - const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( - alertTypes, + const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( + ruleTypes, operations, authorizationType ); - return authorizedAlertTypes; + return authorizedRuleTypes; } - private async augmentAlertTypesWithAuthorization( - alertTypes: Set, + private async augmentRuleTypesWithAuthorization( + ruleTypes: Set, operations: Array, authorizationType: AlertingAuthorizationTypes ): Promise<{ username?: string; hasAllRequested: boolean; - authorizedAlertTypes: Set; + authorizedRuleTypes: Set; }> { const featuresIds = await this.featuresIds; if (this.authorization && this.shouldCheckAuthorization()) { @@ -363,81 +365,76 @@ export class AlertsAuthorization { this.request ); - // add an empty `authorizedConsumers` array on each alertType - const alertTypesWithAuthorization = this.augmentWithAuthorizedConsumers(alertTypes, {}); + // add an empty `authorizedConsumers` array on each ruleType + const ruleTypesWithAuthorization = this.augmentWithAuthorizedConsumers(ruleTypes, {}); // map from privilege to alertType which we can refer back to when analyzing the result // of checkPrivileges - const privilegeToAlertType = new Map< + const privilegeToRuleType = new Map< string, [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel] >(); // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege - for (const alertType of alertTypesWithAuthorization) { + for (const ruleType of ruleTypesWithAuthorization) { for (const feature of featuresIds) { for (const operation of operations) { - privilegeToAlertType.set( + privilegeToRuleType.set( // this function needs to be swappable this.authorization!.actions.alerting.get( - alertType.id, + ruleType.id, feature, authorizationType, operation ), - [ - alertType, - feature, - hasPrivilegeByOperation(operation), - alertType.producer === feature, - ] + [ruleType, feature, hasPrivilegeByOperation(operation), ruleType.producer === feature] ); } } } const { username, hasAllRequested, privileges } = await checkPrivileges({ - kibana: [...privilegeToAlertType.keys()], + kibana: [...privilegeToRuleType.keys()], }); return { username, hasAllRequested, - authorizedAlertTypes: hasAllRequested + authorizedRuleTypes: hasAllRequested ? // has access to all features - this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers) + this.augmentWithAuthorizedConsumers(ruleTypes, await this.allPossibleConsumers) : // only has some of the required privileges - privileges.kibana.reduce((authorizedAlertTypes, { authorized, privilege }) => { - if (authorized && privilegeToAlertType.has(privilege)) { + privileges.kibana.reduce((authorizedRuleTypes, { authorized, privilege }) => { + if (authorized && privilegeToRuleType.has(privilege)) { const [ - alertType, + ruleType, feature, hasPrivileges, isAuthorizedAtProducerLevel, - ] = privilegeToAlertType.get(privilege)!; - alertType.authorizedConsumers[feature] = mergeHasPrivileges( + ] = privilegeToRuleType.get(privilege)!; + ruleType.authorizedConsumers[feature] = mergeHasPrivileges( hasPrivileges, - alertType.authorizedConsumers[feature] + ruleType.authorizedConsumers[feature] ); // this needs to be feature flagged if (isAuthorizedAtProducerLevel) { // granting privileges under the producer automatically authorized the Alerts Management UI as well - alertType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( + ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( hasPrivileges, - alertType.authorizedConsumers[ALERTS_FEATURE_ID] + ruleType.authorizedConsumers[ALERTS_FEATURE_ID] ); } - authorizedAlertTypes.add(alertType); + authorizedRuleTypes.add(ruleType); } - return authorizedAlertTypes; + return authorizedRuleTypes; }, new Set()), }; } else { return { hasAllRequested: true, - authorizedAlertTypes: this.augmentWithAuthorizedConsumers( - new Set([...alertTypes].filter((alertType) => featuresIds.has(alertType.producer))), + authorizedRuleTypes: this.augmentWithAuthorizedConsumers( + new Set([...ruleTypes].filter((ruleType) => featuresIds.has(ruleType.producer))), await this.allPossibleConsumers ), }; @@ -445,12 +442,12 @@ export class AlertsAuthorization { } private augmentWithAuthorizedConsumers( - alertTypes: Set, + ruleTypes: Set, authorizedConsumers: AuthorizedConsumers ): Set { return new Set( - Array.from(alertTypes).map((alertType) => ({ - ...alertType, + Array.from(ruleTypes).map((ruleType) => ({ + ...ruleType, authorizedConsumers: { ...authorizedConsumers }, })) ); From cd6185f534d1f5b7e327a75e6a090230d7abc9d3 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 09:34:06 -0400 Subject: [PATCH 007/186] Changing alertType to ruleType --- .../authorization/alerts_authorization.ts | 22 +++++++++---------- .../alerts_authorization_kuery.ts | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index edbc0e74f2f7f..e9e1a9c37e7df 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -265,10 +265,7 @@ export class AlertsAuthorization { if (this.authorization && this.shouldCheckAuthorization()) { const { username, authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( this.alertTypeRegistry.list(), - [ - // maybe pass in this operation? or require it to be 'find' - ReadOperations.Find, - ], + [ReadOperations.Find], authorizationType ); @@ -368,7 +365,7 @@ export class AlertsAuthorization { // add an empty `authorizedConsumers` array on each ruleType const ruleTypesWithAuthorization = this.augmentWithAuthorizedConsumers(ruleTypes, {}); - // map from privilege to alertType which we can refer back to when analyzing the result + // map from privilege to ruleType which we can refer back to when analyzing the result // of checkPrivileges const privilegeToRuleType = new Map< string, @@ -417,13 +414,14 @@ export class AlertsAuthorization { ruleType.authorizedConsumers[feature] ); - // this needs to be feature flagged - if (isAuthorizedAtProducerLevel) { - // granting privileges under the producer automatically authorized the Alerts Management UI as well - ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[ALERTS_FEATURE_ID] - ); + if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) { + // granting privileges under the producer automatically authorized exempt consumer IDs as well + this.exemptConsumerIds.forEach((exemptId: string) => { + ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges( + hasPrivileges, + ruleType.authorizedConsumers[exemptId] + ); + }); } authorizedRuleTypes.add(ruleType); } diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts index 3c5ff4345095b..d017361456752 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts @@ -11,7 +11,7 @@ import { KueryNode } from '../../../../../src/plugins/data/server'; import { RegistryAlertTypeWithAuth } from './alerts_authorization'; // pass in the field name instead of hardcoding `alert.attributes.alertTypeId` and `alertTypeId` -export function asFiltersByAlertTypeAndConsumer( +export function asFiltersByRuleTypeAndConsumer( alertTypes: Set ): KueryNode { return nodeBuilder.or( From 6daa4702bfbb0709879bcc06fb7f16082b2dbfcb Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 10:49:45 -0400 Subject: [PATCH 008/186] Updating unit tests --- .../server/alerts_client/alerts_client.ts | 6 +- .../alerts_client/tests/aggregate.test.ts | 4 +- .../server/alerts_client/tests/find.test.ts | 14 +- .../tests/list_alert_types.test.ts | 4 +- .../alerts_authorization.mock.ts | 2 +- .../alerts_authorization.test.ts | 970 ++++++++++++------ .../authorization/alerts_authorization.ts | 12 +- .../server/authorization/audit_logger.test.ts | 61 +- .../server/authorization/audit_logger.ts | 8 +- .../__snapshots__/alerting.test.ts.snap | 24 + .../authorization/actions/alerting.test.ts | 25 +- 11 files changed, 778 insertions(+), 352 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 9f9a18ab524c8..c1ef670ca3bc2 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -494,7 +494,11 @@ export class AlertsClient { const authorizedData = data.map(({ id, attributes, references }) => { try { - ensureRuleTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); + ensureRuleTypeIsAuthorized( + attributes.alertTypeId, + attributes.consumer, + AlertingAuthorizationTypes.Rule + ); } catch (error) { this.auditLogger?.log( alertAuditEvent({ diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts index 81240f1e88531..896a070db945e 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts @@ -67,7 +67,7 @@ describe('aggregate()', () => { ]); beforeEach(() => { authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized() {}, + ensureRuleTypeIsAuthorized() {}, logSuccessfulAuthorization() {}, }); unsecuredSavedObjectsClient.find @@ -102,7 +102,7 @@ describe('aggregate()', () => { saved_objects: [], }); alertTypeRegistry.list.mockReturnValue(listedTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( + authorization.filterByRuleTypeAuthorization.mockResolvedValue( new Set([ { id: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts index bfeecd4540d15..b71a50a24db64 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts @@ -75,7 +75,7 @@ describe('find()', () => { ]); beforeEach(() => { authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized() {}, + ensureRuleTypeIsAuthorized() {}, logSuccessfulAuthorization() {}, }); unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ @@ -117,7 +117,7 @@ describe('find()', () => { ], }); alertTypeRegistry.list.mockReturnValue(listedTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( + authorization.filterByRuleTypeAuthorization.mockResolvedValue( new Set([ { id: 'myType', @@ -196,7 +196,7 @@ describe('find()', () => { ); authorization.getFindAuthorizationFilter.mockResolvedValue({ filter, - ensureAlertTypeIsAuthorized() {}, + ensureRuleTypeIsAuthorized() {}, logSuccessfulAuthorization() {}, }); @@ -219,10 +219,10 @@ describe('find()', () => { }); test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { - const ensureAlertTypeIsAuthorized = jest.fn(); + const ensureRuleTypeIsAuthorized = jest.fn(); const logSuccessfulAuthorization = jest.fn(); authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized, + ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, }); @@ -271,7 +271,7 @@ describe('find()', () => { fields: ['tags', 'alertTypeId', 'consumer'], type: 'alert', }); - expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); + expect(ensureRuleTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'rule'); expect(logSuccessfulAuthorization).toHaveBeenCalled(); }); }); @@ -313,7 +313,7 @@ describe('find()', () => { test('logs audit event when not authorised to search alert type', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized: jest.fn(() => { + ensureRuleTypeIsAuthorized: jest.fn(() => { throw new Error('Unauthorized'); }), logSuccessfulAuthorization: jest.fn(), diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts index b8d597ab15471..39aad2150a91b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts @@ -89,7 +89,7 @@ describe('listAlertTypes', () => { test('should return a list of AlertTypes that exist in the registry', async () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( + authorization.filterByRuleTypeAuthorization.mockResolvedValue( new Set([ { ...myAppAlertType, authorizedConsumers }, { ...alertingAlertType, authorizedConsumers }, @@ -147,7 +147,7 @@ describe('listAlertTypes', () => { enabledInLicense: true, }, ]); - authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); + authorization.filterByRuleTypeAuthorization.mockResolvedValue(authorizedTypes); expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts index e996258f07717..e2789c5fe7d45 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts @@ -14,7 +14,7 @@ export type AlertsAuthorizationMock = jest.Mocked; const createAlertsAuthorizationMock = () => { const mocked: AlertsAuthorizationMock = { ensureAuthorized: jest.fn(), - filterByAlertTypeAuthorization: jest.fn(), + filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), }; return mocked; diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts index bc4404b3e0a4b..9fb3d8468f5a7 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts @@ -13,7 +13,12 @@ import { KibanaFeature, } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { AlertsAuthorization, WriteOperations, ReadOperations } from './alerts_authorization'; +import { + AlertsAuthorization, + WriteOperations, + ReadOperations, + AlertingAuthorizationTypes, +} from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import uuid from 'uuid'; @@ -29,8 +34,15 @@ const realAuditLogger = new AlertsAuthorizationAuditLogger(); const getSpace = jest.fn(); -const mockAuthorizationAction = (type: string, app: string, operation: string) => - `${type}/${app}/${operation}`; +const privilegeName = 'alerting'; +const exemptConsumerIds: string[] = []; + +const mockAuthorizationAction = ( + type: string, + app: string, + alertingType: string, + operation: string +) => `${type}/${app}/${alertingType}/${operation}`; function mockSecurity() { const security = securityMock.createSetup(); const authorization = security.authz; @@ -205,6 +217,8 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); expect(getSpace).toHaveBeenCalledWith(request); @@ -219,9 +233,16 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, + }); expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); @@ -236,14 +257,21 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, + }); expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); - test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { + test('ensures the user has privileges to execute the specified rule type, operation and alerting type without consumer when producer and consumer are the same', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -256,6 +284,8 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -264,29 +294,41 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [] }, }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, + }); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'rule', + 'create' + ); expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'create')], + kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myApp", - "create", - ] - `); + Array [ + "some-user", + "myType", + 0, + "myApp", + "create", + "rule", + ] + `); }); - test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { + test('ensures the user has privileges to execute the specified rule type, operation and alerting type without consumer when consumer is exempt', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -299,6 +341,8 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds: ['exemptConsumer'], }); checkPrivileges.mockResolvedValueOnce({ @@ -307,29 +351,47 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [] }, }); - await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'exemptConsumer', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, + }); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'exemptConsumer', + 'rule', + 'create' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'rule', + 'create' + ); expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'create')], + kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "alerts", - "create", - ] - `); + Array [ + "some-user", + "myType", + 0, + "exemptConsumer", + "create", + "rule", + ] + `); }); - test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { + test('ensures the user has privileges to execute the specified rule type, operation, alerting type and producer when producer is different from consumer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -348,39 +410,54 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); - await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create); + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myOtherApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, + }); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'rule', + 'create' + ); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', 'myOtherApp', + 'rule', 'create' ); expect(checkPrivileges).toHaveBeenCalledWith({ kibana: [ - mockAuthorizationAction('myType', 'myOtherApp', 'create'), - mockAuthorizationAction('myType', 'myApp', 'create'), + mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), + mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), ], }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myOtherApp", - "create", - ] - `); + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + "rule", + ] + `); }); - test('throws if user lacks the required privieleges for the consumer', async () => { + test('throws if user lacks the required privileges for the consumer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -393,6 +470,8 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -401,11 +480,11 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), authorized: false, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), authorized: true, }, ], @@ -413,22 +492,28 @@ describe('AlertsAuthorization', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myOtherApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Rule, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` + `"Unauthorized to create a \\"myType\\" rule for \\"myOtherApp\\""` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myOtherApp", - "create", - ] - `); + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + "rule", + ] + `); }); test('throws if user lacks the required privieleges for the producer', async () => { @@ -444,6 +529,8 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -452,11 +539,11 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'create'), authorized: false, }, ], @@ -464,7 +551,12 @@ describe('AlertsAuthorization', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myOtherApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Alert, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` ); @@ -472,14 +564,15 @@ describe('AlertsAuthorization', () => { expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 1, - "myApp", - "create", - ] - `); + Array [ + "some-user", + "myType", + 1, + "myApp", + "create", + "alert", + ] + `); }); test('throws if user lacks the required privieleges for both consumer and producer', async () => { @@ -495,6 +588,8 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -503,11 +598,11 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'create'), authorized: false, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'create'), authorized: false, }, ], @@ -515,7 +610,12 @@ describe('AlertsAuthorization', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myOtherApp', + operation: WriteOperations.Create, + authorizationType: AlertingAuthorizationTypes.Alert, + }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); @@ -523,14 +623,15 @@ describe('AlertsAuthorization', () => { expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myOtherApp", - "create", - ] - `); + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + "alert", + ] + `); }); }); @@ -577,14 +678,16 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); const { filter, - ensureAlertTypeIsAuthorized, - } = await alertAuthorization.getFindAuthorizationFilter(); + ensureRuleTypeIsAuthorized, + } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule); - expect(() => ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp')).not.toThrow(); + expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); expect(filter).toEqual(undefined); }); @@ -596,11 +699,15 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( + AlertingAuthorizationTypes.Rule + ); - ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp'); + ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule'); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -625,20 +732,25 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); // TODO: once issue https://github.com/elastic/kibana/issues/89473 is // resolved, we can start using this code again, instead of toMatchSnapshot(): // - // expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toEqual( + // expect((await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule)).filter).toEqual( // esKuery.fromKueryExpression( // `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` // ) // ); // This code is the replacement code for above - expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchSnapshot(); + // expect( + // (await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule)) + // .filter + // ).toMatchSnapshot(); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); }); @@ -655,19 +767,24 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'find' + ), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'find'), authorized: false, }, ], @@ -681,12 +798,16 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( + AlertingAuthorizationTypes.Alert + ); expect(() => { - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'alert'); }).toThrowErrorMatchingInlineSnapshot( `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` ); @@ -694,14 +815,15 @@ describe('AlertsAuthorization', () => { expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myAppAlertType", - 0, - "myOtherApp", - "find", - ] - `); + Array [ + "some-user", + "myAppAlertType", + 0, + "myOtherApp", + "find", + "alert", + ] + `); }); test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { @@ -716,19 +838,24 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'rule', + 'find' + ), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), authorized: true, }, ], @@ -742,12 +869,16 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( + AlertingAuthorizationTypes.Rule + ); expect(() => { - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); }).not.toThrow(); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -766,27 +897,37 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'rule', + 'find' + ), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction( + 'mySecondAppAlertType', + 'myOtherApp', + 'rule', + 'find' + ), authorized: true, }, ], @@ -800,17 +941,19 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { - ensureAlertTypeIsAuthorized, + ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, - } = await alertAuthorization.getFindAuthorizationFilter(); + } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule); expect(() => { - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); - ensureAlertTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp'); - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); + ensureRuleTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp', 'rule'); + ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); }).not.toThrow(); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -820,22 +963,23 @@ describe('AlertsAuthorization', () => { expect(auditLogger.alertsBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", Array [ + "some-user", Array [ - "myAppAlertType", - "myOtherApp", - ], - Array [ - "mySecondAppAlertType", - "myOtherApp", + Array [ + "myAppAlertType", + "myOtherApp", + ], + Array [ + "mySecondAppAlertType", + "myOtherApp", + ], ], - ], - 0, - "find", - ] - `); + 0, + "find", + "rule", + ] + `); }); }); @@ -871,13 +1015,16 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.filterByAlertTypeAuthorization( + alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create] + [WriteOperations.Create], + AlertingAuthorizationTypes.Rule ) ).resolves.toMatchInlineSnapshot(` Set { @@ -885,10 +1032,6 @@ describe('AlertsAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, "myApp": Object { "all": true, "read": true, @@ -917,10 +1060,6 @@ describe('AlertsAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, "myApp": Object { "all": true, "read": true, @@ -949,51 +1088,23 @@ describe('AlertsAuthorization', () => { `); }); - test('augments a list of types with consumers under which the operation is authorized', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - ], - }, - }); - + test('augments a list of types with all features and exempt consumer ids when there is no authorization api', async () => { const alertAuthorization = new AlertsAuthorization({ request, - authorization, alertTypeRegistry, features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.filterByAlertTypeAuthorization( + alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create] + [WriteOperations.Create], + AlertingAuthorizationTypes.Rule ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1001,17 +1112,33 @@ describe('AlertsAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "exemptConsumerA": Object { + "all": true, + "read": true, + }, + "exemptConsumerB": Object { + "all": true, + "read": true, + }, "myApp": Object { "all": true, "read": true, }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, }, "defaultActionGroupId": "default", "enabledInLicense": true, - "id": "myOtherAppAlertType", + "id": "myAppAlertType", "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", + "name": "myAppAlertType", + "producer": "myApp", "recoveryActionGroup": Object { "id": "recovered", "name": "Recovered", @@ -1021,7 +1148,11 @@ describe('AlertsAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { + "exemptConsumerA": Object { + "all": true, + "read": true, + }, + "exemptConsumerB": Object { "all": true, "read": true, }, @@ -1029,6 +1160,10 @@ describe('AlertsAuthorization', () => { "all": true, "read": true, }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, "myOtherApp": Object { "all": true, "read": true, @@ -1036,10 +1171,10 @@ describe('AlertsAuthorization', () => { }, "defaultActionGroupId": "default", "enabledInLicense": true, - "id": "myAppAlertType", + "id": "myOtherAppAlertType", "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", "recoveryActionGroup": Object { "id": "recovered", "name": "Recovered", @@ -1049,7 +1184,7 @@ describe('AlertsAuthorization', () => { `); }); - test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { + test('augments a list of types with consumers under which the operation is authorized', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -1061,13 +1196,26 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'rule', + 'create' + ), authorized: false, }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'create'), + authorized: true, + }, ], }, }); @@ -1079,41 +1227,244 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.filterByAlertTypeAuthorization(new Set([myAppAlertType]), [ - WriteOperations.Create, - ]) + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create], + AlertingAuthorizationTypes.Rule + ) ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, }, - "myApp": Object { - "all": true, - "read": true, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", }, }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myAppAlertType", - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, }, - }, - } - `); + } + `); + }); + + test('augments a list of types with consumers and exempt consumer ids under which the operation is authorized', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'rule', + 'create' + ), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'create'), + authorized: true, + }, + ], + }, + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + privilegeName, + exemptConsumerIds: ['exemptConsumerA'], + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create], + AlertingAuthorizationTypes.Rule + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "exemptConsumerA": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + } + `); + }); + + test('authorizes user under exempt consumers when they are authorized by the producer', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), + authorized: false, + }, + ], + }, + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + privilegeName, + exemptConsumerIds: ['exemptConsumerA'], + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType]), + [WriteOperations.Create], + AlertingAuthorizationTypes.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "exemptConsumerA": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + }, + } + `); }); test('augments a list of types with consumers under which multiple operations are authorized', async () => { @@ -1128,35 +1479,45 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'create' + ), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), authorized: false, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'get' + ), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'get'), authorized: true, }, ], @@ -1170,74 +1531,69 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.filterByAlertTypeAuthorization( + alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create, ReadOperations.Get] + [WriteOperations.Create, ReadOperations.Get], + AlertingAuthorizationTypes.Alert ) ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": false, - "read": true, + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, }, - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": false, - "read": true, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", }, }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myOtherAppAlertType", - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": false, - "read": true, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": false, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, }, - "myApp": Object { - "all": false, - "read": true, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", }, - "myOtherApp": Object { - "all": false, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myAppAlertType", - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", }, - }, - } - `); + } + `); }); test('omits types which have no consumers under which the operation is authorized', async () => { @@ -1252,19 +1608,24 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'create' + ), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), authorized: false, }, ], @@ -1278,46 +1639,45 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, + privilegeName, + exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.filterByAlertTypeAuthorization( + alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create] + [WriteOperations.Create], + AlertingAuthorizationTypes.Alert ) ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, }, - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, + "defaultActionGroupId": "default", + "enabledInLicense": true, + "id": "myOtherAppAlertType", + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", }, }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myOtherAppAlertType", - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - } - `); + } + `); }); }); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index e9e1a9c37e7df..daf37f21c2b8d 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -259,7 +259,7 @@ export class AlertsAuthorization { authorizationType: AlertingAuthorizationTypes ): Promise<{ filter?: KueryNode; - ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string) => void; + ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; logSuccessfulAuthorization: () => void; }> { if (this.authorization && this.shouldCheckAuthorization()) { @@ -271,14 +271,14 @@ export class AlertsAuthorization { if (!authorizedRuleTypes.size) { throw Boom.forbidden( - this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find') + this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find', authorizationType) ); } const authorizedRuleTypeIdsToConsumers = new Set( [...authorizedRuleTypes].reduce((ruleTypeIdConsumerPairs, ruleType) => { for (const consumer of Object.keys(ruleType.authorizedConsumers)) { - ruleTypeIdConsumerPairs.push(`${ruleType.id}/${consumer}`); + ruleTypeIdConsumerPairs.push(`${ruleType.id}/${consumer}/${authorizationType}`); } return ruleTypeIdConsumerPairs; }, []) @@ -287,8 +287,8 @@ export class AlertsAuthorization { const authorizedEntries: Map> = new Map(); return { filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes), - ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string) => { - if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}`)) { + ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { + if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( username!, @@ -329,7 +329,7 @@ export class AlertsAuthorization { }; } return { - ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string) => {}, + ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => {}, logSuccessfulAuthorization: () => {}, }; } diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts index 948c745eb2fb2..2d79fd8004acd 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts @@ -22,13 +22,15 @@ describe(`#constructor`, () => { const scopeType = ScopeType.Consumer; const scope = 'myApp'; const operation = 'create'; + const authorizationType = 'rule'; expect(() => { alertsAuditLogger.alertsAuthorizationFailure( username, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); alertsAuditLogger.alertsAuthorizationSuccess( @@ -36,7 +38,8 @@ describe(`#constructor`, () => { alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); }).not.toThrow(); }); @@ -48,13 +51,14 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const operation = 'create'; + const authorizationType = 'rule'; - alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation); + alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation, authorizationType); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_unscoped_authorization_failure", - "foo-user Unauthorized to create any alert types", + "foo-user Unauthorized to create rules for any rule types", Object { "operation": "create", "username": "foo-user", @@ -71,21 +75,24 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { const scopeType = ScopeType.Producer; const scope = 'myOtherApp'; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsAuthorizationFailure( username, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_failure", - "foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"", + "foo-user Unauthorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", + "authorizationType": "rule", "operation": "create", "scope": "myOtherApp", "scopeType": 1, @@ -105,21 +112,24 @@ describe(`#alertsAuthorizationFailure`, () => { const scopeType = ScopeType.Consumer; const scope = 'myApp'; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsAuthorizationFailure( username, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_failure", - "foo-user Unauthorized to create a \\"alert-type-id\\" alert for \\"myApp\\"", + "foo-user Unauthorized to create a \\"alert-type-id\\" rule for \\"myApp\\"", Object { "alertTypeId": "alert-type-id", + "authorizationType": "rule", "operation": "create", "scope": "myApp", "scopeType": 0, @@ -137,21 +147,24 @@ describe(`#alertsAuthorizationFailure`, () => { const scopeType = ScopeType.Producer; const scope = 'myOtherApp'; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsAuthorizationFailure( username, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_failure", - "foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"", + "foo-user Unauthorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", + "authorizationType": "rule", "operation": "create", "scope": "myOtherApp", "scopeType": 1, @@ -173,19 +186,22 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { ['other-alert-type-id', 'myOtherApp'], ]; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsBulkAuthorizationSuccess( username, authorizedEntries, scopeType, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_success", - "foo-user Authorized to create: \\"alert-type-id\\" alert for \\"myApp\\", \\"other-alert-type-id\\" alert for \\"myOtherApp\\"", + "foo-user Authorized to create: \\"alert-type-id\\" rules for \\"myApp\\", \\"other-alert-type-id\\" rules for \\"myOtherApp\\"", Object { + "authorizationType": "rule", "authorizedEntries": Array [ Array [ "alert-type-id", @@ -214,19 +230,22 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { ['other-alert-type-id', 'myOtherApp'], ]; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsBulkAuthorizationSuccess( username, authorizedEntries, scopeType, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_success", - "foo-user Authorized to create: \\"alert-type-id\\" alert by \\"myApp\\", \\"other-alert-type-id\\" alert by \\"myOtherApp\\"", + "foo-user Authorized to create: \\"alert-type-id\\" rules by \\"myApp\\", \\"other-alert-type-id\\" rules by \\"myOtherApp\\"", Object { + "authorizationType": "rule", "authorizedEntries": Array [ Array [ "alert-type-id", @@ -255,21 +274,24 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const scopeType = ScopeType.Consumer; const scope = 'myApp'; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsAuthorizationSuccess( username, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_success", - "foo-user Authorized to create a \\"alert-type-id\\" alert for \\"myApp\\"", + "foo-user Authorized to create a \\"alert-type-id\\" rule for \\"myApp\\"", Object { "alertTypeId": "alert-type-id", + "authorizationType": "rule", "operation": "create", "scope": "myApp", "scopeType": 0, @@ -287,21 +309,24 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const scopeType = ScopeType.Producer; const scope = 'myOtherApp'; const operation = 'create'; + const authorizationType = 'rule'; alertsAuditLogger.alertsAuthorizationSuccess( username, alertTypeId, scopeType, scope, - operation + operation, + authorizationType ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alerts_authorization_success", - "foo-user Authorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"", + "foo-user Authorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", + "authorizationType": "rule", "operation": "create", "scope": "myOtherApp", "scopeType": 1, diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.ts index 36312783ff689..ea5c74078df92 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.ts @@ -64,8 +64,12 @@ export class AlertsAuthorizationAuditLogger { return message; } - public alertsUnscopedAuthorizationFailure(username: string, operation: string): string { - const message = `Unauthorized to ${operation} any alert types`; + public alertsUnscopedAuthorizationFailure( + username: string, + operation: string, + authorizationType: string + ): string { + const message = `Unauthorized to ${operation} ${authorizationType}s for any rule types`; this.auditLogger.log('alerts_unscoped_authorization_failure', `${username} ${message}`, { username, operation, diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap index afa907fe09837..fbf56530196f9 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap @@ -12,6 +12,18 @@ exports[`#get alertType of true throws error 1`] = `"alertTypeId is required and exports[`#get alertType of undefined throws error 1`] = `"alertTypeId is required and must be a string"`; +exports[`#get alertingType of "" throws error 1`] = `"alertingType is required and must be a string"`; + +exports[`#get alertingType of {} throws error 1`] = `"alertingType is required and must be a string"`; + +exports[`#get alertingType of 1 throws error 1`] = `"alertingType is required and must be a string"`; + +exports[`#get alertingType of null throws error 1`] = `"alertingType is required and must be a string"`; + +exports[`#get alertingType of true throws error 1`] = `"alertingType is required and must be a string"`; + +exports[`#get alertingType of undefined throws error 1`] = `"alertingType is required and must be a string"`; + exports[`#get consumer of "" throws error 1`] = `"consumer is required and must be a string"`; exports[`#get consumer of {} throws error 1`] = `"consumer is required and must be a string"`; @@ -35,3 +47,15 @@ exports[`#get operation of null throws error 1`] = `"operation is required and m exports[`#get operation of true throws error 1`] = `"operation is required and must be a string"`; exports[`#get operation of undefined throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get ruleType of "" throws error 1`] = `"ruleTypeId is required and must be a string"`; + +exports[`#get ruleType of {} throws error 1`] = `"ruleTypeId is required and must be a string"`; + +exports[`#get ruleType of 1 throws error 1`] = `"ruleTypeId is required and must be a string"`; + +exports[`#get ruleType of null throws error 1`] = `"ruleTypeId is required and must be a string"`; + +exports[`#get ruleType of true throws error 1`] = `"ruleTypeId is required and must be a string"`; + +exports[`#get ruleType of undefined throws error 1`] = `"ruleTypeId is required and must be a string"`; diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts index 2383e4a7c583a..eff2a37eb9e25 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts @@ -10,11 +10,11 @@ import { AlertingActions } from './alerting'; const version = '1.0.0-zeta1'; describe('#get', () => { - [null, undefined, '', 1, true, {}].forEach((alertType: any) => { - test(`alertType of ${JSON.stringify(alertType)} throws error`, () => { + [null, undefined, '', 1, true, {}].forEach((ruleType: any) => { + test(`ruleType of ${JSON.stringify(ruleType)} throws error`, () => { const alertingActions = new AlertingActions(version); expect(() => - alertingActions.get(alertType, 'consumer', 'foo-action') + alertingActions.get(ruleType, 'consumer', 'alertingType', 'foo-action') ).toThrowErrorMatchingSnapshot(); }); }); @@ -23,7 +23,7 @@ describe('#get', () => { test(`operation of ${JSON.stringify(operation)} throws error`, () => { const alertingActions = new AlertingActions(version); expect(() => - alertingActions.get('foo-alertType', 'consumer', operation) + alertingActions.get('foo-ruleType', 'consumer', 'alertingType', operation) ).toThrowErrorMatchingSnapshot(); }); }); @@ -32,15 +32,24 @@ describe('#get', () => { test(`consumer of ${JSON.stringify(consumer)} throws error`, () => { const alertingActions = new AlertingActions(version); expect(() => - alertingActions.get('foo-alertType', consumer, 'operation') + alertingActions.get('foo-ruleType', consumer, 'alertingType', 'operation') ).toThrowErrorMatchingSnapshot(); }); }); - test('returns `alerting:${alertType}/${consumer}/${operation}`', () => { + [null, '', 1, true, undefined, {}].forEach((alertingType: any) => { + test(`alertingType of ${JSON.stringify(alertingType)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get('foo-ruleType', 'consumer', alertingType, 'operation') + ).toThrowErrorMatchingSnapshot(); + }); + }); + + test('returns `alerting:${ruleType}/${consumer}/${alertingType}/${operation}`', () => { const alertingActions = new AlertingActions(version); - expect(alertingActions.get('foo-alertType', 'consumer', 'bar-operation')).toBe( - 'alerting:1.0.0-zeta1:foo-alertType/consumer/bar-operation' + expect(alertingActions.get('foo-ruleType', 'consumer', 'alertingType', 'bar-operation')).toBe( + 'alerting:1.0.0-zeta1:foo-ruleType/consumer/alertingType/bar-operation' ); }); }); From be0034404b08e78bc8989e33dd24bf0046cd2327 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 11:08:44 -0400 Subject: [PATCH 009/186] Updating unit tests --- .../server/alerts_client/tests/create.test.ts | 21 +++++++++++++++--- .../server/alerts_client/tests/delete.test.ts | 14 ++++++++++-- .../alerts_client/tests/disable.test.ts | 14 ++++++++++-- .../server/alerts_client/tests/enable.test.ts | 14 ++++++++++-- .../server/alerts_client/tests/get.test.ts | 14 ++++++++++-- .../tests/get_alert_state.test.ts | 22 ++++++++++--------- .../alerts_client/tests/mute_all.test.ts | 14 ++++++++++-- .../alerts_client/tests/mute_instance.test.ts | 22 ++++++++++--------- .../alerts_client/tests/unmute_all.test.ts | 14 ++++++++++-- .../tests/unmute_instance.test.ts | 22 ++++++++++--------- .../server/alerts_client/tests/update.test.ts | 14 ++++++++++-- .../tests/update_api_key.test.ts | 22 ++++++++++--------- 12 files changed, 150 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index 6f493ced47371..299ee5ee24ad0 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -186,7 +186,12 @@ describe('create()', () => { await tryToExecuteOperation({ data }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'create', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to create this type of alert', async () => { @@ -203,7 +208,12 @@ describe('create()', () => { `[Error: Unauthorized to create a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'create', + ruleTypeId: 'myType', + }); }); }); @@ -330,7 +340,12 @@ describe('create()', () => { ], }); const result = await alertsClient.create({ data }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'bar', + operation: 'create', + ruleTypeId: '123', + }); expect(result).toMatchInlineSnapshot(` Object { "actions": Array [ diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 82aea8e5b3ba2..6248b50380df5 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -231,7 +231,12 @@ describe('delete()', () => { test('ensures user is authorised to delete this type of alert under the consumer', async () => { await alertsClient.delete({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'delete', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to delete this type of alert', async () => { @@ -243,7 +248,12 @@ describe('delete()', () => { `[Error: Unauthorized to delete a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'delete', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index 712a1c539d8d9..dda25b46116d4 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -99,7 +99,12 @@ describe('disable()', () => { test('ensures user is authorised to disable this type of alert under the consumer', async () => { await alertsClient.disable({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'disable', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to disable this type of alert', async () => { @@ -111,7 +116,12 @@ describe('disable()', () => { `[Error: Unauthorized to disable a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'disable', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 7b0d6d7b1f10b..25efa3d264644 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -137,7 +137,12 @@ describe('enable()', () => { test('ensures user is authorised to enable this type of alert under the consumer', async () => { await alertsClient.enable({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'enable', + ruleTypeId: 'myType', + }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); @@ -150,7 +155,12 @@ describe('enable()', () => { `[Error: Unauthorized to enable a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'enable', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index d25ed1da51577..a7f033c48496f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -182,7 +182,12 @@ describe('get()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.get({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'get', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to get this type of alert', async () => { @@ -195,7 +200,12 @@ describe('get()', () => { `[Error: Unauthorized to get a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'get', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts index 0cd7dcf14c7c3..4e04f44e89055 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts @@ -210,11 +210,12 @@ describe('getAlertState()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.getAlertState({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'getAlertState' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'getAlertState', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to getAlertState this type of alert', async () => { @@ -230,11 +231,12 @@ describe('getAlertState()', () => { `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'getAlertState' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'getAlertState', + ruleTypeId: 'myType', + }); }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 227f57af6a53e..6f175a05c62c0 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -126,7 +126,12 @@ describe('muteAll()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.muteAll({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'muteAll', + ruleTypeId: 'myType', + }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); @@ -140,7 +145,12 @@ describe('muteAll()', () => { `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'muteAll', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index a97cc115a3baf..a67513e266a96 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -159,11 +159,12 @@ describe('muteInstance()', () => { await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'muteInstance', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to muteInstance this type of alert', async () => { @@ -178,11 +179,12 @@ describe('muteInstance()', () => { `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'muteInstance', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index c3c1b609e3da0..5919c4487ebaf 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -126,7 +126,12 @@ describe('unmuteAll()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.unmuteAll({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'unmuteAll', + ruleTypeId: 'myType', + }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); @@ -140,7 +145,12 @@ describe('unmuteAll()', () => { `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'unmuteAll', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index 03736040e7085..b73d1801e7364 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -157,11 +157,12 @@ describe('unmuteInstance()', () => { await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'unmuteInstance', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to unmuteInstance this type of alert', async () => { @@ -176,11 +177,12 @@ describe('unmuteInstance()', () => { `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'unmuteInstance', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index cdbfbbac9f9a1..712ad94cc3104 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -1351,7 +1351,12 @@ describe('update()', () => { }, }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'update', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to update this type of alert', async () => { @@ -1378,7 +1383,12 @@ describe('update()', () => { `[Error: Unauthorized to update a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'update', + ruleTypeId: 'myType', + }); }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 18bae8d34a8da..16991be48eab1 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -268,11 +268,12 @@ describe('updateApiKey()', () => { await alertsClient.updateApiKey({ id: '1' }); expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'updateApiKey', + ruleTypeId: 'myType', + }); }); test('throws when user is not authorised to updateApiKey this type of alert', async () => { @@ -284,11 +285,12 @@ describe('updateApiKey()', () => { `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + authorizationType: 'rule', + consumer: 'myApp', + operation: 'updateApiKey', + ruleTypeId: 'myType', + }); }); }); From 721576a41a267c2220545a3c9c753dc23efbf788 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 11:57:58 -0400 Subject: [PATCH 010/186] Passing field names into authorization query builder. Adding kql/es dsl option --- .../server/alerts_client/alerts_client.ts | 8 +- .../alerts_authorization.test.ts.snap | 316 ------------ .../alerts_authorization_kuery.test.ts.snap | 448 ------------------ .../alerts_authorization.test.ts | 55 ++- .../authorization/alerts_authorization.ts | 8 +- .../alerts_authorization_kuery.test.ts | 72 +-- .../alerts_authorization_kuery.ts | 43 +- 7 files changed, 119 insertions(+), 831 deletions(-) delete mode 100644 x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization.test.ts.snap delete mode 100644 x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization_kuery.test.ts.snap diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index c1ef670ca3bc2..0830563f411f0 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -459,7 +459,8 @@ export class AlertsClient { let authorizationTuple; try { authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule + AlertingAuthorizationTypes.Rule, + { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' } ); } catch (error) { this.auditLogger?.log( @@ -544,7 +545,10 @@ export class AlertsClient { const { filter: authorizationFilter, logSuccessfulAuthorization, - } = await this.authorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule); + } = await this.authorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + ruleTypeId: 'alert.attributes.alertTypeId', + consumer: 'alert.attributes.consumer', + }); const filter = options.filter ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` : `alert.attributes.executionStatus.status:(${status})`; diff --git a/x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization.test.ts.snap b/x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization.test.ts.snap deleted file mode 100644 index f9a28dc3eb119..0000000000000 --- a/x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization.test.ts.snap +++ /dev/null @@ -1,316 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AlertsAuthorization getFindAuthorizationFilter creates a filter based on the privileged types 1`] = ` -Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "myAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myAppWithSubFeature", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "myOtherAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myAppWithSubFeature", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "mySecondAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myAppWithSubFeature", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - ], - "function": "or", - "type": "function", -} -`; diff --git a/x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization_kuery.test.ts.snap b/x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization_kuery.test.ts.snap deleted file mode 100644 index de01a7b27ef05..0000000000000 --- a/x-pack/plugins/alerting/server/authorization/__snapshots__/alerts_authorization_kuery.test.ts.snap +++ /dev/null @@ -1,448 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`asFiltersByAlertTypeAndConsumer constructs filter for multiple alert types across authorized consumer 1`] = ` -Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "myAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myAppWithSubFeature", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "myOtherAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myAppWithSubFeature", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "mySecondAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myAppWithSubFeature", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - ], - "function": "or", - "type": "function", -} -`; - -exports[`asFiltersByAlertTypeAndConsumer constructs filter for single alert type with multiple authorized consumer 1`] = ` -Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "myAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "alerts", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myOtherApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "or", - "type": "function", - }, - ], - "function": "and", - "type": "function", -} -`; - -exports[`asFiltersByAlertTypeAndConsumer constructs filter for single alert type with single authorized consumer 1`] = ` -Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.alertTypeId", - }, - Object { - "type": "literal", - "value": "myAppAlertType", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "alert.attributes.consumer", - }, - Object { - "type": "literal", - "value": "myApp", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", -} -`; diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts index 9fb3d8468f5a7..e1167d197a418 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts @@ -24,6 +24,7 @@ import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_log import uuid from 'uuid'; import { RecoveredActionGroup } from '../../common'; import { RegistryAlertType } from '../alert_type_registry'; +import { esKuery } from '../../../../../src/plugins/data/server'; const alertTypeRegistry = alertTypeRegistryMock.create(); const features: jest.Mocked = featuresPluginMock.createStart(); @@ -685,7 +686,10 @@ describe('AlertsAuthorization', () => { const { filter, ensureRuleTypeIsAuthorized, - } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule); + } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }); expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); @@ -704,7 +708,11 @@ describe('AlertsAuthorization', () => { }); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule + AlertingAuthorizationTypes.Rule, + { + ruleTypeId: 'ruleId', + consumer: 'consumer', + } ); ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule'); @@ -737,20 +745,18 @@ describe('AlertsAuthorization', () => { }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - // TODO: once issue https://github.com/elastic/kibana/issues/89473 is - // resolved, we can start using this code again, instead of toMatchSnapshot(): - // - // expect((await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule)).filter).toEqual( - // esKuery.fromKueryExpression( - // `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` - // ) - // ); - - // This code is the replacement code for above - // expect( - // (await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule)) - // .filter - // ).toMatchSnapshot(); + expect( + ( + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }) + ).filter + ).toEqual( + esKuery.fromKueryExpression( + `((path.to.rule.id:myAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)))` + ) + ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); }); @@ -804,7 +810,11 @@ describe('AlertsAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Alert + AlertingAuthorizationTypes.Alert, + { + ruleTypeId: 'ruleId', + consumer: 'consumer', + } ); expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'alert'); @@ -875,7 +885,11 @@ describe('AlertsAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule + AlertingAuthorizationTypes.Rule, + { + ruleTypeId: 'ruleId', + consumer: 'consumer', + } ); expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); @@ -949,7 +963,10 @@ describe('AlertsAuthorization', () => { const { ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, - } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule); + } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }); expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); ensureRuleTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp', 'rule'); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index daf37f21c2b8d..74e607d2fbb7a 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -8,14 +8,13 @@ import Boom from '@hapi/boom'; import { map, mapValues, fromPairs, has, get } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; -import { asFiltersByRuleTypeAndConsumer } from './alerts_authorization_kuery'; +import { asKqlFiltersByRuleTypeAndConsumer, FilterFieldNames } from './alerts_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; export enum AlertingAuthorizationTypes { @@ -256,7 +255,8 @@ export class AlertsAuthorization { } public async getFindAuthorizationFilter( - authorizationType: AlertingAuthorizationTypes + authorizationType: AlertingAuthorizationTypes, + filterFieldNames: FilterFieldNames ): Promise<{ filter?: KueryNode; ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; @@ -286,7 +286,7 @@ export class AlertsAuthorization { const authorizedEntries: Map> = new Map(); return { - filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes), + filter: asKqlFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterFieldNames), ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts index a09c10f659837..0ba65cf118889 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts @@ -7,14 +7,15 @@ import { RecoveredActionGroup } from '../../common'; import { - asFiltersByAlertTypeAndConsumer, + asKqlFiltersByRuleTypeAndConsumer, ensureFieldIsSafeForQuery, } from './alerts_authorization_kuery'; +import { esKuery } from '../../../../../src/plugins/data/server'; -describe('asFiltersByAlertTypeAndConsumer', () => { - test('constructs filter for single alert type with single authorized consumer', async () => { +describe('asKqlFiltersByRuleTypeAndConsumer', () => { + test('constructs KQL filter for single rule type with single authorized consumer', async () => { expect( - asFiltersByAlertTypeAndConsumer( + asKqlFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -29,21 +30,20 @@ describe('asFiltersByAlertTypeAndConsumer', () => { }, enabledInLicense: true, }, - ]) + ]), + { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + } ) - ).toMatchSnapshot(); - // TODO: once issue https://github.com/elastic/kibana/issues/89473 is - // resolved, we can start using this code again instead of toMatchSnapshot() - // ).toEqual( - // esKuery.fromKueryExpression( - // `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(myApp)))` - // ) - // ); + ).toEqual( + esKuery.fromKueryExpression(`((path.to.rule.id:myAppAlertType and consumer-field:(myApp)))`) + ); }); - test('constructs filter for single alert type with multiple authorized consumer', async () => { + test('constructs KQL filter for single rule type with multiple authorized consumers', async () => { expect( - asFiltersByAlertTypeAndConsumer( + asKqlFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -60,21 +60,22 @@ describe('asFiltersByAlertTypeAndConsumer', () => { }, enabledInLicense: true, }, - ]) + ]), + { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + } + ) + ).toEqual( + esKuery.fromKueryExpression( + `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp)))` ) - ).toMatchSnapshot(); - // TODO: once issue https://github.com/elastic/kibana/issues/89473 is - // resolved, we can start using this code again, instead of toMatchSnapshot(): - // ).toEqual( - // esKuery.fromKueryExpression( - // `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp)))` - // ) - // ); + ); }); - test('constructs filter for multiple alert types across authorized consumer', async () => { + test('constructs KQL filter for multiple rule types across authorized consumer', async () => { expect( - asFiltersByAlertTypeAndConsumer( + asKqlFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -124,16 +125,17 @@ describe('asFiltersByAlertTypeAndConsumer', () => { }, enabledInLicense: true, }, - ]) + ]), + { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + } ) - ).toMatchSnapshot(); - // TODO: once issue https://github.com/elastic/kibana/issues/89473 is - // resolved, we can start using this code again, instead of toMatchSnapshot(): - // ).toEqual( - // esKuery.fromKueryExpression( - // `((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` - // ) - // ); + ).toEqual( + esKuery.fromKueryExpression( + `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + ) + ); }); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts index d017361456752..9fc5873a58874 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts @@ -10,20 +10,31 @@ import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { KueryNode } from '../../../../../src/plugins/data/server'; import { RegistryAlertTypeWithAuth } from './alerts_authorization'; +enum FilterType { + KQL = 'kql', + ESDSL = 'dsl', +} + +export interface FilterFieldNames { + ruleTypeId: string; + consumer: string; +} // pass in the field name instead of hardcoding `alert.attributes.alertTypeId` and `alertTypeId` -export function asFiltersByRuleTypeAndConsumer( - alertTypes: Set +function asFiltersByRuleTypeAndConsumer( + ruleTypes: Set, + filterFieldNames: FilterFieldNames, + filterType: FilterType ): KueryNode { - return nodeBuilder.or( - Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { - ensureFieldIsSafeForQuery('alertTypeId', id); + const kueryNode = nodeBuilder.or( + Array.from(ruleTypes).reduce((filters, { id, authorizedConsumers }) => { + ensureFieldIsSafeForQuery('ruleTypeId', id); filters.push( nodeBuilder.and([ - nodeBuilder.is(`alert.attributes.alertTypeId`, id), + nodeBuilder.is(filterFieldNames.ruleTypeId, id), nodeBuilder.or( Object.keys(authorizedConsumers).map((consumer) => { ensureFieldIsSafeForQuery('consumer', consumer); - return nodeBuilder.is(`alert.attributes.consumer`, consumer); + return nodeBuilder.is(filterFieldNames.consumer, consumer); }) ), ]) @@ -31,6 +42,24 @@ export function asFiltersByRuleTypeAndConsumer( return filters; }, []) ); + + // console.log(`KUERY ${JSON.stringify(kueryNode)}`); + + return kueryNode; +} + +export function asKqlFiltersByRuleTypeAndConsumer( + ruleTypes: Set, + filterFieldNames: FilterFieldNames +): KueryNode { + return asFiltersByRuleTypeAndConsumer(ruleTypes, filterFieldNames, FilterType.KQL); +} + +export function asEsDslFiltersByRuleTypeAndConsumer( + ruleTypes: Set, + filterFieldNames: FilterFieldNames +): KueryNode { + return asFiltersByRuleTypeAndConsumer(ruleTypes, filterFieldNames, FilterType.ESDSL); } export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { From e1cd87293a1df767894ce78a2214a3d0a0e1e276 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 13:12:13 -0400 Subject: [PATCH 011/186] Converting to es query if requested --- .../server/alerts_client/alerts_client.ts | 21 +- .../authorization/alerts_authorization.ts | 12 +- .../alerts_authorization_kuery.test.ts | 318 ++++++++++++++++++ .../alerts_authorization_kuery.ts | 38 ++- 4 files changed, 371 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 0830563f411f0..e446d5ccf2043 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -51,6 +51,7 @@ import { WriteOperations, ReadOperations, AlertingAuthorizationTypes, + AlertingAuthorizationFilterType, } from '../authorization'; import { IEventLogClient } from '../../../../plugins/event_log/server'; import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; @@ -62,7 +63,7 @@ import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; import { alertAuditEvent, AlertAuditAction } from './audit_events'; -import { nodeBuilder } from '../../../../../src/plugins/data/common'; +import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { @@ -460,6 +461,7 @@ export class AlertsClient { try { authorizationTuple = await this.authorization.getFindAuthorizationFilter( AlertingAuthorizationTypes.Rule, + AlertingAuthorizationFilterType.KQL, { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' } ); } catch (error) { @@ -545,10 +547,14 @@ export class AlertsClient { const { filter: authorizationFilter, logSuccessfulAuthorization, - } = await this.authorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { - ruleTypeId: 'alert.attributes.alertTypeId', - consumer: 'alert.attributes.consumer', - }); + } = await this.authorization.getFindAuthorizationFilter( + AlertingAuthorizationTypes.Rule, + AlertingAuthorizationFilterType.KQL, + { + ruleTypeId: 'alert.attributes.alertTypeId', + consumer: 'alert.attributes.consumer', + } + ); const filter = options.filter ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` : `alert.attributes.executionStatus.status:(${status})`; @@ -556,7 +562,10 @@ export class AlertsClient { ...options, filter: (authorizationFilter && filter - ? nodeBuilder.and([esKuery.fromKueryExpression(filter), authorizationFilter]) + ? nodeBuilder.and([ + esKuery.fromKueryExpression(filter), + authorizationFilter as KueryNode, + ]) : authorizationFilter) ?? filter, page: 1, perPage: 0, diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 74e607d2fbb7a..2b329608776e4 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -14,8 +14,13 @@ import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; -import { asKqlFiltersByRuleTypeAndConsumer, FilterFieldNames } from './alerts_authorization_kuery'; +import { + asFiltersByRuleTypeAndConsumer, + FilterFieldNames, + AlertingAuthorizationFilterType, +} from './alerts_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; export enum AlertingAuthorizationTypes { Rule = 'rule', @@ -256,9 +261,10 @@ export class AlertsAuthorization { public async getFindAuthorizationFilter( authorizationType: AlertingAuthorizationTypes, + filterType: AlertingAuthorizationFilterType, filterFieldNames: FilterFieldNames ): Promise<{ - filter?: KueryNode; + filter?: KueryNode | JsonObject; ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; logSuccessfulAuthorization: () => void; }> { @@ -286,7 +292,7 @@ export class AlertsAuthorization { const authorizedEntries: Map> = new Map(); return { - filter: asKqlFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterFieldNames), + filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterFieldNames, filterType), ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts index 0ba65cf118889..1b4ddffc2eef2 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts @@ -8,6 +8,7 @@ import { RecoveredActionGroup } from '../../common'; import { asKqlFiltersByRuleTypeAndConsumer, + asEsDslFiltersByRuleTypeAndConsumer, ensureFieldIsSafeForQuery, } from './alerts_authorization_kuery'; import { esKuery } from '../../../../../src/plugins/data/server'; @@ -139,6 +140,323 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { }); }); +describe('asEsDslFiltersByRuleTypeAndConsumer', () => { + test('constructs ES DSL filter for single rule type with single authorized consumer', async () => { + expect( + asEsDslFiltersByRuleTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + minimumLicenseRequired: 'basic', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + enabledInLicense: true, + }, + ]), + { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + } + ) + ).toEqual({ + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'path.to.rule.id': 'myAppAlertType', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'consumer-field': 'myApp', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }); + }); + + test('constructs ES DSL filter for single rule type with multiple authorized consumers', async () => { + expect( + asEsDslFiltersByRuleTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + }, + enabledInLicense: true, + }, + ]), + { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + } + ) + ).toEqual({ + bool: { + filter: [ + { + bool: { + should: [{ match: { 'path.to.rule.id': 'myAppAlertType' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [{ match: { 'consumer-field': 'alerts' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myOtherApp' } }], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }); + }); + + test('constructs ES DSL filter for multiple rule types across authorized consumer', async () => { + expect( + asEsDslFiltersByRuleTypeAndConsumer( + new Set([ + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: RecoveredActionGroup, + id: 'mySecondAppAlertType', + name: 'mySecondAppAlertType', + producer: 'myApp', + authorizedConsumers: { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + myAppWithSubFeature: { read: true, all: true }, + }, + enabledInLicense: true, + }, + ]), + { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + } + ) + ).toEqual({ + bool: { + should: [ + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'path.to.rule.id': 'myAppAlertType' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [{ match: { 'consumer-field': 'alerts' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myOtherApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myAppWithSubFeature' } }], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'path.to.rule.id': 'myOtherAppAlertType' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [{ match: { 'consumer-field': 'alerts' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myOtherApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myAppWithSubFeature' } }], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'path.to.rule.id': 'mySecondAppAlertType' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [{ match: { 'consumer-field': 'alerts' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myOtherApp' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'consumer-field': 'myAppWithSubFeature' } }], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }); + }); +}); + describe('ensureFieldIsSafeForQuery', () => { test('throws if field contains character that isnt safe in a KQL query', () => { expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts index 9fc5873a58874..4d1ad33a13e96 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts @@ -6,11 +6,13 @@ */ import { remove } from 'lodash'; -import { nodeBuilder } from '../../../../../src/plugins/data/common'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; +import { nodeBuilder, EsQueryConfig } from '../../../../../src/plugins/data/common'; +import { toElasticsearchQuery } from '../../../../../src/plugins/data/common/es_query'; import { KueryNode } from '../../../../../src/plugins/data/server'; import { RegistryAlertTypeWithAuth } from './alerts_authorization'; -enum FilterType { +export enum AlertingAuthorizationFilterType { KQL = 'kql', ESDSL = 'dsl', } @@ -19,12 +21,20 @@ export interface FilterFieldNames { ruleTypeId: string; consumer: string; } + +const esQueryConfig: EsQueryConfig = { + allowLeadingWildcards: true, + dateFormatTZ: 'Zulu', + ignoreFilterIfFieldNotInIndex: false, + queryStringOptions: { analyze_wildcard: true }, +}; + // pass in the field name instead of hardcoding `alert.attributes.alertTypeId` and `alertTypeId` -function asFiltersByRuleTypeAndConsumer( +export function asFiltersByRuleTypeAndConsumer( ruleTypes: Set, filterFieldNames: FilterFieldNames, - filterType: FilterType -): KueryNode { + filterType: AlertingAuthorizationFilterType +): KueryNode | JsonObject { const kueryNode = nodeBuilder.or( Array.from(ruleTypes).reduce((filters, { id, authorizedConsumers }) => { ensureFieldIsSafeForQuery('ruleTypeId', id); @@ -43,7 +53,9 @@ function asFiltersByRuleTypeAndConsumer( }, []) ); - // console.log(`KUERY ${JSON.stringify(kueryNode)}`); + if (filterType === AlertingAuthorizationFilterType.ESDSL) { + return toElasticsearchQuery(kueryNode, undefined, esQueryConfig); + } return kueryNode; } @@ -52,14 +64,22 @@ export function asKqlFiltersByRuleTypeAndConsumer( ruleTypes: Set, filterFieldNames: FilterFieldNames ): KueryNode { - return asFiltersByRuleTypeAndConsumer(ruleTypes, filterFieldNames, FilterType.KQL); + return asFiltersByRuleTypeAndConsumer( + ruleTypes, + filterFieldNames, + AlertingAuthorizationFilterType.KQL + ) as KueryNode; } export function asEsDslFiltersByRuleTypeAndConsumer( ruleTypes: Set, filterFieldNames: FilterFieldNames -): KueryNode { - return asFiltersByRuleTypeAndConsumer(ruleTypes, filterFieldNames, FilterType.ESDSL); +): JsonObject { + return asFiltersByRuleTypeAndConsumer( + ruleTypes, + filterFieldNames, + AlertingAuthorizationFilterType.ESDSL + ) as JsonObject; } export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { From 2007a560bcf47987e8d12257ed83daab992e4a28 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 15:34:27 -0400 Subject: [PATCH 012/186] Fixing functional tests --- .../common/lib/alert_utils.ts | 4 ++-- .../security_and_spaces/tests/alerting/find.ts | 12 ++++++------ .../tests/alerting/rbac_legacy.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 540f8f4d1cad9..ea16351b49543 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -308,7 +308,7 @@ export function getConsumerUnauthorizedErrorMessage( alertType: string, consumer: string ) { - return `Unauthorized to ${operation} a "${alertType}" alert for "${consumer}"`; + return `Unauthorized to ${operation} a "${alertType}" rule for "${consumer}"`; } export function getProducerUnauthorizedErrorMessage( @@ -316,7 +316,7 @@ export function getProducerUnauthorizedErrorMessage( alertType: string, producer: string ) { - return `Unauthorized to ${operation} a "${alertType}" alert by "${producer}"`; + return `Unauthorized to ${operation} a "${alertType}" rule by "${producer}"`; } function getDefaultAlwaysFiringAlertData(reference: string, actionId: string) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index dda5970904f8d..3454ef5c94d9f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -47,7 +47,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find any alert types`, + message: `Unauthorized to find rules for any rule types`, statusCode: 403, }); break; @@ -143,7 +143,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find any alert types`, + message: `Unauthorized to find rules for any rule types`, statusCode: 403, }); break; @@ -239,7 +239,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find any alert types`, + message: `Unauthorized to find rules for any rule types`, statusCode: 403, }); break; @@ -333,7 +333,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find any alert types`, + message: `Unauthorized to find rules for any rule types`, statusCode: 403, }); break; @@ -410,7 +410,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find any alert types`, + message: `Unauthorized to find rules for any rule types`, statusCode: 403, }); break; @@ -470,7 +470,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find any alert types`, + message: `Unauthorized to find rules for any rule types`, statusCode: 403, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 53ea2b845af1f..539e7eb059107 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -112,7 +112,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(failedUpdateKeyDueToAlertsPrivilegesResponse.body).to.eql({ error: 'Forbidden', message: - 'Unauthorized to updateApiKey a "test.always-firing" alert for "alertsFixture"', + 'Unauthorized to updateApiKey a "test.always-firing" rule for "alertsFixture"', statusCode: 403, }); break; From 857b0deaea5c5af18f2b8faa853b2c82498c5bd2 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 3 May 2021 16:10:54 -0400 Subject: [PATCH 013/186] Removing ability to specify feature privilege name in constructor --- .../alerting_authorization_client_factory.ts | 7 +----- .../alerting/server/alerts_client_factory.ts | 4 +--- .../alerts_authorization.test.ts | 23 ------------------- .../authorization/alerts_authorization.ts | 19 ++++++--------- x-pack/plugins/alerting/server/plugin.ts | 9 +++----- 5 files changed, 12 insertions(+), 50 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts index cd1891e7a0f15..8322651672cd2 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -42,11 +42,7 @@ export class AlertingAuthorizationClientFactory { this.features = options.features; } - public create( - request: KibanaRequest, - privilegeName: string, - exemptConsumerIds: string[] = [] - ): AlertsAuthorization { + public create(request: KibanaRequest, exemptConsumerIds: string[] = []): AlertsAuthorization { const { securityPluginSetup, securityPluginStart, features } = this; return new AlertsAuthorization({ authorization: securityPluginStart?.authz, @@ -57,7 +53,6 @@ export class AlertingAuthorizationClientFactory { auditLogger: new AlertsAuthorizationAuditLogger( securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) ), - privilegeName, exemptConsumerIds, }); } diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 36eefc0ccaafd..32cb9462c093e 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -85,9 +85,7 @@ export class AlertsClientFactory { excludedWrappers: ['security'], includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), - authorization: this.authorization!.create(request, FEATURE_PRIVILEGE_NAME, [ - ALERTS_FEATURE_ID, - ]), + authorization: this.authorization!.create(request, [ALERTS_FEATURE_ID]), actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts index e1167d197a418..e5f74b8aac288 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts @@ -35,7 +35,6 @@ const realAuditLogger = new AlertsAuthorizationAuditLogger(); const getSpace = jest.fn(); -const privilegeName = 'alerting'; const exemptConsumerIds: string[] = []; const mockAuthorizationAction = ( @@ -218,7 +217,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -234,7 +232,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -258,7 +255,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -285,7 +281,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -342,7 +337,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds: ['exemptConsumer'], }); @@ -411,7 +405,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -471,7 +464,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -530,7 +522,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -589,7 +580,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -679,7 +669,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -703,7 +692,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); @@ -740,7 +728,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -804,7 +791,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -879,7 +865,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -955,7 +940,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1032,7 +1016,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1112,7 +1095,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1244,7 +1226,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1348,7 +1329,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds: ['exemptConsumerA'], }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1443,7 +1423,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds: ['exemptConsumerA'], }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1548,7 +1527,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1656,7 +1634,6 @@ describe('AlertsAuthorization', () => { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 2b329608776e4..ff341bf9516ae 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -70,7 +70,6 @@ export interface ConstructorOptions { features: FeaturesPluginStart; getSpace: (request: KibanaRequest) => Promise; auditLogger: AlertsAuthorizationAuditLogger; - privilegeName: string; exemptConsumerIds: string[]; authorization?: SecurityPluginSetup['authz']; } @@ -82,7 +81,6 @@ export class AlertsAuthorization { private readonly auditLogger: AlertsAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; - private readonly privilegeName: string; private readonly exemptConsumerIds: string[]; constructor({ @@ -92,14 +90,12 @@ export class AlertsAuthorization { features, auditLogger, getSpace, - privilegeName, exemptConsumerIds, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; - this.privilegeName = privilegeName; // List of consumer ids that are exempt from privilege check. This should be used sparingly. // An example of this is the Rules Management `consumer` as we don't want to have to @@ -113,14 +109,13 @@ export class AlertsAuthorization { new Set( features .getKibanaFeatures() - .filter((feature) => { - // ignore features which are disabled in the user's space - return ( - !disabledFeatures.has(feature.id) && - // ignore features which don't grant privileges to the specified privilege - (get(feature, this.privilegeName, undefined)?.length ?? 0 > 0) - ); - }) + .filter( + ({ id, alerting }) => + // ignore features which are disabled in the user's space + !disabledFeatures.has(id) && + // ignore features which don't grant privileges to alerting + (alerting?.length ?? 0 > 0) + ) .map((feature) => feature.id) ) ) diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index f6b7397eab2c9..e8a2b9dbece9b 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -105,10 +105,7 @@ export interface PluginSetupContract { export interface PluginStartContract { listTypes: AlertTypeRegistry['list']; getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf; - getAlertingAuthorizationWithRequest( - request: KibanaRequest, - privilegeName: string - ): PublicMethodsOf; + getAlertingAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; getFrameworkHealth: () => Promise; } @@ -343,8 +340,8 @@ export class AlertingPlugin { return alertsClientFactory!.create(request, core.savedObjects); }; - const getAlertingAuthorizationWithRequest = (request: KibanaRequest, privilegeName: string) => { - return alertingAuthorizationClientFactory!.create(request, privilegeName); + const getAlertingAuthorizationWithRequest = (request: KibanaRequest) => { + return alertingAuthorizationClientFactory!.create(request); }; taskRunnerFactory.initialize({ From 7ef5bcd259d3e2015d09833f5982b6dbaca40b83 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 4 May 2021 10:38:05 -0400 Subject: [PATCH 014/186] Fixing some types and tests --- .../server/alerts_client/alerts_client.ts | 5 +- .../alerting/server/alerts_client_factory.ts | 2 - .../authorization/alerts_authorization.ts | 2 +- x-pack/plugins/alerting/server/mocks.ts | 1 + x-pack/plugins/alerting/server/plugin.test.ts | 52 +++++++++++++++++++ .../__snapshots__/alerting.test.ts.snap | 12 ----- 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index e446d5ccf2043..04ed9f49b7142 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -489,7 +489,10 @@ export class AlertsClient { sortField: mapSortField(options.sortField), filter: (authorizationFilter && options.filter - ? nodeBuilder.and([esKuery.fromKueryExpression(options.filter), authorizationFilter]) + ? nodeBuilder.and([ + esKuery.fromKueryExpression(options.filter), + authorizationFilter as KueryNode, + ]) : authorizationFilter) ?? options.filter, fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, type: 'alert', diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 32cb9462c093e..666790200141b 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -20,8 +20,6 @@ import { TaskManagerStartContract } from '../../task_manager/server'; import { IEventLogClientService } from '../../../plugins/event_log/server'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; import { ALERTS_FEATURE_ID } from '../common'; - -const FEATURE_PRIVILEGE_NAME = 'alerting'; export interface AlertsClientFactoryOpts { logger: Logger; taskManager: TaskManagerStartContract; diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index ff341bf9516ae..0be28b5ca585e 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { map, mapValues, fromPairs, has, get } from 'lodash'; +import { map, mapValues, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index df9a3c5ddf169..b87e6c8ca8f7c 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -26,6 +26,7 @@ const createSetupMock = () => { const createStartMock = () => { const mock: jest.Mocked = { listTypes: jest.fn(), + getAlertingAuthorizationWithRequest: jest.fn(), getAlertsClientWithRequest: jest.fn().mockResolvedValue(alertsClientMock.create()), getFrameworkHealth: jest.fn(), }; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 3e3ca4854b124..ec4b7095d67f7 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -201,6 +201,58 @@ describe('Alerting Plugin', () => { startContract.getAlertsClientWithRequest(fakeRequest); }); }); + + test(`exposes getAlertingAuthorizationWithRequest()`, async () => { + const context = coreMock.createPluginInitializerContext({ + healthCheck: { + interval: '5m', + }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '1h', + }, + }); + const plugin = new AlertingPlugin(context); + + const encryptedSavedObjectsSetup = { + ...encryptedSavedObjectsMock.createSetup(), + canEncrypt: true, + }; + plugin.setup(coreMock.createSetup(), { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + actions: actionsMock.createSetup(), + statusService: statusServiceMock.createSetupContract(), + }); + + const startContract = plugin.start(coreMock.createStart(), { + actions: actionsMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + features: mockFeatures(), + licensing: licensingMock.createStart(), + eventLog: eventLogMock.createStart(), + taskManager: taskManagerMock.createStart(), + }); + + const fakeRequest = ({ + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: jest.fn(), + } as unknown) as KibanaRequest; + startContract.getAlertingAuthorizationWithRequest(fakeRequest); + }); }); }); diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap index fbf56530196f9..9739af7bcde55 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap @@ -1,17 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get alertType of "" throws error 1`] = `"alertTypeId is required and must be a string"`; - -exports[`#get alertType of {} throws error 1`] = `"alertTypeId is required and must be a string"`; - -exports[`#get alertType of 1 throws error 1`] = `"alertTypeId is required and must be a string"`; - -exports[`#get alertType of null throws error 1`] = `"alertTypeId is required and must be a string"`; - -exports[`#get alertType of true throws error 1`] = `"alertTypeId is required and must be a string"`; - -exports[`#get alertType of undefined throws error 1`] = `"alertTypeId is required and must be a string"`; - exports[`#get alertingType of "" throws error 1`] = `"alertingType is required and must be a string"`; exports[`#get alertingType of {} throws error 1`] = `"alertingType is required and must be a string"`; From 8359289286478b04faf8ea96dbcd0c816b373e86 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 4 May 2021 10:39:47 -0400 Subject: [PATCH 015/186] Consolidating alerting authorization kuery filter options --- .../server/alerts_client/alerts_client.ts | 14 ++--- .../authorization/alerts_authorization.ts | 8 +-- .../alerts_authorization_kuery.test.ts | 58 ++++++++++++------- .../alerts_authorization_kuery.ts | 39 ++++--------- 4 files changed, 58 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 04ed9f49b7142..91d9001c1eb08 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -52,6 +52,7 @@ import { ReadOperations, AlertingAuthorizationTypes, AlertingAuthorizationFilterType, + AlertingAuthorizationFilterOpts, } from '../authorization'; import { IEventLogClient } from '../../../../plugins/event_log/server'; import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; @@ -181,6 +182,10 @@ export interface GetAlertInstanceSummaryParams { dateStart?: string; } +const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' }, +}; export class AlertsClient { private readonly logger: Logger; private readonly getUserName: () => Promise; @@ -461,8 +466,7 @@ export class AlertsClient { try { authorizationTuple = await this.authorization.getFindAuthorizationFilter( AlertingAuthorizationTypes.Rule, - AlertingAuthorizationFilterType.KQL, - { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' } + alertingAuthorizationFilterOpts ); } catch (error) { this.auditLogger?.log( @@ -552,11 +556,7 @@ export class AlertsClient { logSuccessfulAuthorization, } = await this.authorization.getFindAuthorizationFilter( AlertingAuthorizationTypes.Rule, - AlertingAuthorizationFilterType.KQL, - { - ruleTypeId: 'alert.attributes.alertTypeId', - consumer: 'alert.attributes.consumer', - } + alertingAuthorizationFilterOpts ); const filter = options.filter ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 0be28b5ca585e..fd3e64fd23020 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -16,8 +16,7 @@ import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; import { asFiltersByRuleTypeAndConsumer, - FilterFieldNames, - AlertingAuthorizationFilterType, + AlertingAuthorizationFilterOpts, } from './alerts_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; @@ -256,8 +255,7 @@ export class AlertsAuthorization { public async getFindAuthorizationFilter( authorizationType: AlertingAuthorizationTypes, - filterType: AlertingAuthorizationFilterType, - filterFieldNames: FilterFieldNames + filterOpts: AlertingAuthorizationFilterOpts ): Promise<{ filter?: KueryNode | JsonObject; ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; @@ -287,7 +285,7 @@ export class AlertsAuthorization { const authorizedEntries: Map> = new Map(); return { - filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterFieldNames, filterType), + filter: asFiltersByRuleTypeAndConsumer(authorizedRuleTypes, filterOpts), ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts index 1b4ddffc2eef2..a3fedfcf9b8f5 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts @@ -7,8 +7,8 @@ import { RecoveredActionGroup } from '../../common'; import { - asKqlFiltersByRuleTypeAndConsumer, - asEsDslFiltersByRuleTypeAndConsumer, + AlertingAuthorizationFilterType, + asFiltersByRuleTypeAndConsumer, ensureFieldIsSafeForQuery, } from './alerts_authorization_kuery'; import { esKuery } from '../../../../../src/plugins/data/server'; @@ -16,7 +16,7 @@ import { esKuery } from '../../../../../src/plugins/data/server'; describe('asKqlFiltersByRuleTypeAndConsumer', () => { test('constructs KQL filter for single rule type with single authorized consumer', async () => { expect( - asKqlFiltersByRuleTypeAndConsumer( + asFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -33,8 +33,11 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { }, ]), { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, } ) ).toEqual( @@ -44,7 +47,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { test('constructs KQL filter for single rule type with multiple authorized consumers', async () => { expect( - asKqlFiltersByRuleTypeAndConsumer( + asFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -63,8 +66,11 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { }, ]), { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, } ) ).toEqual( @@ -76,7 +82,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { test('constructs KQL filter for multiple rule types across authorized consumer', async () => { expect( - asKqlFiltersByRuleTypeAndConsumer( + asFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -128,8 +134,11 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { }, ]), { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, } ) ).toEqual( @@ -143,7 +152,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { describe('asEsDslFiltersByRuleTypeAndConsumer', () => { test('constructs ES DSL filter for single rule type with single authorized consumer', async () => { expect( - asEsDslFiltersByRuleTypeAndConsumer( + asFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -160,8 +169,11 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { }, ]), { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, } ) ).toEqual({ @@ -198,7 +210,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { test('constructs ES DSL filter for single rule type with multiple authorized consumers', async () => { expect( - asEsDslFiltersByRuleTypeAndConsumer( + asFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -217,8 +229,11 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { }, ]), { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, } ) ).toEqual({ @@ -262,7 +277,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { test('constructs ES DSL filter for multiple rule types across authorized consumer', async () => { expect( - asEsDslFiltersByRuleTypeAndConsumer( + asFiltersByRuleTypeAndConsumer( new Set([ { actionGroups: [], @@ -314,8 +329,11 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { }, ]), { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, } ) ).toEqual({ diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts index 4d1ad33a13e96..3e2db40ba5fef 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts @@ -17,7 +17,12 @@ export enum AlertingAuthorizationFilterType { ESDSL = 'dsl', } -export interface FilterFieldNames { +export interface AlertingAuthorizationFilterOpts { + type: AlertingAuthorizationFilterType; + fieldNames: AlertingAuthorizationFilterFieldNames; +} + +interface AlertingAuthorizationFilterFieldNames { ruleTypeId: string; consumer: string; } @@ -29,22 +34,20 @@ const esQueryConfig: EsQueryConfig = { queryStringOptions: { analyze_wildcard: true }, }; -// pass in the field name instead of hardcoding `alert.attributes.alertTypeId` and `alertTypeId` export function asFiltersByRuleTypeAndConsumer( ruleTypes: Set, - filterFieldNames: FilterFieldNames, - filterType: AlertingAuthorizationFilterType + opts: AlertingAuthorizationFilterOpts ): KueryNode | JsonObject { const kueryNode = nodeBuilder.or( Array.from(ruleTypes).reduce((filters, { id, authorizedConsumers }) => { ensureFieldIsSafeForQuery('ruleTypeId', id); filters.push( nodeBuilder.and([ - nodeBuilder.is(filterFieldNames.ruleTypeId, id), + nodeBuilder.is(opts.fieldNames.ruleTypeId, id), nodeBuilder.or( Object.keys(authorizedConsumers).map((consumer) => { ensureFieldIsSafeForQuery('consumer', consumer); - return nodeBuilder.is(filterFieldNames.consumer, consumer); + return nodeBuilder.is(opts.fieldNames.consumer, consumer); }) ), ]) @@ -53,35 +56,13 @@ export function asFiltersByRuleTypeAndConsumer( }, []) ); - if (filterType === AlertingAuthorizationFilterType.ESDSL) { + if (opts.type === AlertingAuthorizationFilterType.ESDSL) { return toElasticsearchQuery(kueryNode, undefined, esQueryConfig); } return kueryNode; } -export function asKqlFiltersByRuleTypeAndConsumer( - ruleTypes: Set, - filterFieldNames: FilterFieldNames -): KueryNode { - return asFiltersByRuleTypeAndConsumer( - ruleTypes, - filterFieldNames, - AlertingAuthorizationFilterType.KQL - ) as KueryNode; -} - -export function asEsDslFiltersByRuleTypeAndConsumer( - ruleTypes: Set, - filterFieldNames: FilterFieldNames -): JsonObject { - return asFiltersByRuleTypeAndConsumer( - ruleTypes, - filterFieldNames, - AlertingAuthorizationFilterType.ESDSL - ) as JsonObject; -} - export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { const invalid = value.match(/([>=<\*:()]+|\s+)/g); if (invalid) { From b5a49cd6068479d67b89aad3835bcc600f8db11a Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 4 May 2021 12:15:50 -0400 Subject: [PATCH 016/186] Cleanup and tests --- ...rting_authorization_client_factory.mock.ts | 21 ++++++++ .../server/alerts_client_factory.test.ts | 53 +++++++++---------- .../alerts_authorization.test.ts | 43 ++++++++++----- .../authorization/alerts_authorization.ts | 3 +- 4 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 x-pack/plugins/alerting/server/alerting_authorization_client_factory.mock.ts diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.mock.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.mock.ts new file mode 100644 index 0000000000000..d80f1d7289627 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; + +const creatAlertingAuthorizationClientFactoryMock = () => { + const mocked: jest.Mocked> = { + create: jest.fn(), + initialize: jest.fn(), + }; + return mocked; +}; + +export const alertingAuthorizationClientFactoryMock = { + createFactory: creatAlertingAuthorizationClientFactoryMock, +}; diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index 2bcd792e0a1b1..b88a38d5140b9 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -20,10 +20,13 @@ import { AuthenticatedUser } from '../../../plugins/security/common/model'; import { securityMock } from '../../security/server/mocks'; import { PluginStartContract as ActionsStartContract } from '../../actions/server'; import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks'; -import { featuresPluginMock } from '../../features/server/mocks'; import { LegacyAuditLogger } from '../../security/server'; import { ALERTS_FEATURE_ID } from '../common'; import { eventLogMock } from '../../event_log/server/mocks'; +import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; +import { alertingAuthorizationClientFactoryMock } from './alerting_authorization_client_factory.mock'; +import { AlertsAuthorization } from './authorization'; +import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; jest.mock('./alerts_client'); jest.mock('./authorization/alerts_authorization'); @@ -31,23 +34,26 @@ jest.mock('./authorization/audit_logger'); const savedObjectsClient = savedObjectsClientMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); -const features = featuresPluginMock.createStart(); const securityPluginSetup = securityMock.createSetup(); const securityPluginStart = securityMock.createStart(); + +const alertsAuthorization = alertsAuthorizationMock.create(); +const alertingAuthorizationClientFactory = alertingAuthorizationClientFactoryMock.createFactory(); + const alertsClientFactoryParams: jest.Mocked = { logger: loggingSystemMock.create().get(), taskManager: taskManagerMock.createStart(), alertTypeRegistry: alertTypeRegistryMock.create(), getSpaceId: jest.fn(), - getSpace: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), actions: actionsMock.createStart(), - features, eventLog: eventLogMock.createStart(), kibanaVersion: '7.10.0', + authorization: (alertingAuthorizationClientFactory as unknown) as AlertingAuthorizationClientFactory, }; + const fakeRequest = ({ app: {}, headers: {}, @@ -82,8 +88,10 @@ test('creates an alerts client with proper constructor arguments when security i factory.initialize({ securityPluginSetup, securityPluginStart, ...alertsClientFactoryParams }); const request = KibanaRequest.from(fakeRequest); - const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + alertingAuthorizationClientFactory.create.mockReturnValue( + (alertsAuthorization as unknown) as AlertsAuthorization + ); const logger = { log: jest.fn(), @@ -97,18 +105,9 @@ test('creates an alerts client with proper constructor arguments when security i includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); - const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); - expect(AlertsAuthorization).toHaveBeenCalledWith({ - request, - authorization: securityPluginStart.authz, - alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, - features: alertsClientFactoryParams.features, - auditLogger: expect.any(AlertsAuthorizationAuditLogger), - getSpace: expect.any(Function), - }); - - expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger); - expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); + expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request, [ + ALERTS_FEATURE_ID, + ]); expect(alertsClientFactoryParams.actions.getActionsAuthorizationWithRequest).toHaveBeenCalledWith( request @@ -116,7 +115,7 @@ test('creates an alerts client with proper constructor arguments when security i expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, - authorization: expect.any(AlertsAuthorization), + authorization: alertsAuthorization, actionsAuthorization, logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, @@ -138,6 +137,9 @@ test('creates an alerts client with proper constructor arguments', async () => { const request = KibanaRequest.from(fakeRequest); savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + alertingAuthorizationClientFactory.create.mockReturnValue( + (alertsAuthorization as unknown) as AlertsAuthorization + ); factory.create(request, savedObjectsService); @@ -146,20 +148,13 @@ test('creates an alerts client with proper constructor arguments', async () => { includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); - const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); - const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); - expect(AlertsAuthorization).toHaveBeenCalledWith({ - request, - authorization: undefined, - alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, - features: alertsClientFactoryParams.features, - auditLogger: expect.any(AlertsAuthorizationAuditLogger), - getSpace: expect.any(Function), - }); + expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request, [ + ALERTS_FEATURE_ID, + ]); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, - authorization: expect.any(AlertsAuthorization), + authorization: alertsAuthorization, actionsAuthorization, logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts index e5f74b8aac288..39b02bfc6eb11 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts @@ -25,6 +25,7 @@ import uuid from 'uuid'; import { RecoveredActionGroup } from '../../common'; import { RegistryAlertType } from '../alert_type_registry'; import { esKuery } from '../../../../../src/plugins/data/server'; +import { AlertingAuthorizationFilterType } from './alerts_authorization_kuery'; const alertTypeRegistry = alertTypeRegistryMock.create(); const features: jest.Mocked = featuresPluginMock.createStart(); @@ -676,8 +677,11 @@ describe('AlertsAuthorization', () => { filter, ensureRuleTypeIsAuthorized, } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { - ruleTypeId: 'ruleId', - consumer: 'consumer', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, }); expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); @@ -698,8 +702,11 @@ describe('AlertsAuthorization', () => { const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationTypes.Rule, { - ruleTypeId: 'ruleId', - consumer: 'consumer', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, } ); @@ -735,8 +742,11 @@ describe('AlertsAuthorization', () => { expect( ( await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { - ruleTypeId: 'path.to.rule.id', - consumer: 'consumer-field', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule.id', + consumer: 'consumer-field', + }, }) ).filter ).toEqual( @@ -798,8 +808,11 @@ describe('AlertsAuthorization', () => { const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationTypes.Alert, { - ruleTypeId: 'ruleId', - consumer: 'consumer', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, } ); expect(() => { @@ -872,8 +885,11 @@ describe('AlertsAuthorization', () => { const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationTypes.Rule, { - ruleTypeId: 'ruleId', - consumer: 'consumer', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, } ); expect(() => { @@ -948,8 +964,11 @@ describe('AlertsAuthorization', () => { ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { - ruleTypeId: 'ruleId', - consumer: 'consumer', + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, }); expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index fd3e64fd23020..00dd69b28e65e 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -371,12 +371,11 @@ export class AlertsAuthorization { [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel] >(); // as we can't ask ES for the user's individual privileges we need to ask for each feature - // and alertType in the system whether this user has this privilege + // and ruleType in the system whether this user has this privilege for (const ruleType of ruleTypesWithAuthorization) { for (const feature of featuresIds) { for (const operation of operations) { privilegeToRuleType.set( - // this function needs to be swappable this.authorization!.actions.alerting.get( ruleType.id, feature, From 0fbaf237bdfe8ec69d372cc51458020d7e18e98e Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Tue, 4 May 2021 12:33:34 -0400 Subject: [PATCH 017/186] Cleanup and tests --- ...rting_authorization_client_factory.test.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts new file mode 100644 index 0000000000000..fab2e984146a1 --- /dev/null +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { Request } from '@hapi/hapi'; +import { alertTypeRegistryMock } from './alert_type_registry.mock'; +import { KibanaRequest } from '../../../../src/core/server'; +import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { securityMock } from '../../security/server/mocks'; +import { ALERTS_FEATURE_ID } from '../common'; +import { + AlertingAuthorizationClientFactory, + AlertingAuthorizationClientFactoryOpts, +} from './alerting_authorization_client_factory'; +import { featuresPluginMock } from '../../features/server/mocks'; + +jest.mock('./authorization/alerts_authorization'); +jest.mock('./authorization/audit_logger'); + +const savedObjectsClient = savedObjectsClientMock.create(); +const features = featuresPluginMock.createStart(); + +const securityPluginSetup = securityMock.createSetup(); +const securityPluginStart = securityMock.createStart(); + +const alertingAuthorizationClientFactoryParams: jest.Mocked = { + alertTypeRegistry: alertTypeRegistryMock.create(), + getSpace: jest.fn(), + features, +}; + +const fakeRequest = ({ + app: {}, + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: () => savedObjectsClient, +} as unknown) as Request; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +test('creates an alerting authorization client with proper constructor arguments when security is enabled', async () => { + const factory = new AlertingAuthorizationClientFactory(); + factory.initialize({ + securityPluginSetup, + securityPluginStart, + ...alertingAuthorizationClientFactoryParams, + }); + const request = KibanaRequest.from(fakeRequest); + const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); + + factory.create(request); + + const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); + expect(AlertsAuthorization).toHaveBeenCalledWith({ + request, + authorization: securityPluginStart.authz, + alertTypeRegistry: alertingAuthorizationClientFactoryParams.alertTypeRegistry, + features: alertingAuthorizationClientFactoryParams.features, + auditLogger: expect.any(AlertsAuthorizationAuditLogger), + getSpace: expect.any(Function), + exemptConsumerIds: [], + }); + + expect(AlertsAuthorizationAuditLogger).toHaveBeenCalled(); + expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); +}); + +test('creates an alerting authorization client with proper constructor arguments when exemptConsumerIds are specified', async () => { + const factory = new AlertingAuthorizationClientFactory(); + factory.initialize({ + securityPluginSetup, + securityPluginStart, + ...alertingAuthorizationClientFactoryParams, + }); + const request = KibanaRequest.from(fakeRequest); + const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); + + factory.create(request, ['exemptConsumerA', 'exemptConsumerB']); + + const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); + expect(AlertsAuthorization).toHaveBeenCalledWith({ + request, + authorization: securityPluginStart.authz, + alertTypeRegistry: alertingAuthorizationClientFactoryParams.alertTypeRegistry, + features: alertingAuthorizationClientFactoryParams.features, + auditLogger: expect.any(AlertsAuthorizationAuditLogger), + getSpace: expect.any(Function), + exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], + }); + + expect(AlertsAuthorizationAuditLogger).toHaveBeenCalled(); + expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); +}); + +test('creates an alerting authorization client with proper constructor arguments', async () => { + const factory = new AlertingAuthorizationClientFactory(); + factory.initialize(alertingAuthorizationClientFactoryParams); + const request = KibanaRequest.from(fakeRequest); + const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); + + factory.create(request); + + const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); + expect(AlertsAuthorization).toHaveBeenCalledWith({ + request, + alertTypeRegistry: alertingAuthorizationClientFactoryParams.alertTypeRegistry, + features: alertingAuthorizationClientFactoryParams.features, + auditLogger: expect.any(AlertsAuthorizationAuditLogger), + getSpace: expect.any(Function), + exemptConsumerIds: [], + }); + + expect(AlertsAuthorizationAuditLogger).toHaveBeenCalled(); + expect(securityPluginSetup.audit.getLogger).not.toHaveBeenCalled(); +}); From 27af55f7d08605d33a612c25719ef7b6009273d8 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 12 May 2021 09:58:09 -0400 Subject: [PATCH 018/186] Initial commit with changes needed for subfeature privilege --- .../common/feature_kibana_privileges.ts | 4 +- .../plugins/features/server/feature_schema.ts | 31 ++++++++-- .../feature_privilege_builder/alerting.ts | 56 +++++++++++++++--- .../feature_privilege_iterator.ts | 59 +++++++++++++++++-- .../authorization/privileges/privileges.ts | 2 + x-pack/plugins/stack_alerts/server/feature.ts | 52 ++++++++++++++-- 6 files changed, 181 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 7febba197647d..5dc209b03068c 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -91,7 +91,7 @@ export interface FeatureKibanaPrivileges { * } * ``` */ - all?: readonly string[]; + all?: readonly string[] | { rule?: readonly string[]; alert?: readonly string[] }; /** * List of alert types which users should have read-only access to when granted this privilege. @@ -102,7 +102,7 @@ export interface FeatureKibanaPrivileges { * } * ``` */ - read?: readonly string[]; + read?: readonly string[] | { rule?: readonly string[]; alert?: readonly string[] }; }; /** * If your feature requires access to specific saved objects, then specify your access needs here. diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 204c5bdfe2469..941452f248b0d 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -32,7 +32,13 @@ const managementSchema = Joi.object().pattern( Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)) ); const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); -const alertingSchema = Joi.array().items(Joi.string()); +const alertingSchema = Joi.alternatives().try([ + Joi.object({ + rule: Joi.array().items(Joi.string()), + alert: Joi.array().items(Joi.string()), + }), + Joi.array().items(Joi.string()), +]); const appCategorySchema = Joi.object({ id: Joi.string().required(), @@ -112,7 +118,7 @@ const kibanaFeatureSchema = Joi.object({ app: Joi.array().items(Joi.string()).required(), management: managementSchema, catalogue: catalogueSchema, - alerting: alertingSchema, + alerting: Joi.array().items(Joi.string()), privileges: Joi.object({ all: kibanaPrivilegeSchema, read: kibanaPrivilegeSchema, @@ -203,8 +209,25 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { } function validateAlertingEntry(privilegeId: string, entry: FeatureKibanaPrivileges['alerting']) { - const all = entry?.all ?? []; - const read = entry?.read ?? []; + let all: string[] = []; + let read: string[] = []; + if (Array.isArray(entry?.all)) { + all = entry?.all ?? []; + } else { + const allObject = entry?.all as { rule?: readonly string[]; alert?: readonly string[] }; + const rule = allObject?.rule ?? []; + const alert = allObject?.alert ?? []; + all = [...rule, ...alert]; + } + + if (Array.isArray(entry?.read)) { + read = entry?.read ?? []; + } else { + const readObject = entry?.read as { rule?: readonly string[]; alert?: readonly string[] }; + const rule = readObject?.rule ?? []; + const alert = readObject?.alert ?? []; + read = [...rule, ...alert]; + } all.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); read.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index c238d061c1b52..3871e10f8690a 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -46,21 +46,61 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder feature: KibanaFeature ): string[] { const getAlertingPrivilege = ( - operations: Record, + operations: string[], privilegedTypes: readonly string[], + alertingEntity: string, consumer: string ) => - privilegedTypes.flatMap((privilegedType) => - Object.values(AlertingType).flatMap((alertingType) => - operations[alertingType].map((operation) => - this.actions.alerting.get(privilegedType, consumer, alertingType, operation) - ) + privilegedTypes.flatMap((type) => + operations.map((operation) => + this.actions.alerting.get(type, consumer, alertingEntity, operation) ) ); + let ruleAll: string[] = []; + let ruleRead: string[] = []; + let alertAll: string[] = []; + let alertRead: string[] = []; + if (Array.isArray(privilegeDefinition.alerting?.all)) { + ruleAll = [...(privilegeDefinition.alerting?.all ?? [])]; + alertAll = [...(privilegeDefinition.alerting?.all ?? [])]; + } else { + const allObject = privilegeDefinition.alerting?.all as { + rule?: readonly string[]; + alert?: readonly string[]; + }; + const rule = allObject?.rule ?? []; + const alert = allObject?.alert ?? []; + ruleAll = [...rule]; + alertAll = [...alert]; + } + + if (Array.isArray(privilegeDefinition.alerting?.read)) { + ruleRead = [...(privilegeDefinition.alerting?.read ?? [])]; + alertRead = [...(privilegeDefinition.alerting?.read ?? [])]; + } else { + const readObject = privilegeDefinition.alerting?.read as { + rule?: readonly string[]; + alert?: readonly string[]; + }; + const rule = readObject?.rule ?? []; + const alert = readObject?.alert ?? []; + ruleRead = [...rule]; + alertRead = [...alert]; + } + + if (feature.id === 'stackAlerts') { + console.log(`ruleAll ${ruleAll}`); + console.log(`ruleRead ${ruleRead}`); + console.log(`alertAll ${alertAll}`); + console.log(`alertRead ${alertRead}`); + } + return uniq([ - ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.all ?? [], feature.id), - ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.read ?? [], feature.id), + ...getAlertingPrivilege(allOperations.rule, ruleAll, 'rule', feature.id), + ...getAlertingPrivilege(allOperations.alert, alertAll, 'alert', feature.id), + ...getAlertingPrivilege(readOperations.rule, ruleRead, 'rule', feature.id), + ...getAlertingPrivilege(readOperations.alert, alertRead, 'alert', feature.id), ]); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index de2f44a446a19..b4a1c968a4c26 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -81,12 +81,61 @@ function mergeWithSubFeatures( subFeaturePrivilege.savedObject.read ); + let all: string[] = []; + let read: string[] = []; + if (Array.isArray(mergedConfig.alerting?.all)) { + all = mergedConfig.alerting?.all ?? []; + } else { + const allObject = mergedConfig.alerting?.all as { + rule?: readonly string[]; + alert?: readonly string[]; + }; + const rule = allObject?.rule ?? []; + const alert = allObject?.alert ?? []; + all = [...rule, ...alert]; + } + + if (Array.isArray(mergedConfig.alerting?.read)) { + read = mergedConfig.alerting?.read ?? []; + } else { + const readObject = mergedConfig.alerting?.read as { + rule?: readonly string[]; + alert?: readonly string[]; + }; + const rule = readObject?.rule ?? []; + const alert = readObject?.alert ?? []; + read = [...rule, ...alert]; + } + + let subfeatureAll: string[] = []; + let subfeatureRead: string[] = []; + if (Array.isArray(subFeaturePrivilege.alerting?.all)) { + subfeatureAll = subFeaturePrivilege.alerting?.all ?? []; + } else { + const allObject = subFeaturePrivilege.alerting?.all as { + rule?: readonly string[]; + alert?: readonly string[]; + }; + const rule = allObject?.rule ?? []; + const alert = allObject?.alert ?? []; + subfeatureAll = [...rule, ...alert]; + } + + if (Array.isArray(subFeaturePrivilege.alerting?.read)) { + subfeatureRead = subFeaturePrivilege.alerting?.read ?? []; + } else { + const readObject = subFeaturePrivilege.alerting?.read as { + rule?: readonly string[]; + alert?: readonly string[]; + }; + const rule = readObject?.rule ?? []; + const alert = readObject?.alert ?? []; + subfeatureRead = [...rule, ...alert]; + } + mergedConfig.alerting = { - all: mergeArrays(mergedConfig.alerting?.all ?? [], subFeaturePrivilege.alerting?.all ?? []), - read: mergeArrays( - mergedConfig.alerting?.read ?? [], - subFeaturePrivilege.alerting?.read ?? [] - ), + all: mergeArrays(all ?? [], subfeatureAll ?? []), + read: mergeArrays(read ?? [], subfeatureRead ?? []), }; } return mergedConfig; diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 1826b853ce668..b9a874c3ca06e 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -99,6 +99,8 @@ export function privilegesFactory( delete featurePrivileges[feature.id]; } } + + console.log(`stackAlerts: ${JSON.stringify(featurePrivileges.stackAlerts)}`); return { features: featurePrivileges, global: { diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index e168ec21438c0..3455cdb05e833 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -31,8 +31,14 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [IndexThreshold, GeoContainment, ElasticsearchQuery], - read: [], + all: { + rule: [IndexThreshold, GeoContainment, ElasticsearchQuery], + alert: [], + }, + read: { + rule: [], + alert: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, }, savedObject: { all: [], @@ -48,8 +54,14 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [], - read: [IndexThreshold, GeoContainment, ElasticsearchQuery], + all: { + rule: [], + alert: [], + }, + read: { + rule: [IndexThreshold, GeoContainment, ElasticsearchQuery], + alert: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, }, savedObject: { all: [], @@ -59,4 +71,36 @@ export const BUILT_IN_ALERTS_FEATURE = { ui: [], }, }, + subFeatures: [ + { + name: 'Manage Alerts', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'alert_manage', + name: 'Manage Alerts', + includeIn: 'all', + alerting: { + all: { + rule: [], + alert: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, + read: { + rule: [], + alert: [], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }, + ], }; From ee3f2269ce5c043ab604be40a78d1ec261977ecd Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 12 May 2021 10:27:55 -0400 Subject: [PATCH 019/186] Throwing error when AlertingAuthorizationClientFactory is not defined --- x-pack/plugins/alerting/server/alerts_client_factory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 666790200141b..3bb014bf9fd07 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -73,6 +73,10 @@ export class AlertsClientFactory { const { securityPluginSetup, securityPluginStart, actions, eventLog } = this; const spaceId = this.getSpaceId(request); + if (!this.authorization) { + throw new Error('AlertingAuthorizationClientFactory is not defined'); + } + return new AlertsClient({ spaceId, kibanaVersion: this.kibanaVersion, @@ -83,7 +87,7 @@ export class AlertsClientFactory { excludedWrappers: ['security'], includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), - authorization: this.authorization!.create(request, [ALERTS_FEATURE_ID]), + authorization: this.authorization.create(request, [ALERTS_FEATURE_ID]), actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, From bfbfae406722dc86bbfa235c16e618228c3f6ebe Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 12 May 2021 10:45:02 -0400 Subject: [PATCH 020/186] Renaming authorizationType to entity --- .../server/alerts_client/alerts_client.ts | 36 ++++++------- .../server/alerts_client/tests/create.test.ts | 6 +-- .../server/alerts_client/tests/delete.test.ts | 4 +- .../alerts_client/tests/disable.test.ts | 4 +- .../server/alerts_client/tests/enable.test.ts | 4 +- .../server/alerts_client/tests/get.test.ts | 4 +- .../tests/get_alert_state.test.ts | 4 +- .../alerts_client/tests/mute_all.test.ts | 4 +- .../alerts_client/tests/mute_instance.test.ts | 4 +- .../alerts_client/tests/unmute_all.test.ts | 4 +- .../tests/unmute_instance.test.ts | 4 +- .../server/alerts_client/tests/update.test.ts | 4 +- .../tests/update_api_key.test.ts | 4 +- .../alerts_authorization.test.ts | 44 ++++++++-------- .../authorization/alerts_authorization.ts | 52 ++++++++----------- .../server/authorization/audit_logger.test.ts | 52 +++++++++---------- .../server/authorization/audit_logger.ts | 26 +++++----- .../server/authorization/actions/alerting.ts | 8 +-- 18 files changed, 131 insertions(+), 137 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 4706d707f6e3d..e9a7f572b4b36 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -50,7 +50,7 @@ import { AlertsAuthorization, WriteOperations, ReadOperations, - AlertingAuthorizationTypes, + AlertingAuthorizationEntity, AlertingAuthorizationFilterType, AlertingAuthorizationFilterOpts, } from '../authorization'; @@ -248,7 +248,7 @@ export class AlertsClient { ruleTypeId: data.alertTypeId, consumer: data.consumer, operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); } catch (error) { this.auditLogger?.log( @@ -374,7 +374,7 @@ export class AlertsClient { ruleTypeId: result.attributes.alertTypeId, consumer: result.attributes.consumer, operation: ReadOperations.Get, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); } catch (error) { this.auditLogger?.log( @@ -401,7 +401,7 @@ export class AlertsClient { ruleTypeId: alert.alertTypeId, consumer: alert.consumer, operation: ReadOperations.GetAlertState, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (alert.scheduledTaskId) { const { state } = taskInstanceToAlertTaskInstance( @@ -422,7 +422,7 @@ export class AlertsClient { ruleTypeId: alert.alertTypeId, consumer: alert.consumer, operation: ReadOperations.GetAlertInstanceSummary, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); // default duration of instance summary is 60 * alert interval @@ -465,7 +465,7 @@ export class AlertsClient { let authorizationTuple; try { authorizationTuple = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule, + AlertingAuthorizationEntity.Rule, alertingAuthorizationFilterOpts ); } catch (error) { @@ -507,7 +507,7 @@ export class AlertsClient { ensureRuleTypeIsAuthorized( attributes.alertTypeId, attributes.consumer, - AlertingAuthorizationTypes.Rule + AlertingAuthorizationEntity.Rule ); } catch (error) { this.auditLogger?.log( @@ -555,7 +555,7 @@ export class AlertsClient { filter: authorizationFilter, logSuccessfulAuthorization, } = await this.authorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule, + AlertingAuthorizationEntity.Rule, alertingAuthorizationFilterOpts ); const filter = options.filter @@ -619,7 +619,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.Delete, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); } catch (error) { this.auditLogger?.log( @@ -693,7 +693,7 @@ export class AlertsClient { ruleTypeId: alertSavedObject.attributes.alertTypeId, consumer: alertSavedObject.attributes.consumer, operation: WriteOperations.Update, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); } catch (error) { this.auditLogger?.log( @@ -866,7 +866,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.UpdateApiKey, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -971,7 +971,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.Enable, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (attributes.actions.length) { @@ -1084,7 +1084,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.Disable, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); } catch (error) { this.auditLogger?.log( @@ -1157,7 +1157,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.MuteAll, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (attributes.actions.length) { @@ -1219,7 +1219,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.UnmuteAll, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (attributes.actions.length) { @@ -1281,7 +1281,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.MuteInstance, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (attributes.actions.length) { @@ -1349,7 +1349,7 @@ export class AlertsClient { ruleTypeId: attributes.alertTypeId, consumer: attributes.consumer, operation: WriteOperations.UnmuteInstance, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); @@ -1394,7 +1394,7 @@ export class AlertsClient { return await this.authorization.filterByRuleTypeAuthorization( this.alertTypeRegistry.list(), [ReadOperations.Get, WriteOperations.Create], - AlertingAuthorizationTypes.Rule + AlertingAuthorizationEntity.Rule ); } diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index 3da8ec322fd34..e644d043aa255 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -195,7 +195,7 @@ describe('create()', () => { await tryToExecuteOperation({ data }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'create', ruleTypeId: 'myType', @@ -217,7 +217,7 @@ describe('create()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'create', ruleTypeId: 'myType', @@ -349,7 +349,7 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'bar', operation: 'create', ruleTypeId: '123', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 6248b50380df5..483bd7afded6d 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -232,7 +232,7 @@ describe('delete()', () => { await alertsClient.delete({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'delete', ruleTypeId: 'myType', @@ -249,7 +249,7 @@ describe('delete()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'delete', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index dda25b46116d4..12b2ef78980ec 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -100,7 +100,7 @@ describe('disable()', () => { await alertsClient.disable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'disable', ruleTypeId: 'myType', @@ -117,7 +117,7 @@ describe('disable()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'disable', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 25efa3d264644..39c9e40f376dc 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -138,7 +138,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'enable', ruleTypeId: 'myType', @@ -156,7 +156,7 @@ describe('enable()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'enable', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index a7f033c48496f..4458c59083e3b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -183,7 +183,7 @@ describe('get()', () => { await alertsClient.get({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'get', ruleTypeId: 'myType', @@ -201,7 +201,7 @@ describe('get()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'get', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts index 4e04f44e89055..8e9e6db1f8315 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts @@ -211,7 +211,7 @@ describe('getAlertState()', () => { await alertsClient.getAlertState({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'getAlertState', ruleTypeId: 'myType', @@ -232,7 +232,7 @@ describe('getAlertState()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'getAlertState', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 6f175a05c62c0..1213affa4d0c2 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -127,7 +127,7 @@ describe('muteAll()', () => { await alertsClient.muteAll({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'muteAll', ruleTypeId: 'myType', @@ -146,7 +146,7 @@ describe('muteAll()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'muteAll', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index a67513e266a96..7e137fc542aeb 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -160,7 +160,7 @@ describe('muteInstance()', () => { expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'muteInstance', ruleTypeId: 'myType', @@ -180,7 +180,7 @@ describe('muteInstance()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'muteInstance', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index 5919c4487ebaf..6656646a1b0ff 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -127,7 +127,7 @@ describe('unmuteAll()', () => { await alertsClient.unmuteAll({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'unmuteAll', ruleTypeId: 'myType', @@ -146,7 +146,7 @@ describe('unmuteAll()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'unmuteAll', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index b73d1801e7364..39352673cbe86 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -158,7 +158,7 @@ describe('unmuteInstance()', () => { expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'unmuteInstance', ruleTypeId: 'myType', @@ -178,7 +178,7 @@ describe('unmuteInstance()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'unmuteInstance', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index ce6106c59300f..db52863d125ed 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -1416,7 +1416,7 @@ describe('update()', () => { }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'update', ruleTypeId: 'myType', @@ -1448,7 +1448,7 @@ describe('update()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'update', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 16991be48eab1..64f68824d2f86 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -269,7 +269,7 @@ describe('updateApiKey()', () => { expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'updateApiKey', ruleTypeId: 'myType', @@ -286,7 +286,7 @@ describe('updateApiKey()', () => { ); expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ - authorizationType: 'rule', + entity: 'rule', consumer: 'myApp', operation: 'updateApiKey', ruleTypeId: 'myType', diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts index 39b02bfc6eb11..9b3427ef666b4 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts @@ -17,7 +17,7 @@ import { AlertsAuthorization, WriteOperations, ReadOperations, - AlertingAuthorizationTypes, + AlertingAuthorizationEntity, } from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; @@ -240,7 +240,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); @@ -263,7 +263,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); @@ -295,7 +295,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -351,7 +351,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'exemptConsumer', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -413,7 +413,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myOtherApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -490,7 +490,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myOtherApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Rule, + entity: AlertingAuthorizationEntity.Rule, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" rule for \\"myOtherApp\\""` @@ -548,7 +548,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myOtherApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Alert, + entity: AlertingAuthorizationEntity.Alert, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` @@ -606,7 +606,7 @@ describe('AlertsAuthorization', () => { ruleTypeId: 'myType', consumer: 'myOtherApp', operation: WriteOperations.Create, - authorizationType: AlertingAuthorizationTypes.Alert, + entity: AlertingAuthorizationEntity.Alert, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` @@ -676,7 +676,7 @@ describe('AlertsAuthorization', () => { const { filter, ensureRuleTypeIsAuthorized, - } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { ruleTypeId: 'ruleId', @@ -700,7 +700,7 @@ describe('AlertsAuthorization', () => { }); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule, + AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { @@ -741,7 +741,7 @@ describe('AlertsAuthorization', () => { expect( ( - await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { ruleTypeId: 'path.to.rule.id', @@ -806,7 +806,7 @@ describe('AlertsAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Alert, + AlertingAuthorizationEntity.Alert, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { @@ -883,7 +883,7 @@ describe('AlertsAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationTypes.Rule, + AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { @@ -963,7 +963,7 @@ describe('AlertsAuthorization', () => { const { ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, - } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationTypes.Rule, { + } = await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { ruleTypeId: 'ruleId', @@ -1043,7 +1043,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create], - AlertingAuthorizationTypes.Rule + AlertingAuthorizationEntity.Rule ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1122,7 +1122,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create], - AlertingAuthorizationTypes.Rule + AlertingAuthorizationEntity.Rule ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1253,7 +1253,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create], - AlertingAuthorizationTypes.Rule + AlertingAuthorizationEntity.Rule ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1356,7 +1356,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create], - AlertingAuthorizationTypes.Rule + AlertingAuthorizationEntity.Rule ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1450,7 +1450,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType]), [WriteOperations.Create], - AlertingAuthorizationTypes.Alert + AlertingAuthorizationEntity.Alert ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1554,7 +1554,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create, ReadOperations.Get], - AlertingAuthorizationTypes.Alert + AlertingAuthorizationEntity.Alert ) ).resolves.toMatchInlineSnapshot(` Set { @@ -1661,7 +1661,7 @@ describe('AlertsAuthorization', () => { alertAuthorization.filterByRuleTypeAuthorization( new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create], - AlertingAuthorizationTypes.Alert + AlertingAuthorizationEntity.Alert ) ).resolves.toMatchInlineSnapshot(` Set { diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts index 00dd69b28e65e..05abc46e75f67 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts @@ -21,7 +21,7 @@ import { import { KueryNode } from '../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; -export enum AlertingAuthorizationTypes { +export enum AlertingAuthorizationEntity { Rule = 'rule', Alert = 'alert', } @@ -50,7 +50,7 @@ export interface EnsureAuthorizedOpts { ruleTypeId: string; consumer: string; operation: ReadOperations | WriteOperations; - authorizationType: AlertingAuthorizationTypes; + entity: AlertingAuthorizationEntity; } interface HasPrivileges { @@ -138,28 +138,18 @@ export class AlertsAuthorization { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } - public async ensureAuthorized({ - ruleTypeId, - consumer, - operation, - authorizationType, - }: EnsureAuthorizedOpts) { + public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) { const { authorization } = this; const isAvailableConsumer = has(await this.allPossibleConsumers, consumer); if (authorization && this.shouldCheckAuthorization()) { const ruleType = this.alertTypeRegistry.get(ruleTypeId); const requiredPrivilegesByScope = { - consumer: authorization.actions.alerting.get( - ruleTypeId, - consumer, - authorizationType, - operation - ), + consumer: authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation), producer: authorization.actions.alerting.get( ruleTypeId, ruleType.producer, - authorizationType, + entity, operation ), }; @@ -199,7 +189,7 @@ export class AlertsAuthorization { ScopeType.Consumer, consumer, operation, - authorizationType + entity ) ); } @@ -211,7 +201,7 @@ export class AlertsAuthorization { ScopeType.Consumer, consumer, operation, - authorizationType + entity ); } else { const authorizedPrivileges = map( @@ -235,7 +225,7 @@ export class AlertsAuthorization { unauthorizedScopeType, unauthorizedScope, operation, - authorizationType + entity ) ); } @@ -247,14 +237,14 @@ export class AlertsAuthorization { ScopeType.Consumer, consumer, operation, - authorizationType + entity ) ); } } public async getFindAuthorizationFilter( - authorizationType: AlertingAuthorizationTypes, + authorizationEntity: AlertingAuthorizationEntity, filterOpts: AlertingAuthorizationFilterOpts ): Promise<{ filter?: KueryNode | JsonObject; @@ -265,19 +255,23 @@ export class AlertsAuthorization { const { username, authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( this.alertTypeRegistry.list(), [ReadOperations.Find], - authorizationType + authorizationEntity ); if (!authorizedRuleTypes.size) { throw Boom.forbidden( - this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find', authorizationType) + this.auditLogger.alertsUnscopedAuthorizationFailure( + username!, + 'find', + authorizationEntity + ) ); } const authorizedRuleTypeIdsToConsumers = new Set( [...authorizedRuleTypes].reduce((ruleTypeIdConsumerPairs, ruleType) => { for (const consumer of Object.keys(ruleType.authorizedConsumers)) { - ruleTypeIdConsumerPairs.push(`${ruleType.id}/${consumer}/${authorizationType}`); + ruleTypeIdConsumerPairs.push(`${ruleType.id}/${consumer}/${authorizationEntity}`); } return ruleTypeIdConsumerPairs; }, []) @@ -295,7 +289,7 @@ export class AlertsAuthorization { ScopeType.Consumer, consumer, 'find', - authorizationType + authorizationEntity ) ); } else { @@ -321,7 +315,7 @@ export class AlertsAuthorization { ), ScopeType.Consumer, 'find', - authorizationType + authorizationEntity ); } }, @@ -336,12 +330,12 @@ export class AlertsAuthorization { public async filterByRuleTypeAuthorization( ruleTypes: Set, operations: Array, - authorizationType: AlertingAuthorizationTypes + authorizationEntity: AlertingAuthorizationEntity ): Promise> { const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( ruleTypes, operations, - authorizationType + authorizationEntity ); return authorizedRuleTypes; } @@ -349,7 +343,7 @@ export class AlertsAuthorization { private async augmentRuleTypesWithAuthorization( ruleTypes: Set, operations: Array, - authorizationType: AlertingAuthorizationTypes + authorizationEntity: AlertingAuthorizationEntity ): Promise<{ username?: string; hasAllRequested: boolean; @@ -379,7 +373,7 @@ export class AlertsAuthorization { this.authorization!.actions.alerting.get( ruleType.id, feature, - authorizationType, + authorizationEntity, operation ), [ruleType, feature, hasPrivilegeByOperation(operation), ruleType.producer === feature] diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts index 2d79fd8004acd..addcff7611072 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts @@ -22,7 +22,7 @@ describe(`#constructor`, () => { const scopeType = ScopeType.Consumer; const scope = 'myApp'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; expect(() => { alertsAuditLogger.alertsAuthorizationFailure( username, @@ -30,7 +30,7 @@ describe(`#constructor`, () => { scopeType, scope, operation, - authorizationType + entity ); alertsAuditLogger.alertsAuthorizationSuccess( @@ -39,7 +39,7 @@ describe(`#constructor`, () => { scopeType, scope, operation, - authorizationType + entity ); }).not.toThrow(); }); @@ -51,9 +51,9 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; - alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation, authorizationType); + alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation, entity); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -75,7 +75,7 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { const scopeType = ScopeType.Producer; const scope = 'myOtherApp'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsAuthorizationFailure( username, @@ -83,7 +83,7 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { scopeType, scope, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -92,7 +92,7 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { "foo-user Unauthorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", - "authorizationType": "rule", + "entity": "rule", "operation": "create", "scope": "myOtherApp", "scopeType": 1, @@ -112,7 +112,7 @@ describe(`#alertsAuthorizationFailure`, () => { const scopeType = ScopeType.Consumer; const scope = 'myApp'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsAuthorizationFailure( username, @@ -120,7 +120,7 @@ describe(`#alertsAuthorizationFailure`, () => { scopeType, scope, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -129,7 +129,7 @@ describe(`#alertsAuthorizationFailure`, () => { "foo-user Unauthorized to create a \\"alert-type-id\\" rule for \\"myApp\\"", Object { "alertTypeId": "alert-type-id", - "authorizationType": "rule", + "entity": "rule", "operation": "create", "scope": "myApp", "scopeType": 0, @@ -147,7 +147,7 @@ describe(`#alertsAuthorizationFailure`, () => { const scopeType = ScopeType.Producer; const scope = 'myOtherApp'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsAuthorizationFailure( username, @@ -155,7 +155,7 @@ describe(`#alertsAuthorizationFailure`, () => { scopeType, scope, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -164,7 +164,7 @@ describe(`#alertsAuthorizationFailure`, () => { "foo-user Unauthorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", - "authorizationType": "rule", + "entity": "rule", "operation": "create", "scope": "myOtherApp", "scopeType": 1, @@ -186,14 +186,14 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { ['other-alert-type-id', 'myOtherApp'], ]; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsBulkAuthorizationSuccess( username, authorizedEntries, scopeType, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -201,7 +201,6 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { "alerts_authorization_success", "foo-user Authorized to create: \\"alert-type-id\\" rules for \\"myApp\\", \\"other-alert-type-id\\" rules for \\"myOtherApp\\"", Object { - "authorizationType": "rule", "authorizedEntries": Array [ Array [ "alert-type-id", @@ -212,6 +211,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { "myOtherApp", ], ], + "entity": "rule", "operation": "create", "scopeType": 0, "username": "foo-user", @@ -230,14 +230,14 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { ['other-alert-type-id', 'myOtherApp'], ]; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsBulkAuthorizationSuccess( username, authorizedEntries, scopeType, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -245,7 +245,6 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { "alerts_authorization_success", "foo-user Authorized to create: \\"alert-type-id\\" rules by \\"myApp\\", \\"other-alert-type-id\\" rules by \\"myOtherApp\\"", Object { - "authorizationType": "rule", "authorizedEntries": Array [ Array [ "alert-type-id", @@ -256,6 +255,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { "myOtherApp", ], ], + "entity": "rule", "operation": "create", "scopeType": 1, "username": "foo-user", @@ -274,7 +274,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const scopeType = ScopeType.Consumer; const scope = 'myApp'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsAuthorizationSuccess( username, @@ -282,7 +282,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { scopeType, scope, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -291,7 +291,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { "foo-user Authorized to create a \\"alert-type-id\\" rule for \\"myApp\\"", Object { "alertTypeId": "alert-type-id", - "authorizationType": "rule", + "entity": "rule", "operation": "create", "scope": "myApp", "scopeType": 0, @@ -309,7 +309,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const scopeType = ScopeType.Producer; const scope = 'myOtherApp'; const operation = 'create'; - const authorizationType = 'rule'; + const entity = 'rule'; alertsAuditLogger.alertsAuthorizationSuccess( username, @@ -317,7 +317,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { scopeType, scope, operation, - authorizationType + entity ); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` @@ -326,7 +326,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { "foo-user Authorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", - "authorizationType": "rule", + "entity": "rule", "operation": "create", "scope": "myOtherApp", "scopeType": 1, diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.ts index ea5c74078df92..7aa6c299a3848 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.ts @@ -30,9 +30,9 @@ export class AlertsAuthorizationAuditLogger { scopeType: ScopeType, scope: string, operation: string, - authorizationType: string + entity: string ): string { - return `${authorizationResult} to ${operation} a "${alertTypeId}" ${authorizationType} ${ + return `${authorizationResult} to ${operation} a "${alertTypeId}" ${entity} ${ scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` }`; } @@ -43,7 +43,7 @@ export class AlertsAuthorizationAuditLogger { scopeType: ScopeType, scope: string, operation: string, - authorizationType: string + entity: string ): string { const message = this.getAuthorizationMessage( AuthorizationResult.Unauthorized, @@ -51,7 +51,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, scope, operation, - authorizationType + entity ); this.auditLogger.log('alerts_authorization_failure', `${username} ${message}`, { username, @@ -59,7 +59,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, scope, operation, - authorizationType, + entity, }); return message; } @@ -67,9 +67,9 @@ export class AlertsAuthorizationAuditLogger { public alertsUnscopedAuthorizationFailure( username: string, operation: string, - authorizationType: string + entity: string ): string { - const message = `Unauthorized to ${operation} ${authorizationType}s for any rule types`; + const message = `Unauthorized to ${operation} ${entity}s for any rule types`; this.auditLogger.log('alerts_unscoped_authorization_failure', `${username} ${message}`, { username, operation, @@ -83,7 +83,7 @@ export class AlertsAuthorizationAuditLogger { scopeType: ScopeType, scope: string, operation: string, - authorizationType: string + entity: string ): string { const message = this.getAuthorizationMessage( AuthorizationResult.Authorized, @@ -91,7 +91,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, scope, operation, - authorizationType + entity ); this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, { username, @@ -99,7 +99,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, scope, operation, - authorizationType, + entity, }); return message; } @@ -109,12 +109,12 @@ export class AlertsAuthorizationAuditLogger { authorizedEntries: Array<[string, string]>, scopeType: ScopeType, operation: string, - authorizationType: string + entity: string ): string { const message = `${AuthorizationResult.Authorized} to ${operation}: ${authorizedEntries .map( ([alertTypeId, scope]) => - `"${alertTypeId}" ${authorizationType}s ${ + `"${alertTypeId}" ${entity}s ${ scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` }` ) @@ -124,7 +124,7 @@ export class AlertsAuthorizationAuditLogger { scopeType, authorizedEntries, operation, - authorizationType, + entity, }); return message; } diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts index 47d575994d31c..339e8801e896f 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.ts @@ -17,7 +17,7 @@ export class AlertingActions { public get( ruleTypeId: string, consumer: string, - alertingType: string, + alertingEntity: string, operation: string ): string { if (!ruleTypeId || !isString(ruleTypeId)) { @@ -32,10 +32,10 @@ export class AlertingActions { throw new Error('consumer is required and must be a string'); } - if (!alertingType || !isString(alertingType)) { - throw new Error('alertingType is required and must be a string'); + if (!alertingEntity || !isString(alertingEntity)) { + throw new Error('alertingEntity is required and must be a string'); } - return `${this.prefix}${ruleTypeId}/${consumer}/${alertingType}/${operation}`; + return `${this.prefix}${ruleTypeId}/${consumer}/${alertingEntity}/${operation}`; } } From 3abf4880dac2ec678073794fb3c91e66fd03e0bf Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 12 May 2021 12:00:37 -0400 Subject: [PATCH 021/186] Renaming AlertsAuthorization to AlertingAuthorization --- ...rting_authorization_client_factory.test.ts | 32 ++--- .../alerting_authorization_client_factory.ts | 10 +- .../server/alerts_client/alerts_client.ts | 6 +- .../alerts_client/tests/aggregate.test.ts | 8 +- .../server/alerts_client/tests/create.test.ts | 8 +- .../server/alerts_client/tests/delete.test.ts | 8 +- .../alerts_client/tests/disable.test.ts | 8 +- .../server/alerts_client/tests/enable.test.ts | 8 +- .../server/alerts_client/tests/find.test.ts | 8 +- .../server/alerts_client/tests/get.test.ts | 8 +- .../tests/get_alert_instance_summary.test.ts | 8 +- .../tests/get_alert_state.test.ts | 8 +- .../tests/list_alert_types.test.ts | 10 +- .../alerts_client/tests/mute_all.test.ts | 8 +- .../alerts_client/tests/mute_instance.test.ts | 8 +- .../alerts_client/tests/unmute_all.test.ts | 8 +- .../tests/unmute_instance.test.ts | 8 +- .../server/alerts_client/tests/update.test.ts | 8 +- .../tests/update_api_key.test.ts | 8 +- .../alerts_client_conflict_retries.test.ts | 8 +- .../server/alerts_client_factory.test.ts | 12 +- ...mock.ts => alerting_authorization.mock.ts} | 16 +-- ...test.ts => alerting_authorization.test.ts} | 126 +++++++++--------- ...orization.ts => alerting_authorization.ts} | 28 ++-- ...s => alerting_authorization_kuery.test.ts} | 2 +- ...ery.ts => alerting_authorization_kuery.ts} | 2 +- .../server/authorization/audit_logger.mock.ts | 20 +-- .../server/authorization/audit_logger.test.ts | 62 ++++----- .../server/authorization/audit_logger.ts | 18 +-- .../alerting/server/authorization/index.ts | 4 +- x-pack/plugins/alerting/server/plugin.ts | 6 +- 31 files changed, 240 insertions(+), 242 deletions(-) rename x-pack/plugins/alerting/server/authorization/{alerts_authorization.mock.ts => alerting_authorization.mock.ts} (53%) rename x-pack/plugins/alerting/server/authorization/{alerts_authorization.test.ts => alerting_authorization.test.ts} (92%) rename x-pack/plugins/alerting/server/authorization/{alerts_authorization.ts => alerting_authorization.ts} (95%) rename x-pack/plugins/alerting/server/authorization/{alerts_authorization_kuery.test.ts => alerting_authorization_kuery.test.ts} (99%) rename x-pack/plugins/alerting/server/authorization/{alerts_authorization_kuery.ts => alerting_authorization_kuery.ts} (97%) diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts index fab2e984146a1..dd7c483c4554e 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts @@ -17,7 +17,7 @@ import { } from './alerting_authorization_client_factory'; import { featuresPluginMock } from '../../features/server/mocks'; -jest.mock('./authorization/alerts_authorization'); +jest.mock('./authorization/alerting_authorization'); jest.mock('./authorization/audit_logger'); const savedObjectsClient = savedObjectsClientMock.create(); @@ -61,22 +61,22 @@ test('creates an alerting authorization client with proper constructor arguments ...alertingAuthorizationClientFactoryParams, }); const request = KibanaRequest.from(fakeRequest); - const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); + const { AlertingAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); factory.create(request); - const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); - expect(AlertsAuthorization).toHaveBeenCalledWith({ + const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization'); + expect(AlertingAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginStart.authz, alertTypeRegistry: alertingAuthorizationClientFactoryParams.alertTypeRegistry, features: alertingAuthorizationClientFactoryParams.features, - auditLogger: expect.any(AlertsAuthorizationAuditLogger), + auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), exemptConsumerIds: [], }); - expect(AlertsAuthorizationAuditLogger).toHaveBeenCalled(); + expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); }); @@ -88,22 +88,22 @@ test('creates an alerting authorization client with proper constructor arguments ...alertingAuthorizationClientFactoryParams, }); const request = KibanaRequest.from(fakeRequest); - const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); + const { AlertingAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); factory.create(request, ['exemptConsumerA', 'exemptConsumerB']); - const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); - expect(AlertsAuthorization).toHaveBeenCalledWith({ + const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization'); + expect(AlertingAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginStart.authz, alertTypeRegistry: alertingAuthorizationClientFactoryParams.alertTypeRegistry, features: alertingAuthorizationClientFactoryParams.features, - auditLogger: expect.any(AlertsAuthorizationAuditLogger), + auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); - expect(AlertsAuthorizationAuditLogger).toHaveBeenCalled(); + expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); }); @@ -111,20 +111,20 @@ test('creates an alerting authorization client with proper constructor arguments const factory = new AlertingAuthorizationClientFactory(); factory.initialize(alertingAuthorizationClientFactoryParams); const request = KibanaRequest.from(fakeRequest); - const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); + const { AlertingAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); factory.create(request); - const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); - expect(AlertsAuthorization).toHaveBeenCalledWith({ + const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization'); + expect(AlertingAuthorization).toHaveBeenCalledWith({ request, alertTypeRegistry: alertingAuthorizationClientFactoryParams.alertTypeRegistry, features: alertingAuthorizationClientFactoryParams.features, - auditLogger: expect.any(AlertsAuthorizationAuditLogger), + auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), exemptConsumerIds: [], }); - expect(AlertsAuthorizationAuditLogger).toHaveBeenCalled(); + expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); expect(securityPluginSetup.audit.getLogger).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts index 8322651672cd2..ea882b07f1e98 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -10,8 +10,8 @@ import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry } from './types'; import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; -import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; +import { AlertingAuthorization } from './authorization/alerting_authorization'; +import { AlertingAuthorizationAuditLogger } from './authorization/audit_logger'; import { Space } from '../../spaces/server'; export interface AlertingAuthorizationClientFactoryOpts { @@ -42,15 +42,15 @@ export class AlertingAuthorizationClientFactory { this.features = options.features; } - public create(request: KibanaRequest, exemptConsumerIds: string[] = []): AlertsAuthorization { + public create(request: KibanaRequest, exemptConsumerIds: string[] = []): AlertingAuthorization { const { securityPluginSetup, securityPluginStart, features } = this; - return new AlertsAuthorization({ + return new AlertingAuthorization({ authorization: securityPluginStart?.authz, request, getSpace: this.getSpace, alertTypeRegistry: this.alertTypeRegistry, features: features!, - auditLogger: new AlertsAuthorizationAuditLogger( + auditLogger: new AlertingAuthorizationAuditLogger( securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) ), exemptConsumerIds, diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index e9a7f572b4b36..2c71784518c9b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -47,7 +47,7 @@ import { TaskManagerStartContract } from '../../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; import { RegistryAlertType, UntypedNormalizedAlertType } from '../alert_type_registry'; import { - AlertsAuthorization, + AlertingAuthorization, WriteOperations, ReadOperations, AlertingAuthorizationEntity, @@ -82,7 +82,7 @@ export interface ConstructorOptions { logger: Logger; taskManager: TaskManagerStartContract; unsecuredSavedObjectsClient: SavedObjectsClientContract; - authorization: AlertsAuthorization; + authorization: AlertingAuthorization; actionsAuthorization: ActionsAuthorization; alertTypeRegistry: AlertTypeRegistry; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; @@ -193,7 +193,7 @@ export class AlertsClient { private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - private readonly authorization: AlertsAuthorization; + private readonly authorization: AlertingAuthorization; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: (name: string) => Promise; private readonly getActionsClient: () => Promise; diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts index 896a070db945e..bf966d38f6bc6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/aggregate.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup, setGlobalDate } from './lib'; import { AlertExecutionStatusValues } from '../../types'; @@ -24,7 +24,7 @@ const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const kibanaVersion = 'v7.10.0'; @@ -32,7 +32,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index e644d043aa255..a2d5a5e0386c4 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -10,10 +10,10 @@ import { AlertsClient, ConstructorOptions, CreateOptions } from '../alerts_clien import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -31,7 +31,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -40,7 +40,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 483bd7afded6d..0f9d91d829854 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -22,7 +22,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -31,7 +31,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index 12b2ef78980ec..7eb107c2f4dec 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { InvalidatePendingApiKey } from '../../types'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; @@ -23,7 +23,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -32,7 +32,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 39c9e40f376dc..1375a7cc5a1d7 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; @@ -24,7 +24,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -33,7 +33,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts index b71a50a24db64..8fa8ae7ae38b0 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts @@ -9,12 +9,12 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { nodeTypes } from '../../../../../../src/plugins/data/common'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -26,7 +26,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -35,7 +35,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index 4458c59083e3b..a958ea4061ae5 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -22,7 +22,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -31,7 +31,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_instance_summary.test.ts index d5a4b2d8d9446..2ef9982ba8f85 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { eventLogClientMock } from '../../../../event_log/server/mocks'; import { QueryEventsBySavedObjectResult } from '../../../../event_log/server'; @@ -27,7 +27,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const eventLogClient = eventLogClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const kibanaVersion = 'v7.10.0'; @@ -35,7 +35,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts index 8e9e6db1f8315..f9e65623f2101 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get_alert_state.test.ts @@ -9,11 +9,11 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { TaskStatus } from '../../../../task_manager/server'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; @@ -22,7 +22,7 @@ const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const kibanaVersion = 'v7.10.0'; @@ -30,7 +30,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts index 39aad2150a91b..9fe33996b9edf 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/list_alert_types.test.ts @@ -9,13 +9,13 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { - AlertsAuthorization, + AlertingAuthorization, RegistryAlertTypeWithAuth, -} from '../../authorization/alerts_authorization'; +} from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; import { RecoveredActionGroup } from '../../../common'; @@ -26,7 +26,7 @@ const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const kibanaVersion = 'v7.10.0'; @@ -34,7 +34,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 1213affa4d0c2..6734ec9b99600 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -22,7 +22,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -31,7 +31,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index 7e137fc542aeb..0fe557bcc0f07 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -22,7 +22,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -31,7 +31,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index 6656646a1b0ff..c061bc7840fb6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -22,7 +22,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -31,7 +31,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index 39352673cbe86..31c7b363b93af 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -22,7 +22,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -31,7 +31,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index db52863d125ed..c743312ef2c4b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -11,12 +11,12 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { IntervalSchedule, InvalidatePendingApiKey } from '../../types'; import { RecoveredActionGroup } from '../../../common'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { resolvable } from '../../test_utils'; import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; @@ -28,7 +28,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -37,7 +37,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 64f68824d2f86..4215f14b4a560 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -9,10 +9,10 @@ import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; -import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; -import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { httpServerMock } from '../../../../../../src/core/server/mocks'; import { auditServiceMock } from '../../../../security/server/audit/index.mock'; @@ -23,7 +23,7 @@ const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); @@ -32,7 +32,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/alerts_client_conflict_retries.test.ts index f16b16cf74dd2..98ad427d0c37b 100644 --- a/x-pack/plugins/alerting/server/alerts_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_conflict_retries.test.ts @@ -11,10 +11,10 @@ import { AlertsClient, ConstructorOptions } from './alerts_client'; import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from './authorization/alerting_authorization.mock'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { AlertingAuthorization } from './authorization/alerting_authorization'; import { ActionsAuthorization } from '../../actions/server'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; import { RetryForConflictsAttempts } from './lib/retry_if_conflicts'; @@ -32,7 +32,7 @@ const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); +const authorization = alertingAuthorizationMock.create(); const actionsAuthorization = actionsAuthorizationMock.create(); const kibanaVersion = 'v7.10.0'; @@ -41,7 +41,7 @@ const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, + authorization: (authorization as unknown) as AlertingAuthorization, actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index b88a38d5140b9..1b39af9972814 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -23,13 +23,13 @@ import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mock import { LegacyAuditLogger } from '../../security/server'; import { ALERTS_FEATURE_ID } from '../common'; import { eventLogMock } from '../../event_log/server/mocks'; -import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; +import { alertingAuthorizationMock } from './authorization/alerting_authorization.mock'; import { alertingAuthorizationClientFactoryMock } from './alerting_authorization_client_factory.mock'; -import { AlertsAuthorization } from './authorization'; +import { AlertingAuthorization } from './authorization'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; jest.mock('./alerts_client'); -jest.mock('./authorization/alerts_authorization'); +jest.mock('./authorization/alerting_authorization'); jest.mock('./authorization/audit_logger'); const savedObjectsClient = savedObjectsClientMock.create(); @@ -38,7 +38,7 @@ const savedObjectsService = savedObjectsServiceMock.createInternalStartContract( const securityPluginSetup = securityMock.createSetup(); const securityPluginStart = securityMock.createStart(); -const alertsAuthorization = alertsAuthorizationMock.create(); +const alertsAuthorization = alertingAuthorizationMock.create(); const alertingAuthorizationClientFactory = alertingAuthorizationClientFactoryMock.createFactory(); const alertsClientFactoryParams: jest.Mocked = { @@ -90,7 +90,7 @@ test('creates an alerts client with proper constructor arguments when security i savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); alertingAuthorizationClientFactory.create.mockReturnValue( - (alertsAuthorization as unknown) as AlertsAuthorization + (alertsAuthorization as unknown) as AlertingAuthorization ); const logger = { @@ -138,7 +138,7 @@ test('creates an alerts client with proper constructor arguments', async () => { savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); alertingAuthorizationClientFactory.create.mockReturnValue( - (alertsAuthorization as unknown) as AlertsAuthorization + (alertsAuthorization as unknown) as AlertingAuthorization ); factory.create(request, savedObjectsService); diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts similarity index 53% rename from x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts rename to x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts index e2789c5fe7d45..4e4cd4419a5a2 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.mock.ts @@ -6,13 +6,13 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { AlertsAuthorization } from './alerts_authorization'; +import { AlertingAuthorization } from './alerting_authorization'; -type Schema = PublicMethodsOf; -export type AlertsAuthorizationMock = jest.Mocked; +type Schema = PublicMethodsOf; +export type AlertingAuthorizationMock = jest.Mocked; -const createAlertsAuthorizationMock = () => { - const mocked: AlertsAuthorizationMock = { +const createAlertingAuthorizationMock = () => { + const mocked: AlertingAuthorizationMock = { ensureAuthorized: jest.fn(), filterByRuleTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), @@ -20,8 +20,8 @@ const createAlertsAuthorizationMock = () => { return mocked; }; -export const alertsAuthorizationMock: { - create: () => AlertsAuthorizationMock; +export const alertingAuthorizationMock: { + create: () => AlertingAuthorizationMock; } = { - create: createAlertsAuthorizationMock, + create: createAlertingAuthorizationMock, }; diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts similarity index 92% rename from x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts rename to x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 9b3427ef666b4..1b5e712a3ee69 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -14,25 +14,25 @@ import { } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; import { - AlertsAuthorization, + AlertingAuthorization, WriteOperations, ReadOperations, AlertingAuthorizationEntity, -} from './alerts_authorization'; -import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; -import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; +} from './alerting_authorization'; +import { alertingAuthorizationAuditLoggerMock } from './audit_logger.mock'; +import { AlertingAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import uuid from 'uuid'; import { RecoveredActionGroup } from '../../common'; import { RegistryAlertType } from '../alert_type_registry'; import { esKuery } from '../../../../../src/plugins/data/server'; -import { AlertingAuthorizationFilterType } from './alerts_authorization_kuery'; +import { AlertingAuthorizationFilterType } from './alerting_authorization_kuery'; const alertTypeRegistry = alertTypeRegistryMock.create(); const features: jest.Mocked = featuresPluginMock.createStart(); const request = {} as KibanaRequest; -const auditLogger = alertsAuthorizationAuditLoggerMock.create(); -const realAuditLogger = new AlertsAuthorizationAuditLogger(); +const auditLogger = alertingAuthorizationAuditLoggerMock.create(); +const realAuditLogger = new AlertingAuthorizationAuditLogger(); const getSpace = jest.fn(); @@ -174,13 +174,13 @@ const myFeatureWithoutAlerting = mockFeature('myOtherApp'); beforeEach(() => { jest.resetAllMocks(); - auditLogger.alertsAuthorizationFailure.mockImplementation((username, ...args) => + auditLogger.logAuthorizationFailure.mockImplementation((username, ...args) => realAuditLogger.getAuthorizationMessage(AuthorizationResult.Unauthorized, ...args) ); - auditLogger.alertsAuthorizationSuccess.mockImplementation((username, ...args) => + auditLogger.logAuthorizationSuccess.mockImplementation((username, ...args) => realAuditLogger.getAuthorizationMessage(AuthorizationResult.Authorized, ...args) ); - auditLogger.alertsUnscopedAuthorizationFailure.mockImplementation( + auditLogger.logUnscopedAuthorizationFailure.mockImplementation( (username, operation) => `Unauthorized ${username}/${operation}` ); alertTypeRegistry.get.mockImplementation((id) => ({ @@ -202,7 +202,7 @@ beforeEach(() => { getSpace.mockResolvedValue(undefined); }); -describe('AlertsAuthorization', () => { +describe('AlertingAuthorization', () => { describe('constructor', () => { test(`fetches the user's current space`, async () => { const space = { @@ -212,7 +212,7 @@ describe('AlertsAuthorization', () => { }; getSpace.mockResolvedValue(space); - new AlertsAuthorization({ + new AlertingAuthorization({ request, alertTypeRegistry, features, @@ -227,7 +227,7 @@ describe('AlertsAuthorization', () => { describe('ensureAuthorized', () => { test('is a no-op when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, features, @@ -249,7 +249,7 @@ describe('AlertsAuthorization', () => { test('is a no-op when the security license is disabled', async () => { const { authorization } = mockSecurity(); authorization.mode.useRbacForRequest.mockReturnValue(false); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, authorization, @@ -275,7 +275,7 @@ describe('AlertsAuthorization', () => { ReturnType > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -311,9 +311,9 @@ describe('AlertsAuthorization', () => { kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], }); - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myType", @@ -331,7 +331,7 @@ describe('AlertsAuthorization', () => { ReturnType > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -373,9 +373,9 @@ describe('AlertsAuthorization', () => { kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], }); - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myType", @@ -399,7 +399,7 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [] }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -438,9 +438,9 @@ describe('AlertsAuthorization', () => { ], }); - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myType", @@ -458,7 +458,7 @@ describe('AlertsAuthorization', () => { ReturnType > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -496,9 +496,9 @@ describe('AlertsAuthorization', () => { `"Unauthorized to create a \\"myType\\" rule for \\"myOtherApp\\""` ); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myType", @@ -516,7 +516,7 @@ describe('AlertsAuthorization', () => { ReturnType > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -554,9 +554,9 @@ describe('AlertsAuthorization', () => { `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` ); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myType", @@ -574,7 +574,7 @@ describe('AlertsAuthorization', () => { ReturnType > = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -612,9 +612,9 @@ describe('AlertsAuthorization', () => { `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myType", @@ -664,7 +664,7 @@ describe('AlertsAuthorization', () => { const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); test('omits filter when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, features, @@ -690,7 +690,7 @@ describe('AlertsAuthorization', () => { }); test('ensureAlertTypeIsAuthorized is no-op when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, features, @@ -712,8 +712,8 @@ describe('AlertsAuthorization', () => { ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule'); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); }); test('creates a filter based on the privileged types', async () => { @@ -728,7 +728,7 @@ describe('AlertsAuthorization', () => { privileges: { kibana: [] }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -755,7 +755,7 @@ describe('AlertsAuthorization', () => { ) ); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); }); test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { @@ -794,7 +794,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -821,9 +821,9 @@ describe('AlertsAuthorization', () => { `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` ); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", "myAppAlertType", @@ -871,7 +871,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -896,8 +896,8 @@ describe('AlertsAuthorization', () => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); }).not.toThrow(); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); }); test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { @@ -949,7 +949,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -976,13 +976,13 @@ describe('AlertsAuthorization', () => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); }).not.toThrow(); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); logSuccessfulAuthorization(); - expect(auditLogger.alertsBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(auditLogger.logBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", Array [ @@ -1029,7 +1029,7 @@ describe('AlertsAuthorization', () => { const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); test('augments a list of types with all features when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, features, @@ -1108,7 +1108,7 @@ describe('AlertsAuthorization', () => { }); test('augments a list of types with all features and exempt consumer ids when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, features, @@ -1238,7 +1238,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -1341,7 +1341,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -1435,7 +1435,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -1539,7 +1539,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, @@ -1646,7 +1646,7 @@ describe('AlertsAuthorization', () => { }, }); - const alertAuthorization = new AlertsAuthorization({ + const alertAuthorization = new AlertingAuthorization({ request, authorization, alertTypeRegistry, diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts similarity index 95% rename from x-pack/plugins/alerting/server/authorization/alerts_authorization.ts rename to x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 05abc46e75f67..8095de927e0cd 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -12,12 +12,12 @@ import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; +import { AlertingAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; import { asFiltersByRuleTypeAndConsumer, AlertingAuthorizationFilterOpts, -} from './alerts_authorization_kuery'; +} from './alerting_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; @@ -68,16 +68,16 @@ export interface ConstructorOptions { request: KibanaRequest; features: FeaturesPluginStart; getSpace: (request: KibanaRequest) => Promise; - auditLogger: AlertsAuthorizationAuditLogger; + auditLogger: AlertingAuthorizationAuditLogger; exemptConsumerIds: string[]; authorization?: SecurityPluginSetup['authz']; } -export class AlertsAuthorization { +export class AlertingAuthorization { private readonly alertTypeRegistry: AlertTypeRegistry; private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; - private readonly auditLogger: AlertsAuthorizationAuditLogger; + private readonly auditLogger: AlertingAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; private readonly exemptConsumerIds: string[]; @@ -183,7 +183,7 @@ export class AlertsAuthorization { * This check will ensure we don't accidentally let these through */ throw Boom.forbidden( - this.auditLogger.alertsAuthorizationFailure( + this.auditLogger.logAuthorizationFailure( username, ruleTypeId, ScopeType.Consumer, @@ -195,7 +195,7 @@ export class AlertsAuthorization { } if (hasAllRequested) { - this.auditLogger.alertsAuthorizationSuccess( + this.auditLogger.logAuthorizationSuccess( username, ruleTypeId, ScopeType.Consumer, @@ -219,7 +219,7 @@ export class AlertsAuthorization { : [ScopeType.Producer, ruleType.producer]; throw Boom.forbidden( - this.auditLogger.alertsAuthorizationFailure( + this.auditLogger.logAuthorizationFailure( username, ruleTypeId, unauthorizedScopeType, @@ -231,7 +231,7 @@ export class AlertsAuthorization { } } else if (!isAvailableConsumer) { throw Boom.forbidden( - this.auditLogger.alertsAuthorizationFailure( + this.auditLogger.logAuthorizationFailure( '', ruleTypeId, ScopeType.Consumer, @@ -260,11 +260,7 @@ export class AlertsAuthorization { if (!authorizedRuleTypes.size) { throw Boom.forbidden( - this.auditLogger.alertsUnscopedAuthorizationFailure( - username!, - 'find', - authorizationEntity - ) + this.auditLogger.logUnscopedAuthorizationFailure(username!, 'find', authorizationEntity) ); } @@ -283,7 +279,7 @@ export class AlertsAuthorization { ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { throw Boom.forbidden( - this.auditLogger.alertsAuthorizationFailure( + this.auditLogger.logAuthorizationFailure( username!, ruleTypeId, ScopeType.Consumer, @@ -302,7 +298,7 @@ export class AlertsAuthorization { }, logSuccessfulAuthorization: () => { if (authorizedEntries.size) { - this.auditLogger.alertsBulkAuthorizationSuccess( + this.auditLogger.logBulkAuthorizationSuccess( username!, [...authorizedEntries.entries()].reduce>( (authorizedPairs, [alertTypeId, consumers]) => { diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts similarity index 99% rename from x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts rename to x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts index a3fedfcf9b8f5..8a558b6427383 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts @@ -10,7 +10,7 @@ import { AlertingAuthorizationFilterType, asFiltersByRuleTypeAndConsumer, ensureFieldIsSafeForQuery, -} from './alerts_authorization_kuery'; +} from './alerting_authorization_kuery'; import { esKuery } from '../../../../../src/plugins/data/server'; describe('asKqlFiltersByRuleTypeAndConsumer', () => { diff --git a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts similarity index 97% rename from x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts rename to x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index 3e2db40ba5fef..eb6f1605f2ba5 100644 --- a/x-pack/plugins/alerting/server/authorization/alerts_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -10,7 +10,7 @@ import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; import { nodeBuilder, EsQueryConfig } from '../../../../../src/plugins/data/common'; import { toElasticsearchQuery } from '../../../../../src/plugins/data/common/es_query'; import { KueryNode } from '../../../../../src/plugins/data/server'; -import { RegistryAlertTypeWithAuth } from './alerts_authorization'; +import { RegistryAlertTypeWithAuth } from './alerting_authorization'; export enum AlertingAuthorizationFilterType { KQL = 'kql', diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.mock.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.mock.ts index 01e1821512606..30fa6c9ef1e28 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.mock.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.mock.ts @@ -5,21 +5,21 @@ * 2.0. */ -import { AlertsAuthorizationAuditLogger } from './audit_logger'; +import { AlertingAuthorizationAuditLogger } from './audit_logger'; -const createAlertsAuthorizationAuditLoggerMock = () => { +const createAlertingAuthorizationAuditLoggerMock = () => { const mocked = ({ getAuthorizationMessage: jest.fn(), - alertsAuthorizationFailure: jest.fn(), - alertsUnscopedAuthorizationFailure: jest.fn(), - alertsAuthorizationSuccess: jest.fn(), - alertsBulkAuthorizationSuccess: jest.fn(), - } as unknown) as jest.Mocked; + logAuthorizationFailure: jest.fn(), + logUnscopedAuthorizationFailure: jest.fn(), + logAuthorizationSuccess: jest.fn(), + logBulkAuthorizationSuccess: jest.fn(), + } as unknown) as jest.Mocked; return mocked; }; -export const alertsAuthorizationAuditLoggerMock: { - create: () => jest.Mocked; +export const alertingAuthorizationAuditLoggerMock: { + create: () => jest.Mocked; } = { - create: createAlertsAuthorizationAuditLoggerMock, + create: createAlertingAuthorizationAuditLoggerMock, }; diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts index addcff7611072..7b0ffdb193c83 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; +import { AlertingAuthorizationAuditLogger, ScopeType } from './audit_logger'; const createMockAuditLogger = () => { return { @@ -15,7 +15,7 @@ const createMockAuditLogger = () => { describe(`#constructor`, () => { test('initializes a noop auditLogger if security logger is unavailable', () => { - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(undefined); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(undefined); const username = 'foo-user'; const alertTypeId = 'alert-type-id'; @@ -24,7 +24,7 @@ describe(`#constructor`, () => { const operation = 'create'; const entity = 'rule'; expect(() => { - alertsAuditLogger.alertsAuthorizationFailure( + alertsAuditLogger.logAuthorizationFailure( username, alertTypeId, scopeType, @@ -33,7 +33,7 @@ describe(`#constructor`, () => { entity ); - alertsAuditLogger.alertsAuthorizationSuccess( + alertsAuditLogger.logAuthorizationSuccess( username, alertTypeId, scopeType, @@ -45,19 +45,19 @@ describe(`#constructor`, () => { }); }); -describe(`#alertsUnscopedAuthorizationFailure`, () => { +describe(`#logUnscopedAuthorizationFailure`, () => { test('logs auth failure of operation', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation, entity); + alertsAuditLogger.logUnscopedAuthorizationFailure(username, operation, entity); expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_unscoped_authorization_failure", + "alerting_unscoped_authorization_failure", "foo-user Unauthorized to create rules for any rule types", Object { "operation": "create", @@ -69,7 +69,7 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { test('logs auth failure with producer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const alertTypeId = 'alert-type-id'; const scopeType = ScopeType.Producer; @@ -77,7 +77,7 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsAuthorizationFailure( + alertsAuditLogger.logAuthorizationFailure( username, alertTypeId, scopeType, @@ -88,7 +88,7 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_failure", + "alerting_authorization_failure", "foo-user Unauthorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", @@ -103,10 +103,10 @@ describe(`#alertsUnscopedAuthorizationFailure`, () => { }); }); -describe(`#alertsAuthorizationFailure`, () => { +describe(`#logAuthorizationFailure`, () => { test('logs auth failure with consumer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const alertTypeId = 'alert-type-id'; const scopeType = ScopeType.Consumer; @@ -114,7 +114,7 @@ describe(`#alertsAuthorizationFailure`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsAuthorizationFailure( + alertsAuditLogger.logAuthorizationFailure( username, alertTypeId, scopeType, @@ -125,7 +125,7 @@ describe(`#alertsAuthorizationFailure`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_failure", + "alerting_authorization_failure", "foo-user Unauthorized to create a \\"alert-type-id\\" rule for \\"myApp\\"", Object { "alertTypeId": "alert-type-id", @@ -141,7 +141,7 @@ describe(`#alertsAuthorizationFailure`, () => { test('logs auth failure with producer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const alertTypeId = 'alert-type-id'; const scopeType = ScopeType.Producer; @@ -149,7 +149,7 @@ describe(`#alertsAuthorizationFailure`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsAuthorizationFailure( + alertsAuditLogger.logAuthorizationFailure( username, alertTypeId, scopeType, @@ -160,7 +160,7 @@ describe(`#alertsAuthorizationFailure`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_failure", + "alerting_authorization_failure", "foo-user Unauthorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", @@ -175,10 +175,10 @@ describe(`#alertsAuthorizationFailure`, () => { }); }); -describe(`#alertsBulkAuthorizationSuccess`, () => { +describe(`#logBulkAuthorizationSuccess`, () => { test('logs auth success with consumer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const scopeType = ScopeType.Consumer; const authorizedEntries: Array<[string, string]> = [ @@ -188,7 +188,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsBulkAuthorizationSuccess( + alertsAuditLogger.logBulkAuthorizationSuccess( username, authorizedEntries, scopeType, @@ -198,7 +198,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_success", + "alerting_authorization_success", "foo-user Authorized to create: \\"alert-type-id\\" rules for \\"myApp\\", \\"other-alert-type-id\\" rules for \\"myOtherApp\\"", Object { "authorizedEntries": Array [ @@ -222,7 +222,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { test('logs auth success with producer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const scopeType = ScopeType.Producer; const authorizedEntries: Array<[string, string]> = [ @@ -232,7 +232,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsBulkAuthorizationSuccess( + alertsAuditLogger.logBulkAuthorizationSuccess( username, authorizedEntries, scopeType, @@ -242,7 +242,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_success", + "alerting_authorization_success", "foo-user Authorized to create: \\"alert-type-id\\" rules by \\"myApp\\", \\"other-alert-type-id\\" rules by \\"myOtherApp\\"", Object { "authorizedEntries": Array [ @@ -268,7 +268,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { describe(`#savedObjectsAuthorizationSuccess`, () => { test('logs auth success with consumer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const alertTypeId = 'alert-type-id'; const scopeType = ScopeType.Consumer; @@ -276,7 +276,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsAuthorizationSuccess( + alertsAuditLogger.logAuthorizationSuccess( username, alertTypeId, scopeType, @@ -287,7 +287,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_success", + "alerting_authorization_success", "foo-user Authorized to create a \\"alert-type-id\\" rule for \\"myApp\\"", Object { "alertTypeId": "alert-type-id", @@ -303,7 +303,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { test('logs auth success with producer scope', () => { const auditLogger = createMockAuditLogger(); - const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const alertsAuditLogger = new AlertingAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const alertTypeId = 'alert-type-id'; const scopeType = ScopeType.Producer; @@ -311,7 +311,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { const operation = 'create'; const entity = 'rule'; - alertsAuditLogger.alertsAuthorizationSuccess( + alertsAuditLogger.logAuthorizationSuccess( username, alertTypeId, scopeType, @@ -322,7 +322,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "alerts_authorization_success", + "alerting_authorization_success", "foo-user Authorized to create a \\"alert-type-id\\" rule by \\"myOtherApp\\"", Object { "alertTypeId": "alert-type-id", diff --git a/x-pack/plugins/alerting/server/authorization/audit_logger.ts b/x-pack/plugins/alerting/server/authorization/audit_logger.ts index 7aa6c299a3848..2a0c851733800 100644 --- a/x-pack/plugins/alerting/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerting/server/authorization/audit_logger.ts @@ -17,7 +17,7 @@ export enum AuthorizationResult { Authorized = 'Authorized', } -export class AlertsAuthorizationAuditLogger { +export class AlertingAuthorizationAuditLogger { private readonly auditLogger: LegacyAuditLogger; constructor(auditLogger: LegacyAuditLogger = { log() {} }) { @@ -37,7 +37,7 @@ export class AlertsAuthorizationAuditLogger { }`; } - public alertsAuthorizationFailure( + public logAuthorizationFailure( username: string, alertTypeId: string, scopeType: ScopeType, @@ -53,7 +53,7 @@ export class AlertsAuthorizationAuditLogger { operation, entity ); - this.auditLogger.log('alerts_authorization_failure', `${username} ${message}`, { + this.auditLogger.log('alerting_authorization_failure', `${username} ${message}`, { username, alertTypeId, scopeType, @@ -64,20 +64,20 @@ export class AlertsAuthorizationAuditLogger { return message; } - public alertsUnscopedAuthorizationFailure( + public logUnscopedAuthorizationFailure( username: string, operation: string, entity: string ): string { const message = `Unauthorized to ${operation} ${entity}s for any rule types`; - this.auditLogger.log('alerts_unscoped_authorization_failure', `${username} ${message}`, { + this.auditLogger.log('alerting_unscoped_authorization_failure', `${username} ${message}`, { username, operation, }); return message; } - public alertsAuthorizationSuccess( + public logAuthorizationSuccess( username: string, alertTypeId: string, scopeType: ScopeType, @@ -93,7 +93,7 @@ export class AlertsAuthorizationAuditLogger { operation, entity ); - this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, { + this.auditLogger.log('alerting_authorization_success', `${username} ${message}`, { username, alertTypeId, scopeType, @@ -104,7 +104,7 @@ export class AlertsAuthorizationAuditLogger { return message; } - public alertsBulkAuthorizationSuccess( + public logBulkAuthorizationSuccess( username: string, authorizedEntries: Array<[string, string]>, scopeType: ScopeType, @@ -119,7 +119,7 @@ export class AlertsAuthorizationAuditLogger { }` ) .join(', ')}`; - this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, { + this.auditLogger.log('alerting_authorization_success', `${username} ${message}`, { username, scopeType, authorizedEntries, diff --git a/x-pack/plugins/alerting/server/authorization/index.ts b/x-pack/plugins/alerting/server/authorization/index.ts index 06bfe5e99f572..17f0f9f22bbe1 100644 --- a/x-pack/plugins/alerting/server/authorization/index.ts +++ b/x-pack/plugins/alerting/server/authorization/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './alerts_authorization'; -export * from './alerts_authorization_kuery'; +export * from './alerting_authorization'; +export * from './alerting_authorization_kuery'; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 482910d2af904..990733c320dfe 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -68,7 +68,7 @@ import { import { AlertsConfig } from './config'; import { getHealth } from './health/get_health'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; -import { AlertsAuthorization } from './authorization'; +import { AlertingAuthorization } from './authorization'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -105,7 +105,9 @@ export interface PluginSetupContract { export interface PluginStartContract { listTypes: AlertTypeRegistry['list']; getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf; - getAlertingAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; + getAlertingAuthorizationWithRequest( + request: KibanaRequest + ): PublicMethodsOf; getFrameworkHealth: () => Promise; } From a04dc1e482951efbfabaa7a903b9f271b86f2c61 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Wed, 12 May 2021 13:18:26 -0400 Subject: [PATCH 022/186] Fixing unit tests --- .../actions/__snapshots__/alerting.test.ts.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap index 9739af7bcde55..107c708c4811f 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap @@ -1,16 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#get alertingType of "" throws error 1`] = `"alertingType is required and must be a string"`; +exports[`#get alertingType of "" throws error 1`] = `"alertingEntity is required and must be a string"`; -exports[`#get alertingType of {} throws error 1`] = `"alertingType is required and must be a string"`; +exports[`#get alertingType of {} throws error 1`] = `"alertingEntity is required and must be a string"`; -exports[`#get alertingType of 1 throws error 1`] = `"alertingType is required and must be a string"`; +exports[`#get alertingType of 1 throws error 1`] = `"alertingEntity is required and must be a string"`; -exports[`#get alertingType of null throws error 1`] = `"alertingType is required and must be a string"`; +exports[`#get alertingType of null throws error 1`] = `"alertingEntity is required and must be a string"`; -exports[`#get alertingType of true throws error 1`] = `"alertingType is required and must be a string"`; +exports[`#get alertingType of true throws error 1`] = `"alertingEntity is required and must be a string"`; -exports[`#get alertingType of undefined throws error 1`] = `"alertingType is required and must be a string"`; +exports[`#get alertingType of undefined throws error 1`] = `"alertingEntity is required and must be a string"`; exports[`#get consumer of "" throws error 1`] = `"consumer is required and must be a string"`; From 2de423d70953e39267f2074094132b0f4fcdd382 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 13 May 2021 11:03:16 -0400 Subject: [PATCH 023/186] Changing schema of alerting feature privilege --- .../alerting_example/server/plugin.ts | 14 +++- x-pack/plugins/apm/server/feature.ts | 14 +++- .../common/feature_kibana_privileges.ts | 65 +++++++++++++------ .../plugins/features/server/feature_schema.ts | 51 ++++++--------- x-pack/plugins/infra/server/features.ts | 24 +++++-- .../plugins/ml/common/types/capabilities.ts | 16 +++-- x-pack/plugins/monitoring/server/plugin.ts | 7 +- .../security_solution/server/plugin.ts | 14 +++- x-pack/plugins/uptime/server/kibana.index.ts | 14 +++- 9 files changed, 150 insertions(+), 69 deletions(-) diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts index f6131679874db..2420be798ec84 100644 --- a/x-pack/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -44,7 +44,12 @@ export class AlertingExamplePlugin implements Plugin unseenAlertTypes.delete(privilegeAlertTypes)); read.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index aa2c628a23ddd..0fd06c70d2c73 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -34,7 +34,10 @@ export const METRICS_FEATURE = { read: ['index-pattern'], }, alerting: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + all: { + rule: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + alert: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -50,7 +53,10 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + read: { + rule: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + alert: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -83,7 +89,12 @@ export const LOGS_FEATURE = { read: [], }, alerting: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + rule: { + all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, + alert: { + all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -95,7 +106,12 @@ export const LOGS_FEATURE = { catalogue: ['infralogging', 'logs'], api: ['infra'], alerting: { - read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + rule: { + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, + alert: { + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 1e6a76caf70e9..3545a85305c17 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -117,8 +117,12 @@ export function getPluginPrivileges() { read: savedObjects, }, alerting: { - all: Object.values(ML_ALERT_TYPES), - read: [], + rule: { + all: Object.values(ML_ALERT_TYPES), + }, + alert: { + all: Object.values(ML_ALERT_TYPES), + }, }, }, user: { @@ -132,8 +136,12 @@ export function getPluginPrivileges() { read: savedObjects, }, alerting: { - all: [], - read: Object.values(ML_ALERT_TYPES), + rule: { + read: Object.values(ML_ALERT_TYPES), + }, + alert: { + read: Object.values(ML_ALERT_TYPES), + }, }, }, apmUser: { diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 56c654963d340..10724594ce576 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -262,7 +262,12 @@ export class MonitoringPlugin read: [], }, alerting: { - all: ALERTS, + rule: { + all: ALERTS, + }, + alert: { + all: ALERTS, + }, }, ui: [], }, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 158c2e94b2d7a..efeabc844a810 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -237,7 +237,12 @@ export class Plugin implements IPlugin Date: Thu, 13 May 2021 11:38:47 -0400 Subject: [PATCH 024/186] Changing schema of alerting feature privilege --- x-pack/plugins/stack_alerts/server/feature.ts | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 3455cdb05e833..2638405470260 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -6,13 +6,14 @@ */ import { i18n } from '@kbn/i18n'; +import { KibanaFeatureConfig } from '../../../plugins/features/common'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; -export const BUILT_IN_ALERTS_FEATURE = { +export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { id: STACK_ALERTS_FEATURE_ID, name: i18n.translate('xpack.stackAlerts.featureRegistry.actionsFeatureName', { defaultMessage: 'Stack Rules', @@ -31,13 +32,11 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: { - rule: [IndexThreshold, GeoContainment, ElasticsearchQuery], - alert: [], + rule: { + all: [IndexThreshold, GeoContainment, ElasticsearchQuery], }, - read: { - rule: [], - alert: [IndexThreshold, GeoContainment, ElasticsearchQuery], + alert: { + read: [IndexThreshold, GeoContainment, ElasticsearchQuery], }, }, savedObject: { @@ -54,13 +53,11 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: { - rule: [], - alert: [], + rule: { + read: [IndexThreshold, GeoContainment, ElasticsearchQuery], }, - read: { - rule: [IndexThreshold, GeoContainment, ElasticsearchQuery], - alert: [IndexThreshold, GeoContainment, ElasticsearchQuery], + alert: { + read: [IndexThreshold, GeoContainment, ElasticsearchQuery], }, }, savedObject: { @@ -83,13 +80,8 @@ export const BUILT_IN_ALERTS_FEATURE = { name: 'Manage Alerts', includeIn: 'all', alerting: { - all: { - rule: [], - alert: [IndexThreshold, GeoContainment, ElasticsearchQuery], - }, - read: { - rule: [], - alert: [], + alert: { + all: [IndexThreshold, GeoContainment, ElasticsearchQuery], }, }, savedObject: { From 804fa5df79d76773f34dac1a5e8fccc732c257c4 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 13 May 2021 12:38:44 -0400 Subject: [PATCH 025/186] Updating feature privilege iterator --- .../feature_privilege_iterator.ts | 74 +++++-------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index b4a1c968a4c26..20dbaaaebeacb 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -81,61 +81,27 @@ function mergeWithSubFeatures( subFeaturePrivilege.savedObject.read ); - let all: string[] = []; - let read: string[] = []; - if (Array.isArray(mergedConfig.alerting?.all)) { - all = mergedConfig.alerting?.all ?? []; - } else { - const allObject = mergedConfig.alerting?.all as { - rule?: readonly string[]; - alert?: readonly string[]; - }; - const rule = allObject?.rule ?? []; - const alert = allObject?.alert ?? []; - all = [...rule, ...alert]; - } - - if (Array.isArray(mergedConfig.alerting?.read)) { - read = mergedConfig.alerting?.read ?? []; - } else { - const readObject = mergedConfig.alerting?.read as { - rule?: readonly string[]; - alert?: readonly string[]; - }; - const rule = readObject?.rule ?? []; - const alert = readObject?.alert ?? []; - read = [...rule, ...alert]; - } - - let subfeatureAll: string[] = []; - let subfeatureRead: string[] = []; - if (Array.isArray(subFeaturePrivilege.alerting?.all)) { - subfeatureAll = subFeaturePrivilege.alerting?.all ?? []; - } else { - const allObject = subFeaturePrivilege.alerting?.all as { - rule?: readonly string[]; - alert?: readonly string[]; - }; - const rule = allObject?.rule ?? []; - const alert = allObject?.alert ?? []; - subfeatureAll = [...rule, ...alert]; - } - - if (Array.isArray(subFeaturePrivilege.alerting?.read)) { - subfeatureRead = subFeaturePrivilege.alerting?.read ?? []; - } else { - const readObject = subFeaturePrivilege.alerting?.read as { - rule?: readonly string[]; - alert?: readonly string[]; - }; - const rule = readObject?.rule ?? []; - const alert = readObject?.alert ?? []; - subfeatureRead = [...rule, ...alert]; - } - mergedConfig.alerting = { - all: mergeArrays(all ?? [], subfeatureAll ?? []), - read: mergeArrays(read ?? [], subfeatureRead ?? []), + rule: { + all: mergeArrays( + mergedConfig.alerting?.rule?.all ?? [], + subFeaturePrivilege.alerting?.rule?.all ?? [] + ), + read: mergeArrays( + mergedConfig.alerting?.rule?.read ?? [], + subFeaturePrivilege.alerting?.rule?.read ?? [] + ), + }, + alert: { + all: mergeArrays( + mergedConfig.alerting?.alert?.all ?? [], + subFeaturePrivilege.alerting?.alert?.all ?? [] + ), + read: mergeArrays( + mergedConfig.alerting?.alert?.read ?? [], + subFeaturePrivilege.alerting?.alert?.read ?? [] + ), + }, }; } return mergedConfig; From d4baa3dca5674919665703b58969817406615e2e Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 14 May 2021 10:25:13 -0400 Subject: [PATCH 026/186] Updating feature privilege builder --- x-pack/plugins/infra/server/features.ts | 16 +++-- .../feature_privilege_builder/alerting.ts | 61 +++++-------------- 2 files changed, 25 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 0fd06c70d2c73..91f82e82b33cd 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -34,9 +34,11 @@ export const METRICS_FEATURE = { read: ['index-pattern'], }, alerting: { - all: { - rule: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], - alert: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + rule: { + all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, + alert: { + all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, }, management: { @@ -53,9 +55,11 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - read: { - rule: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], - alert: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + rule: { + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, + alert: { + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, }, management: { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 3871e10f8690a..78f598500612d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -5,22 +5,22 @@ * 2.0. */ -import { uniq } from 'lodash'; +import { get, uniq } from 'lodash'; import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; -enum AlertingType { +enum AlertingEntity { RULE = 'rule', ALERT = 'alert', } -const readOperations: Record = { +const readOperations: Record = { rule: ['get', 'getAlertState', 'getAlertInstanceSummary', 'find'], alert: ['get', 'find'], }; -const writeOperations: Record = { +const writeOperations: Record = { rule: [ 'create', 'delete', @@ -35,7 +35,7 @@ const writeOperations: Record = { ], alert: ['update'], }; -const allOperations: Record = { +const allOperations: Record = { rule: [...readOperations.rule, ...writeOperations.rule], alert: [...readOperations.alert, ...writeOperations.alert], }; @@ -57,50 +57,19 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder ) ); - let ruleAll: string[] = []; - let ruleRead: string[] = []; - let alertAll: string[] = []; - let alertRead: string[] = []; - if (Array.isArray(privilegeDefinition.alerting?.all)) { - ruleAll = [...(privilegeDefinition.alerting?.all ?? [])]; - alertAll = [...(privilegeDefinition.alerting?.all ?? [])]; - } else { - const allObject = privilegeDefinition.alerting?.all as { - rule?: readonly string[]; - alert?: readonly string[]; - }; - const rule = allObject?.rule ?? []; - const alert = allObject?.alert ?? []; - ruleAll = [...rule]; - alertAll = [...alert]; - } + const getPrivilegesForEntity = (entity: AlertingEntity) => { + const all = get(privilegeDefinition.alerting, `${entity}.all`) ?? []; + const read = get(privilegeDefinition.alerting, `${entity}.read`) ?? []; - if (Array.isArray(privilegeDefinition.alerting?.read)) { - ruleRead = [...(privilegeDefinition.alerting?.read ?? [])]; - alertRead = [...(privilegeDefinition.alerting?.read ?? [])]; - } else { - const readObject = privilegeDefinition.alerting?.read as { - rule?: readonly string[]; - alert?: readonly string[]; - }; - const rule = readObject?.rule ?? []; - const alert = readObject?.alert ?? []; - ruleRead = [...rule]; - alertRead = [...alert]; - } - - if (feature.id === 'stackAlerts') { - console.log(`ruleAll ${ruleAll}`); - console.log(`ruleRead ${ruleRead}`); - console.log(`alertAll ${alertAll}`); - console.log(`alertRead ${alertRead}`); - } + return uniq([ + ...getAlertingPrivilege(allOperations[entity], all, entity, feature.id), + ...getAlertingPrivilege(readOperations[entity], read, entity, feature.id), + ]); + }; return uniq([ - ...getAlertingPrivilege(allOperations.rule, ruleAll, 'rule', feature.id), - ...getAlertingPrivilege(allOperations.alert, alertAll, 'alert', feature.id), - ...getAlertingPrivilege(readOperations.rule, ruleRead, 'rule', feature.id), - ...getAlertingPrivilege(readOperations.alert, alertRead, 'alert', feature.id), + ...getPrivilegesForEntity(AlertingEntity.RULE), + ...getPrivilegesForEntity(AlertingEntity.ALERT), ]); } } From 36f7e9f4aec25bfe7671b9ee4a5e443356f3a888 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 14 May 2021 14:51:33 -0400 Subject: [PATCH 027/186] Fixing types check --- .../alerting_authorization.test.ts | 16 +- .../features/server/feature_registry.test.ts | 250 +++++++++++++++++- .../alerting.test.ts | 28 +- .../feature_privilege_iterator.test.ts | 158 +++++++---- .../authorization/privileges/privileges.ts | 1 - .../stack_alerts/server/feature.test.ts | 8 +- .../fixtures/plugins/alerts/server/plugin.ts | 64 ++--- .../alerts_restricted/server/plugin.ts | 8 +- .../fixtures/plugins/alerts/server/plugin.ts | 8 +- 9 files changed, 430 insertions(+), 111 deletions(-) diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 1b5e712a3ee69..172fc6ea1cc2f 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -71,7 +71,9 @@ function mockFeature(appName: string, typeName?: string) { ...(typeName ? { alerting: { - all: [typeName], + rule: { + all: [typeName], + }, }, } : {}), @@ -85,7 +87,9 @@ function mockFeature(appName: string, typeName?: string) { ...(typeName ? { alerting: { - read: [typeName], + rule: { + read: [typeName], + }, }, } : {}), @@ -138,7 +142,9 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) { name: 'sub feature alert', includeIn: 'all', alerting: { - all: [typeName], + rule: { + all: [typeName], + }, }, savedObject: { all: [], @@ -151,7 +157,9 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) { name: 'sub feature alert', includeIn: 'read', alerting: { - read: [typeName], + rule: { + read: [typeName], + }, }, savedObject: { all: [], diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index eb9b35cc644a7..827773c7d7c5a 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -827,7 +827,7 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { + it(`prevents privileges from specifying alerting/rule entries that don't exist at the root level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -837,8 +837,57 @@ describe('FeatureRegistry', () => { privileges: { all: { alerting: { - all: ['foo', 'bar'], - read: ['baz'], + rule: { + all: ['foo', 'bar'], + read: ['baz'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { + rule: { + read: ['foo', 'bar', 'baz'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents privileges from specifying alerting/alert entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['bar'], + privileges: { + all: { + alerting: { + alert: { + all: ['foo', 'bar'], + read: ['baz'], + }, }, savedObject: { all: [], @@ -848,7 +897,11 @@ describe('FeatureRegistry', () => { app: [], }, read: { - alerting: { read: ['foo', 'bar', 'baz'] }, + alerting: { + alert: { + read: ['foo', 'bar', 'baz'], + }, + }, savedObject: { all: [], read: [], @@ -868,7 +921,80 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { + it(`prevents features from specifying alerting/rule entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['foo', 'bar', 'baz'], + privileges: { + all: { + alerting: { + rule: { + all: ['foo'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { + rule: { + all: ['foo'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + alerting: { + rule: { + all: ['bar'], + }, + }, + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents features from specifying alerting/alert entries that don't exist at the privilege level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -877,7 +1003,11 @@ describe('FeatureRegistry', () => { alerting: ['foo', 'bar', 'baz'], privileges: { all: { - alerting: { all: ['foo'] }, + alerting: { + alert: { + all: ['foo'], + }, + }, savedObject: { all: [], read: [], @@ -886,7 +1016,11 @@ describe('FeatureRegistry', () => { app: [], }, read: { - alerting: { all: ['foo'] }, + alerting: { + alert: { + all: ['foo'], + }, + }, savedObject: { all: [], read: [], @@ -911,7 +1045,11 @@ describe('FeatureRegistry', () => { read: [], }, ui: [], - alerting: { all: ['bar'] }, + alerting: { + alert: { + all: ['bar'], + }, + }, }, ], }, @@ -929,7 +1067,47 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { + it(`prevents reserved privileges from specifying alerting/rule entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { + rule: { + all: ['foo', 'bar', 'baz'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents reserved privileges from specifying alerting/alert entries that don't exist at the root level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -943,7 +1121,11 @@ describe('FeatureRegistry', () => { { id: 'reserved', privilege: { - alerting: { all: ['foo', 'bar', 'baz'] }, + alerting: { + alert: { + all: ['foo', 'bar', 'baz'], + }, + }, savedObject: { all: [], read: [], @@ -965,7 +1147,47 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { + it(`prevents features from specifying alerting/rule entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { + rule: { + all: ['foo', 'bar'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents features from specifying alerting/alert entries that don't exist at the reserved privilege level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -979,7 +1201,11 @@ describe('FeatureRegistry', () => { { id: 'reserved', privilege: { - alerting: { all: ['foo', 'bar'] }, + alerting: { + alert: { + all: ['foo', 'bar'], + }, + }, savedObject: { all: [], read: [], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 1226489c6026c..a02e5eac171e4 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -20,8 +20,14 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - all: [], - read: [], + rule: { + all: [], + read: [], + }, + alert: { + all: [], + read: [], + }, }, savedObject: { @@ -52,8 +58,10 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - all: [], - read: ['alert-type'], + rule: { + all: [], + read: ['alert-type'], + }, }, savedObject: { @@ -92,8 +100,10 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - all: ['alert-type'], - read: [], + rule: { + all: ['alert-type'], + read: [], + }, }, savedObject: { @@ -143,8 +153,10 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - all: ['alert-type'], - read: ['readonly-alert-type'], + rule: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, }, savedObject: { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts index f42ac3ef21c06..95da68af0120d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -46,8 +46,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -63,7 +65,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -93,8 +97,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -113,7 +119,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -139,8 +147,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -156,7 +166,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -187,8 +199,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -212,11 +226,13 @@ describe('featurePrivilegeIterator', () => { }, savedObject: { all: ['all-type'], - read: ['read-type'], + read: [], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -232,7 +248,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -259,8 +277,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -293,8 +313,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -313,7 +335,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -340,8 +364,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -357,7 +383,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -384,8 +412,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -465,8 +495,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -482,7 +514,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -510,8 +544,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -594,8 +630,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -611,7 +649,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -638,7 +678,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -719,8 +761,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -736,7 +780,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -764,8 +810,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -846,8 +894,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -863,7 +913,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -892,8 +944,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -999,8 +1053,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -1083,8 +1139,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -1100,7 +1158,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index b9a874c3ca06e..4ac8b3d8fc584 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -100,7 +100,6 @@ export function privilegesFactory( } } - console.log(`stackAlerts: ${JSON.stringify(featurePrivileges.stackAlerts)}`); return { features: featurePrivileges, global: { diff --git a/x-pack/plugins/stack_alerts/server/feature.test.ts b/x-pack/plugins/stack_alerts/server/feature.test.ts index 62807f1c10a1c..3dde7fd09f347 100644 --- a/x-pack/plugins/stack_alerts/server/feature.test.ts +++ b/x-pack/plugins/stack_alerts/server/feature.test.ts @@ -20,9 +20,11 @@ describe('Stack Alerts Feature Privileges', () => { const featuresSetup = featuresPluginMock.createSetup(); await plugin.setup(coreSetup, { alerting: alertingSetup, features: featuresSetup }); - const typesInFeaturePrivilege = BUILT_IN_ALERTS_FEATURE.alerting; - const typesInFeaturePrivilegeAll = BUILT_IN_ALERTS_FEATURE.privileges.all.alerting.all; - const typesInFeaturePrivilegeRead = BUILT_IN_ALERTS_FEATURE.privileges.read.alerting.read; + const typesInFeaturePrivilege = BUILT_IN_ALERTS_FEATURE.alerting ?? []; + const typesInFeaturePrivilegeAll = + BUILT_IN_ALERTS_FEATURE.privileges?.all?.alerting?.rule?.all ?? []; + const typesInFeaturePrivilegeRead = + BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read ?? []; expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilege.length); expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilegeAll.length); expect(alertingSetup.registerType.mock.calls.length).toEqual( diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 9a7cd8d333b44..e98b7af075d64 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -69,21 +69,23 @@ export class FixturePlugin implements Plugin Date: Thu, 13 May 2021 10:35:03 -0500 Subject: [PATCH 028/186] Clearing the global search bar will reset suggestions (#88637) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../global_search_bar/public/components/search_bar.test.tsx | 4 +--- .../global_search_bar/public/components/search_bar.tsx | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index 94855ab0aa4ab..c8bd54540e6a6 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -66,9 +66,7 @@ describe('SearchBar', () => { }; const simulateTypeChar = async (text: string) => { - await waitFor(() => - getSearchProps(component).onKeyUpCapture({ currentTarget: { value: text } }) - ); + await waitFor(() => getSearchProps(component).onInput({ currentTarget: { value: text } })); }; const getDisplayedOptionsTitle = () => { diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index dfe73512279ed..5234b4e7b0ad5 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -398,8 +398,7 @@ export function SearchBar({ } searchProps={{ - onKeyUpCapture: (e: React.KeyboardEvent) => - setSearchValue(e.currentTarget.value), + onInput: (e: React.UIEvent) => setSearchValue(e.currentTarget.value), 'data-test-subj': 'nav-search-input', inputRef: setSearchRef, compressed: true, From 223696bde513a6837998c144c00fde3865b1b381 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 13 May 2021 12:25:29 -0400 Subject: [PATCH 029/186] Gracefully handle malformed index patterns on role management pages (#99918) --- .../public/management/roles/edit_role/edit_role_page.test.tsx | 3 ++- .../public/management/roles/edit_role/edit_role_page.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index 5df73f7f8ec4e..b8963ea5a76e3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -143,7 +143,8 @@ function getProps({ rolesAPIClient.getRole.mockResolvedValue(role); const indexPatterns = dataPluginMock.createStartContract().indexPatterns; - indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + // `undefined` titles can technically happen via import/export or other manual manipulation + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*', undefined]); const indicesAPIClient = indicesAPIClientMock.create(); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index f810cd2079d16..0f49aaf48c394 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -125,7 +125,7 @@ function useIndexPatternsTitles( fatalErrors.add(err); throw err; }) - .then(setIndexPatternsTitles); + .then((titles) => setIndexPatternsTitles(titles.filter(Boolean))); }, [fatalErrors, indexPatterns, notifications]); return indexPatternsTitles; From b0a2537ee5d0752a3de9496ccb143c49d8781a6d Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Thu, 13 May 2021 10:59:00 -0700 Subject: [PATCH 030/186] Skip flaky functional test suite https://github.com/elastic/kibana/issues/100060 Signed-off-by: Tyler Smalley --- test/plugin_functional/test_suites/doc_views/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/plugin_functional/test_suites/doc_views/index.ts b/test/plugin_functional/test_suites/doc_views/index.ts index 2fed8e10ffc8e..e02b9ac4646f6 100644 --- a/test/plugin_functional/test_suites/doc_views/index.ts +++ b/test/plugin_functional/test_suites/doc_views/index.ts @@ -11,7 +11,8 @@ import { PluginFunctionalProviderContext } from '../../services'; export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) { const esArchiver = getService('esArchiver'); - describe('doc views', function () { + // SKIPPED: https://github.com/elastic/kibana/issues/100060 + describe.skip('doc views', function () { before(async () => { await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/discover'); }); From 280dfb0ee9fba649d84a2952d262e13f26933d08 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 13 May 2021 14:08:33 -0400 Subject: [PATCH 031/186] [Fleet] Do not use async method in plugin setup|start (#100033) --- x-pack/plugins/fleet/common/types/index.ts | 9 --------- .../fleet/mock/plugin_configuration.ts | 9 --------- x-pack/plugins/fleet/server/mocks/index.ts | 4 ++++ x-pack/plugins/fleet/server/plugin.ts | 14 ++++++++------ .../plugins/fleet/server/services/app_context.ts | 5 ++--- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 03584a48ff17c..7117973baa139 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -15,13 +15,6 @@ export interface FleetConfigType { registryProxyUrl?: string; agents: { enabled: boolean; - tlsCheckDisabled: boolean; - pollingRequestTimeout: number; - maxConcurrentConnections: number; - kibana: { - host?: string[] | string; - ca_sha256?: string; - }; elasticsearch: { host?: string; ca_sha256?: string; @@ -29,8 +22,6 @@ export interface FleetConfigType { fleet_server?: { hosts?: string[]; }; - agentPolicyRolloutRateLimitIntervalMs: number; - agentPolicyRolloutRateLimitRequestPerInterval: number; }; agentPolicies?: PreconfiguredAgentPolicy[]; packages?: PreconfiguredPackage[]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 5d53425607361..7f0b71de779dc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -14,19 +14,10 @@ export const createConfigurationMock = (): FleetConfigType => { registryProxyUrl: '', agents: { enabled: true, - tlsCheckDisabled: true, - pollingRequestTimeout: 1000, - maxConcurrentConnections: 100, - kibana: { - host: '', - ca_sha256: '', - }, elasticsearch: { host: '', ca_sha256: '', }, - agentPolicyRolloutRateLimitIntervalMs: 100, - agentPolicyRolloutRateLimitRequestPerInterval: 1000, }, }; }; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 4bc2bea1e58b6..a94f274b202ad 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -30,6 +30,10 @@ export const createAppContextStartContractMock = (): FleetAppContext => { security: securityMock.createStart(), logger: loggingSystemMock.create().get(), isProductionMode: true, + configInitialValue: { + agents: { enabled: true, elasticsearch: {} }, + enabled: true, + }, kibanaVersion: '8.0.0', kibanaBranch: 'master', }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 61c3a83242c57..0fdc6ef651ed1 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -6,7 +6,6 @@ */ import type { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import type { CoreSetup, CoreStart, @@ -110,6 +109,7 @@ export interface FleetAppContext { encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; config$?: Observable; + configInitialValue: FleetConfigType; savedObjects: SavedObjectsServiceStart; isProductionMode: PluginInitializerContext['env']['mode']['prod']; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -189,6 +189,7 @@ export class FleetPlugin implements AsyncPlugin { private licensing$!: Observable; private config$: Observable; + private configInitialValue: FleetConfigType; private cloud: CloudSetup | undefined; private logger: Logger | undefined; @@ -204,15 +205,15 @@ export class FleetPlugin this.kibanaVersion = this.initializerContext.env.packageInfo.version; this.kibanaBranch = this.initializerContext.env.packageInfo.branch; this.logger = this.initializerContext.logger.get(); + this.configInitialValue = this.initializerContext.config.get(); } - public async setup(core: CoreSetup, deps: FleetSetupDeps) { + public setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; this.licensing$ = deps.licensing.license$; this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; - - const config = await this.config$.pipe(first()).toPromise(); + const config = this.configInitialValue; registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -279,13 +280,14 @@ export class FleetPlugin } } - public async start(core: CoreStart, plugins: FleetStartDeps): Promise { - await appContextService.start({ + public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { + appContextService.start({ elasticsearch: core.elasticsearch, data: plugins.data, encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: plugins.security, + configInitialValue: this.configInitialValue, config$: this.config$, savedObjects: core.savedObjects, isProductionMode: this.isProductionMode, diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 954308a980861..82ec0aad52651 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -7,7 +7,6 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; -import { first } from 'rxjs/operators'; import { kibanaPackageJson } from '@kbn/utils'; import type { KibanaRequest } from 'src/core/server'; import type { @@ -44,7 +43,7 @@ class AppContextService { private httpSetup?: HttpServiceSetup; private externalCallbacks: ExternalCallbacksStorage = new Map(); - public async start(appContext: FleetAppContext) { + public start(appContext: FleetAppContext) { this.data = appContext.data; this.esClient = appContext.elasticsearch.client.asInternalUser; this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); @@ -60,7 +59,7 @@ class AppContextService { if (appContext.config$) { this.config$ = appContext.config$; - const initialValue = await this.config$.pipe(first()).toPromise(); + const initialValue = appContext.configInitialValue; this.configSubject$ = new BehaviorSubject(initialValue); this.config$.subscribe(this.configSubject$); } From 21221497323f81397c21396fad1275623ace3079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 13 May 2021 14:16:36 -0400 Subject: [PATCH 032/186] Rename alert status OK to Recovered and fix some UX issues around disabling a rule while being in an error state (#98135) * Fix UX when alert is disabled and in an error state * Reset executionStatus to pending after enabling an alert * Renames alert instance status OK to Recovered * Fix end to end test * Update doc screenshot * Fix confusing test name * Remove flakiness in integration test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../images/rule-details-alerts-inactive.png | Bin 131717 -> 142687 bytes .../server/alerts_client/alerts_client.ts | 5 ++ .../server/alerts_client/tests/enable.test.ts | 10 +++ .../components/alert_details.test.tsx | 70 ++++++++++++++++++ .../components/alert_details.tsx | 10 ++- .../components/alert_instances.test.tsx | 2 +- .../components/alert_instances.tsx | 2 +- .../apps/triggers_actions_ui/details.ts | 2 +- 8 files changed, 96 insertions(+), 5 deletions(-) diff --git a/docs/user/alerting/images/rule-details-alerts-inactive.png b/docs/user/alerting/images/rule-details-alerts-inactive.png index f84910ae0dcdc4b202ef8b4d298cb5c0e591caf4..fc82cf465ebb2da81b918b00c3a3a2d2010a4102 100644 GIT binary patch literal 142687 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 ?+9GZpr4_@;GP&WN?!@m;+{ENQ^o;{|F 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 literal 131717 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 z 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 diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 2c71784518c9b..f01341339da27 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -1016,6 +1016,11 @@ export class AlertsClient { ...this.apiKeyAsAlertAttributes(createdAPIKey, username), updatedBy: username, updatedAt: new Date().toISOString(), + executionStatus: { + status: 'pending', + lastExecutionDate: new Date().toISOString(), + error: null, + }, }); try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 1375a7cc5a1d7..8329e52d7444a 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -258,6 +258,11 @@ describe('enable()', () => { }, }, ], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, }, { version: '123', @@ -362,6 +367,11 @@ describe('enable()', () => { }, }, ], + executionStatus: { + status: 'pending', + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + }, }, { version: '123', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index a41708f052dc8..1bbffc850ee18 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -8,6 +8,8 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from '@testing-library/react'; import { AlertDetails } from './alert_details'; import { Alert, ActionType, AlertTypeModel, AlertType } from '../../../../types'; import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; @@ -463,6 +465,74 @@ describe('disable button', () => { handler!({} as React.FormEvent); expect(enableAlert).toHaveBeenCalledTimes(1); }); + + it('should reset error banner dismissal after re-enabling the alert', async () => { + const alert = mockAlert({ + enabled: true, + executionStatus: { + status: 'error', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: AlertExecutionStatusErrorReasons.Execute, + message: 'Fail', + }, + }, + }); + + const alertType: AlertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup, + actionVariables: { context: [], state: [], params: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + authorizedConsumers, + minimumLicenseRequired: 'basic', + enabledInLicense: true, + }; + + const disableAlert = jest.fn(); + const enableAlert = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + // Dismiss the error banner + await act(async () => { + wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click'); + await nextTick(); + }); + + // Disable the alert + await act(async () => { + wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + await nextTick(); + }); + expect(disableAlert).toHaveBeenCalled(); + + // Enable the alert + await act(async () => { + wrapper.find('[data-test-subj="disableSwitch"] .euiSwitch__button').first().simulate('click'); + await nextTick(); + }); + expect(enableAlert).toHaveBeenCalled(); + + // Ensure error banner is back + expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0); + }); }); describe('mute button', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 0796f09b13460..d85e792f4a9bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -236,6 +236,8 @@ export const AlertDetails: React.FunctionComponent = ({ if (isEnabled) { setIsEnabled(false); await disableAlert(alert); + // Reset dismiss if previously clicked + setDissmissAlertErrors(false); } else { setIsEnabled(true); await enableAlert(alert); @@ -277,7 +279,7 @@ export const AlertDetails: React.FunctionComponent = ({ - {!dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( + {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( = ({ - setDissmissAlertErrors(true)}> + setDissmissAlertErrors(true)} + > { }; expect(alertInstanceToListItem(fakeNow.getTime(), alertType, 'id', instance)).toEqual({ instance: 'id', - status: { label: 'OK', healthColor: 'subdued' }, + status: { label: 'Recovered', healthColor: 'subdued' }, start: undefined, duration: 0, sortPriority: 1, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index d2919194125f8..5ba4c466f6fad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -226,7 +226,7 @@ const ACTIVE_LABEL = i18n.translate( const INACTIVE_LABEL = i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive', - { defaultMessage: 'OK' } + { defaultMessage: 'Recovered' } ); function getActionGroupName(alertType: AlertType, actionGroupId?: string): string | undefined { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index b38b605bc1b67..34e08ad257f84 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -655,7 +655,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ).to.eql([ { instance: 'eu/east', - status: 'OK', + status: 'Recovered', start: '', duration: '', }, From b009dab2618094e523717da4bbf58f866d2f8665 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Thu, 13 May 2021 12:11:16 -0700 Subject: [PATCH 033/186] Remove outdated comment about schema validation not working (it does work now). (#100055) --- src/plugins/console/server/config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts index 90839a18e1210..4e42e3c21d2ad 100644 --- a/src/plugins/console/server/config.ts +++ b/src/plugins/console/server/config.ts @@ -15,8 +15,6 @@ export const config = schema.object( enabled: schema.boolean({ defaultValue: true }), proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), - - // This does not actually work, track this issue: https://github.com/elastic/kibana/issues/55576 proxyConfig: schema.arrayOf( schema.object({ match: schema.object({ From 1a955a28841773d75e43c31cec01aa3a9b00a39c Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 13 May 2021 12:11:30 -0700 Subject: [PATCH 034/186] [Enterprise Search] Fix SchemaFieldTypeSelect axe issues (#100035) * Update SchemaFieldTypeSelect to allow passing any aria props - We'll specifically be using aria-labelledby in this PR, but theoretically any aria prop should be fine. * Update AS & WS schema tables to use the type table column heading as an aria-labelledby ID --- .../components/schema/components/schema_table.tsx | 5 ++++- .../shared/schema/field_type_select/index.test.tsx | 6 ++++++ .../applications/shared/schema/field_type_select/index.tsx | 2 ++ .../components/schema/schema_fields_table.tsx | 3 ++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx index d9187bb65adf0..8fff01b268b12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/schema_table.tsx @@ -35,7 +35,9 @@ export const SchemaTable: React.FC = () => { {FIELD_NAME} - {FIELD_TYPE} + + {FIELD_TYPE} + @@ -74,6 +76,7 @@ export const SchemaTable: React.FC = () => { fieldName={fieldName} fieldType={fieldType} updateExistingFieldType={updateSchemaFieldType} + aria-labelledby="schemaFieldType" /> diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx index df28719839011..6d51a06273712 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.test.tsx @@ -39,4 +39,10 @@ describe('SchemaFieldTypeSelect', () => { expect(wrapper.find(EuiSelect).prop('disabled')).toEqual(true); }); + + it('passes arbitrary props', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('aria-label')).toEqual('Test label'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx index 8dfd87f4015d6..fb6c0f2047b12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/field_type_select/index.tsx @@ -23,10 +23,12 @@ export const SchemaFieldTypeSelect: React.FC = ({ fieldType, updateExistingFieldType, disabled, + ...rest }) => { const fieldTypeOptions = Object.values(SchemaType).map((type) => ({ value: type, text: type })); return ( { {SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER} - + {SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER} @@ -58,6 +58,7 @@ export const SchemaFieldsTable: React.FC = () => { fieldName={fieldName} fieldType={filteredSchemaFields[fieldName]} updateExistingFieldType={updateExistingFieldType} + aria-labelledby="schemaDataType" /> From 62086281b46b9b9ba9c6bffa4cb85b8e29f9be46 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 13 May 2021 20:39:15 +0100 Subject: [PATCH 035/186] chore(NA): moving @kbn/docs-utils into bazel (#100051) --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-docs-utils/BUILD.bazel | 88 +++++++++++++++++++ packages/kbn-docs-utils/package.json | 4 - packages/kbn-docs-utils/tsconfig.json | 3 +- yarn.lock | 2 +- 7 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 packages/kbn-docs-utils/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 1e7a95b83dd67..7265cd415949c 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -74,6 +74,7 @@ yarn kbn watch-bazel - @kbn/config-schema - @kbn/crypto - @kbn/dev-utils +- @kbn/docs-utils - @kbn/es - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint diff --git a/package.json b/package.json index d46617f2a6f2a..c04face1233ae 100644 --- a/package.json +++ b/package.json @@ -448,7 +448,7 @@ "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset/npm_module", "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils/npm_module", - "@kbn/docs-utils": "link:packages/kbn-docs-utils", + "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module", "@kbn/es": "link:bazel-bin/packages/kbn-es/npm_module", "@kbn/es-archiver": "link:packages/kbn-es-archiver", "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 2ae04e02cffd2..c3d08ad49daea 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -16,6 +16,7 @@ filegroup( "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", "//packages/kbn-dev-utils:build", + "//packages/kbn-docs-utils:build", "//packages/kbn-es:build", "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel new file mode 100644 index 0000000000000..e72d83851f5d2 --- /dev/null +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -0,0 +1,88 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-docs-utils" +PKG_REQUIRE_NAME = "@kbn/docs-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/snapshots/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +SRC_DEPS = [ + "//packages/kbn-config", + "//packages/kbn-dev-utils", + "//packages/kbn-utils", + "@npm//dedent", + "@npm//ts-morph", +] + +TYPES_DEPS = [ + "@npm//@types/dedent", + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 27d38d2d8ed4f..b2a52b2d1f78e 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -7,9 +7,5 @@ "types": "target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "kbn:bootstrap": "../../node_modules/.bin/tsc", - "kbn:watch": "../../node_modules/.bin/tsc --watch" } } \ No newline at end of file diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 6f4a6fa2af8a5..9868c8b3d2bb4 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,11 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "target": "ES2019", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-docs-utils/src", "types": [ diff --git a/yarn.lock b/yarn.lock index 69d5c9553a3b6..b8b4e54d25dcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2634,7 +2634,7 @@ version "0.0.0" uid "" -"@kbn/docs-utils@link:packages/kbn-docs-utils": +"@kbn/docs-utils@link:bazel-bin/packages/kbn-docs-utils/npm_module": version "0.0.0" uid "" From f9654a71287e357eabec194cf388a9405b7f5c47 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 13 May 2021 16:03:57 -0400 Subject: [PATCH 036/186] [Uptime] Increase debounce and add immediate submit to `useQueryBar` (#99675) * Increase debounce and add immediate submit to `useQueryBar`. * Reduce debounce to 800ms. --- .../overview/query_bar/query_bar.tsx | 5 +- .../overview/query_bar/use_query_bar.ts | 68 ++++++++++++++----- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx index 0543e5868bb9e..9436f420f7740 100644 --- a/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/query_bar.tsx @@ -34,11 +34,11 @@ export const isValidKuery = (query: string) => { export const QueryBar = () => { const { search: urlValue } = useGetUrlParams(); - const { query, setQuery } = useQueryBar(); + const { query, setQuery, submitImmediately } = useQueryBar(); const { index_pattern: indexPattern } = useIndexPattern(); - const [inputVal, setInputVal] = useState(query.query); + const [inputVal, setInputVal] = useState(query.query as string); const isInValid = () => { if (query.language === SyntaxType.text) { @@ -66,6 +66,7 @@ export const QueryBar = () => { }} onSubmit={(queryN) => { if (queryN) setQuery({ query: queryN.query as string, language: queryN.language }); + submitImmediately(); }} query={{ ...query, query: inputVal }} aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', { diff --git a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts index 0d8a2ee17994a..164231bfdd89b 100644 --- a/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts +++ b/x-pack/plugins/uptime/public/components/overview/query_bar/use_query_bar.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDebounce } from 'react-use'; import { useDispatch } from 'react-redux'; +import { Query } from 'src/plugins/data/common'; import { useGetUrlParams, useUpdateKueryString, useUrlParams } from '../../../hooks'; import { setEsKueryString } from '../../../state/actions'; import { useIndexPattern } from './use_index_pattern'; @@ -20,7 +21,26 @@ export enum SyntaxType { } const SYNTAX_STORAGE = 'uptime:queryBarSyntax'; -export const useQueryBar = () => { +const DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL = 800; + +interface UseQueryBarUtils { + // The Query object used by the search bar + query: Query; + // Update the Query object + setQuery: React.Dispatch>; + /** + * By default the search bar uses a debounce to delay submitting input; + * this function will cancel the debounce and submit immediately. + */ + submitImmediately: () => void; +} + +/** + * Provides state management and automatic dispatching of a Query object. + * + * @returns {UseQueryBarUtils} + */ +export const useQueryBar = (): UseQueryBarUtils => { const dispatch = useDispatch(); const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); @@ -30,7 +50,7 @@ export const useQueryBar = () => { services: { storage }, } = useKibana(); - const [query, setQuery] = useState( + const [query, setQuery] = useState( queryParam ? { query: queryParam, @@ -59,23 +79,37 @@ export const useQueryBar = () => { [dispatch] ); - useEffect(() => { - setEsKueryFilters(esFilters ?? ''); - }, [esFilters, setEsKueryFilters]); + const setEs = useCallback(() => setEsKueryFilters(esFilters ?? ''), [ + esFilters, + setEsKueryFilters, + ]); + const [, cancelEsKueryUpdate] = useDebounce(setEs, DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL, [ + esFilters, + setEsKueryFilters, + ]); - useDebounce( - () => { - if (query.language === SyntaxType.text && queryParam !== query.query) { - updateUrlParams({ query: query.query as string }); - } - if (query.language === SyntaxType.kuery) { - updateUrlParams({ query: '' }); - } - }, - 350, + const handleQueryUpdate = useCallback(() => { + if (query.language === SyntaxType.text && queryParam !== query.query) { + updateUrlParams({ query: query.query as string }); + } + if (query.language === SyntaxType.kuery) { + updateUrlParams({ query: '' }); + } + }, [query.language, query.query, queryParam, updateUrlParams]); + + const [, cancelQueryUpdate] = useDebounce( + handleQueryUpdate, + DEFAULT_QUERY_UPDATE_DEBOUNCE_INTERVAL, [query] ); + const submitImmediately = useCallback(() => { + cancelQueryUpdate(); + cancelEsKueryUpdate(); + handleQueryUpdate(); + setEs(); + }, [cancelEsKueryUpdate, cancelQueryUpdate, handleQueryUpdate, setEs]); + useDebounce( () => { if (query.language === SyntaxType.kuery && !error && esFilters) { @@ -92,5 +126,5 @@ export const useQueryBar = () => { [esFilters, error] ); - return { query, setQuery }; + return { query, setQuery, submitImmediately }; }; From 7e800993ccd24001abb8d672f949da10629abc8b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 14:05:02 -0600 Subject: [PATCH 037/186] [Security Solutions][Lists] Trims down list plugin size by breaking out the exception builder into chunks by using react lazy loading (#99989) ## Summary Trims down the list plugin size by breaking out the exception builder into a dedicated chunk by using React Suspense and React lazy loading. Before this PR the page load bundle size was `260503`, after the page load bundle size will be `194132`: You can calculate this through: ```ts node ./scripts/build_kibana_platform_plugins --dist --focus lists cat ./x-pack/plugins/lists/target/public/metrics.json ``` Before ```json [ { "group": "@kbn/optimizer bundle module count", "id": "lists", "value": 227 }, { "group": "page load bundle size", "id": "lists", "value": 260503, <--- Very large load bundle size "limit": 280504, "limitConfigPath": "packages/kbn-optimizer/limits.yml" }, { "group": "async chunks size", "id": "lists", "value": 0 }, { "group": "async chunk count", "id": "lists", "value": 0 }, { "group": "miscellaneous assets size", "id": "lists", "value": 0 } ] ``` After: ```json [ { "group": "@kbn/optimizer bundle module count", "id": "lists", "value": 227 }, { "group": "page load bundle size", "id": "lists", "value": 194132, <--- Not as large bundle size "limit": 280504, "limitConfigPath": "packages/kbn-optimizer/limits.yml" }, { "group": "async chunks size", "id": "lists", "value": 70000 }, { "group": "async chunk count", "id": "lists", "value": 1 }, { "group": "miscellaneous assets size", "id": "lists", "value": 0 } ] ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../builder/exception_items_renderer.tsx | 3 ++ .../exceptions/components/builder/index.tsx | 32 ++++++++++++-- .../lists/public/exceptions/transforms.ts | 2 +- .../add_exception_modal/index.test.tsx | 7 +-- .../exceptions/add_exception_modal/index.tsx | 43 +++++++++---------- .../edit_exception_modal/index.test.tsx | 11 ++--- .../exceptions/edit_exception_modal/index.tsx | 41 +++++++++--------- .../view/components/form/index.test.tsx | 6 ++- .../view/components/form/index.tsx | 39 ++++++++--------- 9 files changed, 108 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index a698feb93722c..646803f2e6794 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -444,3 +444,6 @@ export const ExceptionBuilderComponent = ({ }; ExceptionBuilderComponent.displayName = 'ExceptionBuilder'; + +// eslint-disable-next-line import/no-default-export +export default ExceptionBuilderComponent; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx index 833034aa0a542..551889e4a821d 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/index.tsx @@ -5,6 +5,32 @@ * 2.0. */ -export { BuilderEntryItem } from './entry_renderer'; -export { BuilderExceptionListItemComponent } from './exception_item_renderer'; -export { ExceptionBuilderComponent, OnChangeProps } from './exception_items_renderer'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import React, { Suspense, lazy } from 'react'; + +// Note: Only use import type/export type here to avoid pulling anything non-lazy into the main plugin and increasing the plugin size +import type { ExceptionBuilderProps } from './exception_items_renderer'; +export type { OnChangeProps } from './exception_items_renderer'; + +interface ExtraProps { + dataTestSubj: string; + idAria: string; +} + +/** + * This lazy load allows the exception builder to pull everything out into a plugin chunk. + * You want to be careful of not directly importing/exporting things from exception_items_renderer + * unless you use a import type, and/or a export type to ensure full type erasure + */ +const ExceptionBuilderComponentLazy = lazy(() => import('./exception_items_renderer')); +export const getExceptionBuilderComponentLazy = ( + props: ExceptionBuilderProps & ExtraProps +): JSX.Element => ( + }> + + +); diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 468dfc00ca852..50ce1b6e33a4b 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -8,7 +8,7 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { +import type { CreateExceptionListItemSchema, EntriesArray, Entry, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index bf15994f60cbc..d659f557ee751 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -58,13 +58,14 @@ describe('When the add exception modal is opened', () => { ReturnType >; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems'); ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useAsync as jest.Mock).mockImplementation(() => ({ start: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 96335f8d85d90..120c4ad8efc1b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -469,28 +469,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: initialExceptionItems, + listType: exceptionListType, + osTypes: osTypesSelection, + listId: ruleExceptionList.list_id, + listNamespaceType: ruleExceptionList.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + indexPatterns, + isOrDisabled: isExceptionBuilderFormDisabled, + isAndDisabled: isExceptionBuilderFormDisabled, + isNestedDisabled: isExceptionBuilderFormDisabled, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleBuilderOnChange, + isDisabled: isExceptionBuilderFormDisabled, + })} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 7ee0e6888a42e..64ef1dead7e75 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -49,11 +49,11 @@ jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_ jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); jest.mock('../../../../shared_imports', () => { const originalModule = jest.requireActual('../../../../shared_imports'); - + const emptyComp = ; return { ...originalModule, ExceptionBuilder: { - ExceptionBuilderComponent: () => ({} as JSX.Element), + getExceptionBuilderComponentLazy: () => emptyComp, }, }; }); @@ -62,13 +62,14 @@ describe('When the edit exception modal is opened', () => { const ruleName = 'test rule'; let ExceptionBuilderComponent: jest.SpyInstance< - ReturnType + ReturnType >; beforeEach(() => { + const emptyComp = ; ExceptionBuilderComponent = jest - .spyOn(ExceptionBuilder, 'ExceptionBuilderComponent') - .mockReturnValue(<>); + .spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy') + .mockReturnValue(emptyComp); (useSignalIndex as jest.Mock).mockReturnValue({ loading: false, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 8bf5ea9f8a80f..5fb52994fb0f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -342,27 +342,26 @@ export const EditExceptionModal = memo(function EditExceptionModal({ )} - + {ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: + !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exceptionItem], + listType: exceptionListType, + listId: exceptionItem.list_id, + listNamespaceType: exceptionItem.namespace_type, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + ruleName, + isOrDisabled: true, + isAndDisabled: false, + osTypes: exceptionItem.os_types, + isNestedDisabled: false, + dataTestSubj: 'edit-exception-modal-builder', + idAria: 'edit-exception-modal-builder', + onChange: handleBuilderOnChange, + indexPatterns, + })} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index 940882d079a12..0867d0542e4c1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -17,6 +17,7 @@ import { createGlobalNoMiddlewareStore, ecsEventMock } from '../../../test_utils import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock'; import { NAME_ERROR, NAME_PLACEHOLDER } from './translations'; import { useCurrentUser, useKibana } from '../../../../../../common/lib/kibana'; +import { ExceptionBuilder } from '../../../../../../shared_imports'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../../common/containers/source'); @@ -53,6 +54,9 @@ describe('Event filter form', () => { }; beforeEach(() => { + const emptyComp = ; + jest.spyOn(ExceptionBuilder, 'getExceptionBuilderComponentLazy').mockReturnValue(emptyComp); + (useFetchIndex as jest.Mock).mockImplementation(() => [ false, { @@ -77,7 +81,7 @@ describe('Event filter form', () => { it('should renders correctly with data', () => { component = renderComponentWithdata(); - expect(component.getByText(ecsEventMock().process!.executable![0])).not.toBeNull(); + expect(component.getByTestId('alert-exception-builder')).not.toBeNull(); expect(component.getByText(NAME_ERROR)).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 744fb9930321d..d74baab0d2bbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -115,26 +115,25 @@ export const EventFiltersForm: React.FC = memo( ); const exceptionBuilderComponentMemo = useMemo( - () => ( - - ), + () => + ExceptionBuilder.getExceptionBuilderComponentLazy({ + allowLargeValueLists: true, + httpService: http, + autocompleteService: data.autocomplete, + exceptionListItems: [exception as ExceptionListItemSchema], + listType: EVENT_FILTER_LIST_TYPE, + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + listNamespaceType: 'agnostic', + ruleName: RULE_NAME, + indexPatterns, + isOrDisabled: true, // TODO: pending to be validated + isAndDisabled: false, + isNestedDisabled: false, + dataTestSubj: 'alert-exception-builder', + idAria: 'alert-exception-builder', + onChange: handleOnBuilderChange, + listTypeSpecificIndexPatternFilter: filterIndexPatterns, + }), [data, handleOnBuilderChange, http, indexPatterns, exception] ); From f331d64bca854d9c68db3c9670ce38a7cc73920f Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 13 May 2021 16:15:59 -0400 Subject: [PATCH 038/186] [Uptime] [Synthetics Integration] ensure that proxy url is not overwritten (#99944) --- .../synthetics_policy_create_extension.tsx | 7 ++- ...s_policy_create_extension_wrapper.test.tsx | 58 +++++++++++++++++-- .../synthetics_policy_edit_extension.tsx | 7 ++- ...ics_policy_edit_extension_wrapper.test.tsx | 54 ++++++++++++++++- 4 files changed, 116 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx index 51585e227b56e..1306308f8ba4e 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension.tsx @@ -9,7 +9,7 @@ import React, { memo, useContext, useEffect } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { PackagePolicyCreateExtensionComponentProps } from '../../../../fleet/public'; import { useTrackPageview } from '../../../../observability/public'; -import { Config, ConfigKeys } from './types'; +import { Config, ConfigKeys, DataStream } from './types'; import { SimpleFieldsContext, HTTPAdvancedFieldsContext, @@ -63,6 +63,11 @@ export const SyntheticsPolicyCreateExtension = memo', () => { }); }); - it('handles updating each field', async () => { + it('handles updating fields', async () => { const { getByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; @@ -336,6 +336,54 @@ describe('', () => { expect(apmServiceName.value).toEqual('APM Service'); expect(maxRedirects.value).toEqual('2'); expect(timeout.value).toEqual('3'); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + isValid: true, + updatedPolicy: { + ...defaultNewPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + streams: [ + { + ...defaultNewPolicy.inputs[0].streams[0], + vars: { + ...defaultNewPolicy.inputs[0].streams[0].vars, + urls: { + value: 'http://elastic.co', + type: 'text', + }, + proxy_url: { + value: 'http://proxy.co', + type: 'text', + }, + schedule: { + value: '"@every 1m"', + type: 'text', + }, + 'service.name': { + value: 'APM Service', + type: 'text', + }, + max_redirects: { + value: '2', + type: 'integer', + }, + timeout: { + value: '3s', + type: 'text', + }, + }, + }, + ], + }, + defaultNewPolicy.inputs[1], + defaultNewPolicy.inputs[2], + ], + }, + }); + }); }); it('handles calling onChange', async () => { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx index 386d99add87b6..e29a5c6a363ed 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension.tsx @@ -15,7 +15,7 @@ import { TCPAdvancedFieldsContext, TLSFieldsContext, } from './contexts'; -import { Config, ConfigKeys } from './types'; +import { Config, ConfigKeys, DataStream } from './types'; import { CustomFields } from './custom_fields'; import { useUpdatePolicy } from './use_update_policy'; import { validate } from './validation'; @@ -48,6 +48,11 @@ export const SyntheticsPolicyEditExtension = memo', () => { expect(queryByLabelText('Monitor type')).not.toBeInTheDocument(); }); - it('handles updating each field', async () => { + it('handles updating fields', async () => { const { getByLabelText } = render(); const url = getByLabelText('URL') as HTMLInputElement; const proxyUrl = getByLabelText('Proxy URL') as HTMLInputElement; @@ -364,6 +364,54 @@ describe('', () => { expect(apmServiceName.value).toEqual('APM Service'); expect(maxRedirects.value).toEqual('2'); expect(timeout.value).toEqual('3'); + + await waitFor(() => { + expect(onChange).toBeCalledWith({ + isValid: true, + updatedPolicy: { + ...defaultNewPolicy, + inputs: [ + { + ...defaultNewPolicy.inputs[0], + streams: [ + { + ...defaultNewPolicy.inputs[0].streams[0], + vars: { + ...defaultNewPolicy.inputs[0].streams[0].vars, + urls: { + value: 'http://elastic.co', + type: 'text', + }, + proxy_url: { + value: 'http://proxy.co', + type: 'text', + }, + schedule: { + value: '"@every 1m"', + type: 'text', + }, + 'service.name': { + value: 'APM Service', + type: 'text', + }, + max_redirects: { + value: '2', + type: 'integer', + }, + timeout: { + value: '3s', + type: 'text', + }, + }, + }, + ], + }, + defaultNewPolicy.inputs[1], + defaultNewPolicy.inputs[2], + ], + }, + }); + }); }); it('handles calling onChange', async () => { From 3deb2bd80e77aedb49c2da3eaeabaa5c7dbe492a Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Thu, 13 May 2021 15:24:26 -0500 Subject: [PATCH 039/186] Re-enable formerly flaky shareable test (#98826) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/canvas/shareable_runtime/components/app.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index b68642d184542..acf71cad3f3ba 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,8 +59,7 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -// FLAKY: https://github.com/elastic/kibana/issues/95899 -describe.skip('', () => { +describe('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); From 1c82ec332229e8267086d3c18138955759311586 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Thu, 13 May 2021 15:24:40 -0500 Subject: [PATCH 040/186] [Canvas] Remove unused legacy autocomplete component (#99215) * Remove unused autocomplete component * Remove reference to autocomplete CSS Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/autocomplete/autocomplete.js | 264 ------------------ .../components/autocomplete/autocomplete.scss | 52 ---- .../public/components/autocomplete/index.js | 8 - x-pack/plugins/canvas/public/style/index.scss | 1 - 4 files changed, 325 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js delete mode 100644 x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss delete mode 100644 x-pack/plugins/canvas/public/components/autocomplete/index.js diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js deleted file mode 100644 index 302e45ade8d43..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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. - */ - -/*disabling eslint because of these jsx-a11y errors(https://www.npmjs.com/package/eslint-plugin-jsx-a11y): -181:7 error Elements with the 'combobox' interactive role must be focusable jsx-a11y/interactive-supports-focus - 187:9 error Elements with the ARIA role "combobox" must have the following attributes defined: aria-controls,aria-expanded jsx-a11y/role-has-required-aria-props - 209:23 error Elements with the 'option' interactive role must be focusable jsx-a11y/interactive-supports-focus - 218:25 error Elements with the ARIA role "option" must have the following attributes defined: aria-selected jsx-a11y/role-has-required-aria-props -*/ -/* eslint-disable jsx-a11y/interactive-supports-focus */ -/* eslint-disable jsx-a11y/role-has-required-aria-props */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, keys } from '@elastic/eui'; - -/** - * An autocomplete component. Currently this is only used for the expression editor but in theory - * it could be extended to any autocomplete-related component. It expects these props: - * - * header: The header node - * items: The list of items for autocompletion - * onSelect: The function to invoke when an item is selected (passing in the item) - * children: Any child nodes, which should include the text input itself - * reference: A function that is passed the selected item which generates a reference node - */ -export class Autocomplete extends React.Component { - static propTypes = { - header: PropTypes.node, - items: PropTypes.array, - onSelect: PropTypes.func, - children: PropTypes.node, - reference: PropTypes.func, - }; - - constructor() { - super(); - this.state = { - isOpen: false, - isFocused: false, - isMousedOver: false, - selectedIndex: -1, - }; - - // These are used for automatically scrolling items into view when selected - this.containerRef = null; - this.itemRefs = []; - } - - componentDidUpdate(prevProps, prevState) { - if ( - this.props.items && - prevProps.items !== this.props.items && - this.props.items.length === 1 && - this.state.selectedIndex !== 0 - ) { - this.selectFirst(); - } - - if (prevState.selectedIndex !== this.state.selectedIndex) { - this.scrollIntoView(); - } - } - - selectFirst() { - this.setState({ selectedIndex: 0 }); - } - - isVisible() { - const { isOpen, isFocused, isMousedOver } = this.state; - const { items, reference } = this.props; - - // We have to check isMousedOver because the blur event fires before the click event, which - // means if we didn't keep track of isMousedOver, we wouldn't even get the click event - const visible = isOpen && (isFocused || isMousedOver); - const hasItems = items && items.length; - const hasReference = reference(this.getSelectedItem()); - - return visible && (hasItems || hasReference); - } - - getSelectedItem() { - return this.props.items && this.props.items[this.state.selectedIndex]; - } - - selectPrevious() { - const { items } = this.props; - const { selectedIndex } = this.state; - if (selectedIndex > 0) { - this.setState({ selectedIndex: selectedIndex - 1 }); - } else { - this.setState({ selectedIndex: items.length - 1 }); - } - } - - selectNext() { - const { items } = this.props; - const { selectedIndex } = this.state; - if (selectedIndex >= 0 && selectedIndex < items.length - 1) { - this.setState({ selectedIndex: selectedIndex + 1 }); - } else { - this.setState({ selectedIndex: 0 }); - } - } - - scrollIntoView() { - const { - containerRef, - itemRefs, - state: { selectedIndex }, - } = this; - const itemRef = itemRefs[selectedIndex]; - if (!containerRef || !itemRef) { - return; - } - containerRef.scrollTop = Math.max( - Math.min(containerRef.scrollTop, itemRef.offsetTop), - itemRef.offsetTop + itemRef.offsetHeight - containerRef.offsetHeight - ); - } - - onSubmit = () => { - const { selectedIndex } = this.state; - const { items, onSelect } = this.props; - onSelect(items[selectedIndex]); - this.setState({ selectedIndex: -1 }); - }; - - /** - * Handle key down events for the menu, including selecting the previous and next items, making - * the item selection, closing the menu, etc. - */ - onKeyDown = (e) => { - const { BACKSPACE, ESCAPE, TAB, ENTER, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT } = keys; - const { key } = e; - const { items } = this.props; - const { isOpen, selectedIndex } = this.state; - - if ([ESCAPE, ARROW_LEFT, ARROW_RIGHT].includes(key)) { - this.setState({ isOpen: false }); - } - - if ([TAB, ENTER].includes(key) && isOpen && selectedIndex >= 0) { - e.preventDefault(); - this.onSubmit(); - } else if (key === ARROW_UP && items.length > 0 && isOpen) { - e.preventDefault(); - this.selectPrevious(); - } else if (key === ARROW_DOWN && items.length > 0 && isOpen) { - e.preventDefault(); - this.selectNext(); - } else if (key === BACKSPACE) { - this.setState({ isOpen: true }); - } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(key)) { - this.setState({ selectedIndex: -1 }); - } - }; - - /** - * On key press (character keys), show the menu. We don't want to willy nilly show the menu - * whenever ANY key down event happens (like arrow keys) cuz that would be just downright - * annoying. - */ - onKeyPress = () => { - this.setState({ isOpen: true }); - }; - - onFocus = () => { - this.setState({ - isFocused: true, - }); - }; - - onBlur = () => { - this.setState({ - isFocused: false, - }); - }; - - onMouseDown = () => { - this.setState({ - isOpen: false, - }); - }; - - onMouseEnter = () => { - this.setState({ - isMousedOver: true, - }); - }; - - onMouseLeave = () => { - this.setState({ isMousedOver: false }); - }; - - render() { - const { header, items, reference } = this.props; - return ( -
- {this.isVisible() ? ( - - - {items && items.length ? ( - -
(this.containerRef = ref)} - role="listbox" - > - {header} - {items.map((item, i) => ( -
(this.itemRefs[i] = ref)} - className={ - 'autocompleteItem' + - (this.state.selectedIndex === i ? ' autocompleteItem--isActive' : '') - } - onMouseEnter={() => this.setState({ selectedIndex: i })} - onClick={this.onSubmit} - role="option" - > - {item.text} -
- ))} -
-
- ) : ( - '' - )} - -
{reference(this.getSelectedItem())}
-
-
-
- ) : ( - '' - )} -
- {this.props.children} -
-
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss deleted file mode 100644 index 7f723b5549acf..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.scss +++ /dev/null @@ -1,52 +0,0 @@ -.autocomplete { - position: relative; -} - -.autocompletePopup { - position: absolute; - top: -262px; - height: 260px; - width: 100%; -} - -.autocompleteItems { - border-right: $euiBorderThin; -} - -.autocompleteItems, -.autocompleteReference { - @include euiScrollBar; - height: 258px; - overflow: auto; -} - -.autocompleteReference { - padding: $euiSizeS $euiSizeM; - background-color: tintOrShade($euiColorLightestShade, 65%, 20%); -} - -.autocompleteItem { - padding: $euiSizeS $euiSizeM; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-family: $euiCodeFontFamily; - font-weight: $euiFontWeightRegular; -} - -.autocompleteItem--isActive { - color: $euiColorPrimary; - background-color: $euiFocusBackgroundColor; -} - -.autocompleteType { - padding: $euiSizeS; -} - -.autocompleteTable .euiTable { - background-color: transparent; -} - -.autocompleteDescList .euiDescriptionList__description { - margin-right: $euiSizeS; -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/autocomplete/index.js b/x-pack/plugins/canvas/public/components/autocomplete/index.js deleted file mode 100644 index a5572c26d04f5..0000000000000 --- a/x-pack/plugins/canvas/public/components/autocomplete/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Autocomplete } from './autocomplete'; diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 41d12db3a1853..6c883b832737f 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -13,7 +13,6 @@ @import '../components/arg_form/arg_form'; @import '../components/asset_manager/asset_manager'; @import '../components/asset_picker/asset_picker'; -@import '../components/autocomplete/autocomplete'; @import '../components/clipboard/clipboard'; @import '../components/color_dot/color_dot'; @import '../components/color_palette/color_palette'; From e6c88d77a0ab5f3e3fec6bbe41f392373230ab9c Mon Sep 17 00:00:00 2001 From: Tre Date: Thu, 13 May 2021 14:32:58 -0600 Subject: [PATCH 041/186] [QA] Switch tests to use importExport - visualize (#98063) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/visualize/_add_to_dashboard.ts | 3 + test/functional/apps/visualize/_area_chart.ts | 3 + .../functional/apps/visualize/_chart_types.ts | 2 +- test/functional/apps/visualize/_data_table.ts | 1 + .../visualize/_data_table_nontimeindex.ts | 1 + .../_data_table_notimeindex_filters.ts | 1 + .../apps/visualize/_embedding_chart.ts | 1 + .../apps/visualize/_experimental_vis.ts | 4 + .../functional/apps/visualize/_gauge_chart.ts | 3 + .../apps/visualize/_heatmap_chart.ts | 1 + .../visualize/_histogram_request_start.ts | 8 + test/functional/apps/visualize/_inspector.ts | 1 + test/functional/apps/visualize/_lab_mode.ts | 5 +- .../apps/visualize/_line_chart_split_chart.ts | 5 +- .../visualize/_line_chart_split_series.ts | 5 +- .../apps/visualize/_linked_saved_searches.ts | 1 + .../apps/visualize/_markdown_vis.ts | 1 + .../apps/visualize/_metric_chart.ts | 1 + test/functional/apps/visualize/_pie_chart.ts | 1 + .../apps/visualize/_point_series_options.ts | 5 +- test/functional/apps/visualize/_region_map.ts | 1 + .../functional/apps/visualize/_shared_item.ts | 1 + test/functional/apps/visualize/_tag_cloud.ts | 1 + test/functional/apps/visualize/_tile_map.ts | 1 + test/functional/apps/visualize/_tsvb_chart.ts | 18 +- .../apps/visualize/_tsvb_markdown.ts | 7 +- test/functional/apps/visualize/_tsvb_table.ts | 3 + .../apps/visualize/_tsvb_time_series.ts | 3 + test/functional/apps/visualize/_vega_chart.ts | 1 + .../apps/visualize/_vertical_bar_chart.ts | 4 + .../_vertical_bar_chart_nontimeindex.ts | 5 +- .../apps/visualize/_visualize_listing.ts | 1 + test/functional/apps/visualize/index.ts | 18 +- .../input_control_vis/chained_controls.ts | 1 + .../input_control_vis/dynamic_options.ts | 4 + .../input_control_options.ts | 1 + .../input_control_vis/input_control_range.ts | 7 +- .../apps/visualize/legacy/_data_table.ts | 1 + .../functional/apps/visualize/legacy/index.ts | 6 +- .../fixtures/kbn_archiver/visualize.json | 300 ++++++++++++++++++ .../functional/page_objects/visualize_page.ts | 12 + 41 files changed, 419 insertions(+), 30 deletions(-) create mode 100644 test/functional/fixtures/kbn_archiver/visualize.json diff --git a/test/functional/apps/visualize/_add_to_dashboard.ts b/test/functional/apps/visualize/_add_to_dashboard.ts index 17d628db86d25..4343f8b1469d6 100644 --- a/test/functional/apps/visualize/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/_add_to_dashboard.ts @@ -26,6 +26,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); describe('Add to Dashboard', function describeIndexTests() { + before(async () => { + await PageObjects.visualize.initTests(); + }); it('adding a new metric to a new dashboard by value', async function () { await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickMetric(); diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/_area_chart.ts index 2bad91565de72..99f65458bb606 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/_area_chart.ts @@ -34,6 +34,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); describe('area charts', function indexPatternCreation() { + before(async () => { + await PageObjects.visualize.initTests(); + }); const initAreaChart = async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 71bdc75d41d9c..f52d8f00c1e48 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import _ from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -17,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('chart types', function () { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); }); diff --git a/test/functional/apps/visualize/_data_table.ts b/test/functional/apps/visualize/_data_table.ts index 1ff5bdcc6da78..14181c084a77f 100644 --- a/test/functional/apps/visualize/_data_table.ts +++ b/test/functional/apps/visualize/_data_table.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.ts b/test/functional/apps/visualize/_data_table_nontimeindex.ts index 0146bb81134a7..1549f2aac0735 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.ts +++ b/test/functional/apps/visualize/_data_table_nontimeindex.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable without time filter'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts index df219edc1d2d5..ef664bf4b3054 100644 --- a/test/functional/apps/visualize/_data_table_notimeindex_filters.ts +++ b/test/functional/apps/visualize/_data_table_notimeindex_filters.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization DataTable w/o time filter'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_embedding_chart.ts b/test/functional/apps/visualize/_embedding_chart.ts index a6f0b21f96b35..93ab2987dc4a8 100644 --- a/test/functional/apps/visualize/_embedding_chart.ts +++ b/test/functional/apps/visualize/_embedding_chart.ts @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('embedding', () => { describe('a data table', () => { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickDataTable(); await PageObjects.visualize.clickNewSearch(); diff --git a/test/functional/apps/visualize/_experimental_vis.ts b/test/functional/apps/visualize/_experimental_vis.ts index 7132d252cd23c..8e33285f909be 100644 --- a/test/functional/apps/visualize/_experimental_vis.ts +++ b/test/functional/apps/visualize/_experimental_vis.ts @@ -15,6 +15,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize']); describe('experimental visualizations in visualize app ', function () { + before(async () => { + await PageObjects.visualize.initTests(); + }); + describe('experimental visualizations', () => { beforeEach(async () => { log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_gauge_chart.ts b/test/functional/apps/visualize/_gauge_chart.ts index 0153e022e71b3..6dd460d4ac32b 100644 --- a/test/functional/apps/visualize/_gauge_chart.ts +++ b/test/functional/apps/visualize/_gauge_chart.ts @@ -19,6 +19,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('gauge chart', function indexPatternCreation() { + before(async () => { + await PageObjects.visualize.initTests(); + }); async function initGaugeVis() { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/_heatmap_chart.ts index 660f45179631e..d71d524cc8f3b 100644 --- a/test/functional/apps/visualize/_heatmap_chart.ts +++ b/test/functional/apps/visualize/_heatmap_chart.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const vizName1 = 'Visualization HeatmapChart'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickHeatmapChart'); diff --git a/test/functional/apps/visualize/_histogram_request_start.ts b/test/functional/apps/visualize/_histogram_request_start.ts index b027dbcd57780..8b5c31701d025 100644 --- a/test/functional/apps/visualize/_histogram_request_start.ts +++ b/test/functional/apps/visualize/_histogram_request_start.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); + const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects([ 'common', @@ -24,6 +25,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('histogram agg onSearchRequestStart', function () { before(async function () { + // loading back default data + await esArchiver.load('empty_kibana'); + + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('long_window_logstash'); + + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index e46a833fd0fd7..f83eae2fc00bc 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('inspector', function describeIndexTests() { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewAggBasedVisualization(); await PageObjects.visualize.clickVerticalBarChart(); await PageObjects.visualize.clickNewSearch(); diff --git a/test/functional/apps/visualize/_lab_mode.ts b/test/functional/apps/visualize/_lab_mode.ts index 0a099ec27eee5..d3a02a8d17291 100644 --- a/test/functional/apps/visualize/_lab_mode.ts +++ b/test/functional/apps/visualize/_lab_mode.ts @@ -13,9 +13,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); - const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings', 'visualize']); describe('visualize lab mode', () => { + before(async () => { + await PageObjects.visualize.initTests(); + }); it('disabling does not break loading saved searches', async () => { await PageObjects.common.navigateToUrl('discover', '', { useActualUrl: true }); await PageObjects.discover.saveSearch('visualize_lab_mode_test'); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index 1b6da1b39f1e3..91c1db533cee9 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -43,7 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initLineChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initLineChart(); + }); afterEach(async () => { await inspector.close(); diff --git a/test/functional/apps/visualize/_line_chart_split_series.ts b/test/functional/apps/visualize/_line_chart_split_series.ts index b3debc13c7770..6630690b89c2c 100644 --- a/test/functional/apps/visualize/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/_line_chart_split_series.ts @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initLineChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initLineChart(); + }); afterEach(async () => { await inspector.close(); diff --git a/test/functional/apps/visualize/_linked_saved_searches.ts b/test/functional/apps/visualize/_linked_saved_searches.ts index 160720d27ab61..cfb20cf4b59b8 100644 --- a/test/functional/apps/visualize/_linked_saved_searches.ts +++ b/test/functional/apps/visualize/_linked_saved_searches.ts @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { let discoverSavedSearchUrlPath: string; before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('discover'); await filterBar.addFilter('extension.raw', 'is', 'jpg'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/functional/apps/visualize/_markdown_vis.ts b/test/functional/apps/visualize/_markdown_vis.ts index c8a481dbda2c3..16cdda9b610ca 100644 --- a/test/functional/apps/visualize/_markdown_vis.ts +++ b/test/functional/apps/visualize/_markdown_vis.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('markdown app in visualize app', () => { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(markdown); diff --git a/test/functional/apps/visualize/_metric_chart.ts b/test/functional/apps/visualize/_metric_chart.ts index 8c74784ef96d8..7853a3a845bfc 100644 --- a/test/functional/apps/visualize/_metric_chart.ts +++ b/test/functional/apps/visualize/_metric_chart.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('metric chart', function () { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickMetric'); diff --git a/test/functional/apps/visualize/_pie_chart.ts b/test/functional/apps/visualize/_pie_chart.ts index 16826b1676589..dd58ca6514c36 100644 --- a/test/functional/apps/visualize/_pie_chart.ts +++ b/test/functional/apps/visualize/_pie_chart.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('pie chart', function () { const vizName1 = 'Visualization PieChart'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickPieChart'); diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/_point_series_options.ts index d4bcc19a7c87c..b81feaf29e194 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/_point_series_options.ts @@ -61,7 +61,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } describe('vlad point series', function describeIndexTests() { - before(initChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initChart(); + }); describe('secondary value axis', function () { it('should show correct chart', async function () { diff --git a/test/functional/apps/visualize/_region_map.ts b/test/functional/apps/visualize/_region_map.ts index 3801d7d0cec12..916e8dbaee3a0 100644 --- a/test/functional/apps/visualize/_region_map.ts +++ b/test/functional/apps/visualize/_region_map.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'timePicker']); before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickRegionMap'); diff --git a/test/functional/apps/visualize/_shared_item.ts b/test/functional/apps/visualize/_shared_item.ts index a5dbe6c692978..3f9016ca2ff82 100644 --- a/test/functional/apps/visualize/_shared_item.ts +++ b/test/functional/apps/visualize/_shared_item.ts @@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('data-shared-item', function indexPatternCreation() { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.common.navigateToApp('visualize'); }); diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts index c7d864e5cfb23..a6ac324d9dc61 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/_tag_cloud.ts @@ -34,6 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const termsField = 'machine.ram'; before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickTagCloud'); diff --git a/test/functional/apps/visualize/_tile_map.ts b/test/functional/apps/visualize/_tile_map.ts index 719c2c48761f9..812b6a7d86802 100644 --- a/test/functional/apps/visualize/_tile_map.ts +++ b/test/functional/apps/visualize/_tile_map.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('tile map visualize app', function () { describe('incomplete config', function describeIndexTests() { before(async function () { + await PageObjects.visualize.initTests(); await browser.setWindowSize(1280, 1000); log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 6568eab0fc1f4..690db676cb368 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -17,6 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const retry = getService('retry'); const security = getService('security'); + const PageObjects = getPageObjects([ 'visualize', 'visualBuilder', @@ -27,12 +28,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { this.tags('includeFirefox'); + + before(async () => { + await PageObjects.visualize.initTests(); + }); + beforeEach(async () => { - await security.testUser.setRoles([ - 'kibana_admin', - 'test_logstash_reader', - 'kibana_sample_admin', - ]); + await security.testUser.setRoles( + ['kibana_admin', 'test_logstash_reader', 'kibana_sample_admin'], + false + ); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); @@ -141,7 +146,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - await esArchiver.unload('index_pattern_without_timefield'); + await esArchiver.load('empty_kibana'); + await PageObjects.visualize.initTests(); }); const switchIndexTest = async (useKibanaIndexes: boolean) => { diff --git a/test/functional/apps/visualize/_tsvb_markdown.ts b/test/functional/apps/visualize/_tsvb_markdown.ts index 880255eede5aa..89db60bc7645c 100644 --- a/test/functional/apps/visualize/_tsvb_markdown.ts +++ b/test/functional/apps/visualize/_tsvb_markdown.ts @@ -11,7 +11,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const { visualBuilder, timePicker } = getPageObjects(['visualBuilder', 'timePicker']); + const { visualBuilder, timePicker, visualize } = getPageObjects([ + 'visualBuilder', + 'timePicker', + 'visualize', + ]); const retry = getService('retry'); async function cleanupMarkdownData(variableName: 'variable' | 'label', checkedValue: string) { @@ -31,6 +35,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('visual builder', function describeIndexTests() { describe('markdown', () => { before(async () => { + await visualize.initTests(); await visualBuilder.resetPage(); await visualBuilder.clickMarkdown(); await timePicker.setAbsoluteRange( diff --git a/test/functional/apps/visualize/_tsvb_table.ts b/test/functional/apps/visualize/_tsvb_table.ts index 662ca59dc192d..abe3b799e4711 100644 --- a/test/functional/apps/visualize/_tsvb_table.ts +++ b/test/functional/apps/visualize/_tsvb_table.ts @@ -18,6 +18,9 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); describe('visual builder', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); describe('table', () => { beforeEach(async () => { await visualBuilder.resetPage('Sep 22, 2015 @ 06:00:00.000', 'Sep 22, 2015 @ 11:00:00.000'); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 58e8cd8dd0d46..a0c9d806facc6 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -26,6 +26,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('visual builder', function describeIndexTests() { + before(async () => { + await visualize.initTests(); + }); beforeEach(async () => { await visualize.navigateToNewVisualization(); await visualize.clickVisualBuilder(); diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index 15d6e81c659f9..da33b390925a4 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -43,6 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('vega chart in visualize app', () => { before(async () => { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickVega'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.ts b/test/functional/apps/visualize/_vertical_bar_chart.ts index 5dafdd5b04010..1fe0d2f9a955b 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart.ts @@ -19,6 +19,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('vertical bar chart', function () { + before(async () => { + await PageObjects.visualize.initTests(); + }); + const vizName1 = 'Visualization VerticalBarChart'; const initBarChart = async () => { diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts index 34f401b5afff6..5f066e96c6e7c 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts @@ -39,7 +39,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); }; - before(initBarChart); + before(async () => { + await PageObjects.visualize.initTests(); + await initBarChart(); + }); it('should save and load', async function () { await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName1); diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/_visualize_listing.ts index 399fa73da4bb3..90e7da1696702 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/_visualize_listing.ts @@ -18,6 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('create and delete', function () { before(async function () { + await PageObjects.visualize.initTests(); await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.visualize.deleteAllVisualizations(); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 4dff3eada1f24..eb224b3c9b879 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -7,7 +7,6 @@ */ import { FtrProviderContext } from '../../ftr_provider_context.d'; -import { UI_SETTINGS } from '../../../../src/plugins/data/common'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); @@ -19,17 +18,14 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { log.debug('Starting visualize before method'); await browser.setWindowSize(1280, 800); + await esArchiver.load('empty_kibana'); + await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', - }); }); // TODO: Remove when vislib is removed - describe('new charts library', function () { + describe('new charts library visualize ciGroup7', function () { this.tags('ciGroup7'); before(async () => { @@ -55,7 +51,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); }); - describe('', function () { + describe('visualize ciGroup9', function () { this.tags('ciGroup9'); loadTestFile(require.resolve('./_embedding_chart')); @@ -66,7 +62,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_chart_types')); }); - describe('', function () { + describe('visualize ciGroup10', function () { this.tags('ciGroup10'); loadTestFile(require.resolve('./_inspector')); @@ -78,7 +74,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('', function () { + describe('visualize ciGroup4', function () { this.tags('ciGroup4'); loadTestFile(require.resolve('./_line_chart_split_series')); @@ -95,7 +91,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_region_map')); }); - describe('', function () { + describe('visualize ciGroup12', function () { this.tags('ciGroup12'); loadTestFile(require.resolve('./_tag_cloud')); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.ts b/test/functional/apps/visualize/input_control_vis/chained_controls.ts index 7172be6c96966..18d1367b37e72 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.ts +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.ts @@ -21,6 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { this.tags('includeFirefox'); before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.loadSavedVisualization('chained input control', { navigateToVisualize: false, diff --git a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts index babe5a61a0cbb..633ba40bb0493 100644 --- a/test/functional/apps/visualize/input_control_vis/dynamic_options.ts +++ b/test/functional/apps/visualize/input_control_vis/dynamic_options.ts @@ -16,6 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/98974 describe.skip('dynamic options', () => { + before(async () => { + await PageObjects.visualize.initTests(); + }); + describe('without chained controls', () => { beforeEach(async () => { await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_options.ts b/test/functional/apps/visualize/input_control_vis/input_control_options.ts index 2e3b5d758436e..82f003440364f 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_options.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_options.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('input control options', () => { before(async () => { + await PageObjects.visualize.initTests(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickInputControlVis(); // set time range to time with no documents - input controls do not use time filter be default diff --git a/test/functional/apps/visualize/input_control_vis/input_control_range.ts b/test/functional/apps/visualize/input_control_vis/input_control_range.ts index caa008080b2a3..97e746ba4a4c0 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_range.ts +++ b/test/functional/apps/visualize/input_control_vis/input_control_range.ts @@ -14,10 +14,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const find = getService('find'); const security = getService('security'); + const PageObjects = getPageObjects(['visualize']); + const { visualize, visEditor } = getPageObjects(['visualize', 'visEditor']); describe('input control range', () => { before(async () => { + await PageObjects.visualize.initTests(); await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await esArchiver.load('kibana_sample_data_flights_index_pattern'); await visualize.navigateToNewVisualization(); @@ -48,10 +51,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await esArchiver.unload('kibana_sample_data_flights_index_pattern'); - // loading back default data - await esArchiver.loadIfNeeded('logstash_functional'); - await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); await security.testUser.restoreDefaults(); }); }); diff --git a/test/functional/apps/visualize/legacy/_data_table.ts b/test/functional/apps/visualize/legacy/_data_table.ts index 41ddbd2dfc236..6613e3d13a31b 100644 --- a/test/functional/apps/visualize/legacy/_data_table.ts +++ b/test/functional/apps/visualize/legacy/_data_table.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('legacy data table visualization', function indexPatternCreation() { before(async function () { + await PageObjects.visualize.initTests(); log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); log.debug('clickDataTable'); diff --git a/test/functional/apps/visualize/legacy/index.ts b/test/functional/apps/visualize/legacy/index.ts index 677f1b33df2b6..187e8f3f3a663 100644 --- a/test/functional/apps/visualize/legacy/index.ts +++ b/test/functional/apps/visualize/legacy/index.ts @@ -9,19 +9,21 @@ import { FtrProviderContext } from '../../../ftr_provider_context.d'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -export default function ({ getService, loadTestFile }: FtrProviderContext) { +export default function ({ getPageObjects, getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const log = getService('log'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['visualize']); describe('visualize with legacy visualizations', () => { before(async () => { + await PageObjects.visualize.initTests(); log.debug('Starting visualize legacy before method'); await browser.setWindowSize(1280, 800); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.loadIfNeeded('long_window_logstash'); - await esArchiver.load('visualize'); + await kibanaServer.importExport.load('visualize'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json new file mode 100644 index 0000000000000..758841e8d81ef --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/visualize.json @@ -0,0 +1,300 @@ +{ + "attributes": { + "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI2LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control with dynamic options", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "5d2de430-87c0-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:33:07.827Z", + "version": "WzMzLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "dynamic options input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "64983230-87bf-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:26:10.771Z", + "version": "WzMyLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "chained input control", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "68305470-87bc-11e9-a991-3b492a7c3e09", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "control_0_index_pattern", + "type": "index-pattern" + }, + { + "id": "logstash-*", + "name": "control_1_index_pattern", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-06-05T18:04:48.310Z", + "version": "WzMxLDJd" +} + +{ + "attributes": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", + "title": "test_index*" + }, + "id": "test_index*", + "references": [], + "type": "index-pattern", + "version": "WzI1LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "AreaChart [no date field]", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "AreaChart-no-date-field", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "test_index*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzM0LDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "log*" + }, + "coreMigrationVersion": "8.0.0", + "id": "log*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzM1LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "AreaChart [no time filter]", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "AreaChart-no-time-filter", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "log*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzM2LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Shared-Item Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" + }, + "coreMigrationVersion": "8.0.0", + "id": "Shared-Item-Visualization-AreaChart", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzI5LDJd" +} + +{ + "attributes": { + "description": "VegaMap", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "VegaMap", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "VegaMap", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [], + "type": "visualization", + "version": "WzM3LDJd" +} + +{ + "attributes": { + "description": "AreaChart", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "Visualization AreaChart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" + }, + "coreMigrationVersion": "8.0.0", + "id": "Visualization-AreaChart", + "migrationVersion": { + "visualization": "7.13.0" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "version": "WzMwLDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "title": "logstash*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI3LDJd" +} + +{ + "attributes": { + "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "long-window-logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "long-window-logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzI4LDJd" +} \ No newline at end of file diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 1ccea86905431..9a4c01f0f2767 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; import { VisualizeConstants } from '../../../src/plugins/visualize/public/application/visualize_constants'; +import { UI_SETTINGS } from '../../../src/plugins/data/common'; // TODO: Remove & Refactor to use the TTV page objects interface VisualizeSaveModalArgs { @@ -23,6 +24,7 @@ type DashboardPickerOption = | 'new-dashboard-option'; export function VisualizePageProvider({ getService, getPageObjects }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); @@ -48,6 +50,16 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide LOGSTASH_NON_TIME_BASED: 'logstash*', }; + public async initTests() { + await kibanaServer.savedObjects.clean({ types: ['visualization'] }); + await kibanaServer.importExport.load('visualize'); + + await kibanaServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + } + public async gotoVisualizationLandingPage() { await common.navigateToApp('visualize'); } From 65371d93d54d804c7d0eaa94f95616e8093a5132 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 13 May 2021 15:44:18 -0500 Subject: [PATCH 042/186] [index pattern field editor] Update runtime field painless docs url (#100014) * update runtime field painless docs url --- .../index_pattern_field_editor/public/lib/documentation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts index 9577f25184ba0..70f180d7cb5f2 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -11,11 +11,11 @@ import { DocLinksStart } from 'src/core/public'; export const getLinks = (docLinks: DocLinksStart) => { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + const kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`; return { - runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + runtimePainless: `${kibanaDocsBase}/managing-index-patterns.html#runtime-fields`, painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, }; }; From 4d180a455923d6f71acab56530a8ee8af2be307d Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 13 May 2021 16:16:28 -0500 Subject: [PATCH 043/186] [Workplace Search] Fix bug when transitioning to personal dashboard (#100061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unmount callback should have never been in the useEffect keyed off of the pathname. Another issue appeared earlier and I tried to fix it with the now removed conditional, but it should have been removed into it’s own useEffect that only runs when the component is unmounted, not on every route change. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../views/content_sources/source_router.test.tsx | 8 -------- .../views/content_sources/source_router.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index 528065da23af6..dda3eeea54926 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -120,14 +120,6 @@ describe('SourceRouter', () => { }); describe('reset state', () => { - it('does not reset state when switching between source tree views', () => { - mockLocation.pathname = `/sources/${contentSource.id}`; - shallow(); - unmountHandler(); - - expect(resetSourceState).not.toHaveBeenCalled(); - }); - it('resets state when leaving source tree', () => { mockLocation.pathname = '/home'; shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index cd20e32def16d..d5d6c8e541e4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -55,12 +55,12 @@ export const SourceRouter: React.FC = () => { useEffect(() => { initializeSource(sourceId); - return () => { - // We only want to reset the state when leaving the source section. Otherwise there is an unwanted flash of UI. - if (!pathname.includes(sourceId)) resetSourceState(); - }; }, [pathname]); + useEffect(() => { + return resetSourceState; + }, []); + if (dataLoading) return ; const { From 8c0993bc7300e643dd2a52de4cfccd45da037a60 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 13 May 2021 17:32:14 -0400 Subject: [PATCH 044/186] [Fleet] Fix error when searching for keys whose names have spaces (#100056) ## Summary fixes #99895 Can reproduce #99895 with something like ```shell curl 'http://localhost:5601/api/fleet/enrollment-api-keys' \ -H 'content-type: application/json' \ -H 'kbn-version: 8.0.0' \ -u elastic:changeme \ --data-raw '{"name":"with spaces","policy_id":"d6a93200-b1bd-11eb-90ac-052b474d74cd"}' ``` Kibana logs this stack trace ``` server log [10:57:07.863] [error][fleet][plugins] KQLSyntaxError: Expected AND, OR, end of input but "\" found. policy_id:"d6a93200-b1bd-11eb-90ac-052b474d74cd" AND name:with\ spaces* --------------------------------------------------------------^ at Object.fromKueryExpression (/Users/jfsiii/work/kibana/src/plugins/data/common/es_query/kuery/ast/ast.ts:52:13) at listEnrollmentApiKeys (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:37:69) at Object.generateEnrollmentAPIKey (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:160:31) at processTicksAndRejections (internal/process/task_queues.js:93:5) at postEnrollmentApiKeyHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts:53:20) at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:273:30) at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:228:11) at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28) at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20) at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20) at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32) at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9) { shortMessage: 'Expected AND, OR, end of input but "\\" found.' ``` the `kuery` value which causes the `KQLSyntaxError` is ``` policy_id:\"d6a93200-b1bd-11eb-90ac-052b474d74cd\" AND name:with\\ spaces* ``` a value without spaces, e.g. `no_spaces` ``` policy_id:\"d6a93200-b1bd-11eb-90ac-052b474d74cd\" AND name:no_spaces* ``` is converted to this query object ``` { "bool": { "filter": [ { "bool": { "should": [ { "match_phrase": { "policy_id": "d6a93200-b1bd-11eb-90ac-052b474d74cd" } } ], "minimum_should_match": 1 } }, { "bool": { "should": [ { "query_string": { "fields": [ "name" ], "query": "no_spaces*" } } ], "minimum_should_match": 1 } } ] } ``` I tried some other approaches for handling the spaces based on what I saw in the docs like `name:"\"with spaces\"` and `name:(with spaces)*`but they all failed as well, like ``` KQLSyntaxError: Expected AND, OR, end of input but "*" found. policy_id:"d6a93200-b1bd-11eb-90ac-052b474d74cd" AND name:(with spaces)* -----------------------------------------------------------------------^ at Object.fromKueryExpression (/Users/jfsiii/work/kibana/src/plugins/data/common/es_query/kuery/ast/ast.ts:52:13) at listEnrollmentApiKeys (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:37:69) at Object.generateEnrollmentAPIKey (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts:166:31) at processTicksAndRejections (internal/process/task_queues.js:93:5) at postEnrollmentApiKeyHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts:53:20) at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:273:30) at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:228:11) at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28) at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20) at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20) at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32) at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9) { shortMessage: 'Expected AND, OR, end of input but "*" found.' ``` So I logged out the query object for a successful request, and put that in a function ``` { "query": { "bool": { "filter": [ { "bool": { "should": [ { "match_phrase": { "policy_id": "d6a93200-b1bd-11eb-90ac-052b474d74cd" } } ], "minimum_should_match": 1 } }, { "bool": { "should": [ { "query_string": { "fields": [ "name" ], "query": "(with spaces) *" } } ], "minimum_should_match": 1 } } ] } } } ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../services/api_keys/enrollment_api_key.ts | 35 +++++++++++-- .../apis/enrollment_api_keys/crud.ts | 51 ++++++++++++++++++- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index f0991ab01a6ed..511a0abecbc18 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -14,6 +14,7 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/s import { esKuery } from '../../../../../../src/plugins/data/server'; import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch'; import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; +import { IngestManagerError } from '../../errors'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { agentPolicyService } from '../agent_policy'; import { escapeSearchQueryPhrase } from '../saved_object'; @@ -28,10 +29,13 @@ export async function listEnrollmentApiKeys( page?: number; perPage?: number; kuery?: string; + query?: ReturnType; showInactive?: boolean; } ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { const { page = 1, perPage = 20, kuery } = options; + const query = + options.query ?? (kuery && esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery))); const res = await esClient.search>({ index: ENROLLMENT_API_KEYS_INDEX, @@ -40,9 +44,7 @@ export async function listEnrollmentApiKeys( sort: 'created_at:desc', track_total_hits: true, ignore_unavailable: true, - body: kuery - ? { query: esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kuery)) } - : undefined, + body: query ? { query } : undefined, }); // @ts-expect-error @elastic/elasticsearch @@ -159,7 +161,7 @@ export async function generateEnrollmentAPIKey( const { items } = await listEnrollmentApiKeys(esClient, { page: page++, perPage: 100, - kuery: `policy_id:"${agentPolicyId}" AND name:${providedKeyName.replace(/ /g, '\\ ')}*`, + query: getQueryForExistingKeyNameOnPolicy(agentPolicyId, providedKeyName), }); if (items.length === 0) { hasMore = false; @@ -176,7 +178,7 @@ export async function generateEnrollmentAPIKey( k.name?.replace(providedKeyName, '').trim().match(uuidRegex) ) ) { - throw new Error( + throw new IngestManagerError( i18n.translate('xpack.fleet.serverError.enrollmentKeyDuplicate', { defaultMessage: 'An enrollment key named {providedKeyName} already exists for agent policy {agentPolicyId}', @@ -254,6 +256,29 @@ export async function generateEnrollmentAPIKey( }; } +function getQueryForExistingKeyNameOnPolicy(agentPolicyId: string, providedKeyName: string) { + const query = { + bool: { + filter: [ + { + bool: { + should: [{ match_phrase: { policy_id: agentPolicyId } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ query_string: { fields: ['name'], query: `(${providedKeyName}) *` } }], + minimum_should_match: 1, + }, + }, + ], + }, + }; + + return query; +} + export async function getEnrollmentAPIKeyById(esClient: ElasticsearchClient, apiKeyId: string) { const res = await esClient.search({ index: ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 5f38a6e050f40..25fb71ae42807 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -103,7 +103,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - it('should allow to create an enrollment api key with an agent policy', async () => { + it('should allow to create an enrollment api key with only an agent policy', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) .set('kbn-xsrf', 'xxx') @@ -115,6 +115,55 @@ export default function (providerContext: FtrProviderContext) { expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); }); + it('should allow to create an enrollment api key with agent policy and unique name', async () => { + const { body: noSpacesRes } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something', + }); + expect(noSpacesRes.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); + + const { body: hasSpacesRes } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something else', + }); + expect(hasSpacesRes.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); + + const { body: noSpacesDupe } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something', + }) + .expect(400); + + expect(noSpacesDupe).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'An enrollment key named something already exists for agent policy policy1', + }); + + const { body: hasSpacesDupe } = await supertest + .post(`/api/fleet/enrollment-api-keys`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy1', + name: 'something else', + }) + .expect(400); + expect(hasSpacesDupe).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: 'An enrollment key named something else already exists for agent policy policy1', + }); + }); + it('should create an ES ApiKey with metadata', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/enrollment-api-keys`) From fbe120d0f3a618dc2e24eac8b7678f11871ba615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 13 May 2021 23:35:38 +0200 Subject: [PATCH 045/186] fix-typo: Use of `than` instead of `then` (#100030) --- packages/kbn-legacy-logging/src/rotate/log_rotator.ts | 2 +- src/plugins/console/public/lib/autocomplete/engine.js | 2 +- .../importer/geojson_importer/geojson_importer.test.js | 2 +- .../inventory_view/components/waffle/legend_controls.tsx | 2 +- .../elasticsearch_util/elasticsearch_geo_utils.test.js | 4 ++-- .../plugins/maps/common/elasticsearch_util/total_hits.ts | 2 +- .../public/components/app/section/apm/index.test.tsx | 2 +- .../app/section/metrics/lib/format_duration.test.ts | 8 ++++---- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts index 4d57d869b9008..4b1e34839030f 100644 --- a/packages/kbn-legacy-logging/src/rotate/log_rotator.ts +++ b/packages/kbn-legacy-logging/src/rotate/log_rotator.ts @@ -149,7 +149,7 @@ export class LogRotator { if (this.usePolling && !this.shouldUsePolling) { this.log( ['warning', 'logging:rotate'], - 'Looks like your current environment support a faster algorithm then polling. You can try to disable `usePolling`' + 'Looks like your current environment support a faster algorithm than polling. You can try to disable `usePolling`' ); } diff --git a/src/plugins/console/public/lib/autocomplete/engine.js b/src/plugins/console/public/lib/autocomplete/engine.js index 7852c9da7898f..bd72af3c0e8cf 100644 --- a/src/plugins/console/public/lib/autocomplete/engine.js +++ b/src/plugins/console/public/lib/autocomplete/engine.js @@ -146,7 +146,7 @@ export function populateContext(tokenPath, context, editor, includeAutoComplete, if (!wsToUse && walkStates.length > 1 && !includeAutoComplete) { console.info( - "more then one context active for current path, but autocomplete isn't requested", + "more than one context active for current path, but autocomplete isn't requested", walkStates ); } diff --git a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js index 6b16c955c396e..6a58e863aed5f 100644 --- a/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js +++ b/x-pack/plugins/file_upload/public/importer/geojson_importer/geojson_importer.test.js @@ -286,7 +286,7 @@ describe('createChunks', () => { expect(chunks[1].length).toBe(2); }); - test('should break features into chunks containing only single feature when feature size is greater then maxChunkCharCount', () => { + test('should break features into chunks containing only single feature when feature size is greater than maxChunkCharCount', () => { const maxChunkCharCount = GEOMETRY_COLLECTION_DOC_CHARS * 0.8; const chunks = createChunks(features, ES_FIELD_TYPES.GEO_SHAPE, maxChunkCharCount); expect(chunks.length).toBe(5); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index f9e04b3d1772c..06b7739e03c54 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -173,7 +173,7 @@ export const LegendControls = ({ const errors = !boundsValidRange ? [ i18n.translate('xpack.infra.legnedControls.boundRangeError', { - defaultMessage: 'Minimum must be smaller then the maximum', + defaultMessage: 'Minimum must be smaller than the maximum', }), ] : []; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index 22b8a86158a74..a0908035c1480 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -421,7 +421,7 @@ describe('createExtentFilter', () => { }); }); - it('should make left longitude greater then right longitude when area crosses 180 meridian east to west', () => { + it('should make left longitude greater than right longitude when area crosses 180 meridian east to west', () => { const mapExtent = { maxLat: 39, maxLon: 200, @@ -440,7 +440,7 @@ describe('createExtentFilter', () => { }); }); - it('should make left longitude greater then right longitude when area crosses 180 meridian west to east', () => { + it('should make left longitude greater than right longitude when area crosses 180 meridian west to east', () => { const mapExtent = { maxLat: 39, maxLon: -100, diff --git a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts index 5de38d3f28851..be197becc6a9d 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/total_hits.ts @@ -20,7 +20,7 @@ export function isTotalHitsGreaterThan(totalHits: TotalHits, value: number) { if (value > totalHits.value) { throw new Error( i18n.translate('xpack.maps.totalHits.lowerBoundPrecisionExceeded', { - defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower then value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, + defaultMessage: `Unable to determine if total hits is greater than value. Total hits precision is lower than value. Total hits: {totalHitsString}, value: {value}. Ensure _search.body.track_total_hits is at least as large as value.`, values: { totalHitsString: JSON.stringify(totalHits, null, ''), value, diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index e2669d87d6776..67fede05f3ced 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -60,7 +60,7 @@ describe('APMSection', () => { })); }); - it('renders transaction stat less then 1k', () => { + it('renders transaction stat less than 1k', () => { const resp = { appLink: '/app/apm', stats: { diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts index b4b03b2194ef2..f3853fa5a91da 100644 --- a/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts +++ b/x-pack/plugins/observability/public/components/app/section/metrics/lib/format_duration.test.ts @@ -8,19 +8,19 @@ import { formatDuration } from './format_duration'; describe('formatDuration(seconds)', () => { - it('should work for less then a minute', () => { + it('should work for less than a minute', () => { expect(formatDuration(56)).toBe('56s'); }); - it('should work for less then a hour', () => { + it('should work for less than a hour', () => { expect(formatDuration(2000)).toBe('33m 20s'); }); - it('should work for less then a day', () => { + it('should work for less than a day', () => { expect(formatDuration(74566)).toBe('20h 42m'); }); - it('should work for more then a day', () => { + it('should work for more than a day', () => { expect(formatDuration(86400 * 3 + 3600 * 4)).toBe('3d 4h'); expect(formatDuration(86400 * 419 + 3600 * 6)).toBe('419d 6h'); }); From 50145bac63d7144f40bab95bf92c6b3987f51ccc Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 15:36:06 -0600 Subject: [PATCH 046/186] [Security Solutions] Breaks down the io-ts packages to decrease plugin size (#100058) ## Summary The io-ts package was too large and needed to broken down more by domain to decrease the lists plugin size and any other plugin wanting to use the packages will not incur big hits as well. Before we had one large io-ts package: ``` @kbn/securitysolution-io-ts-utils ``` Now we have these broken down 4 packages: ``` @kbn/securitysolution-io-ts-utils @kbn/securitysolution-io-ts-types @kbn/securitysolution-io-ts-alerting-types @kbn/securitysolution-io-ts-list-types ``` Deps between these packages are: ``` @kbn/securitysolution-io-ts-utils (none) @kbn/securitysolution-io-ts-types -> @kbn/securitysolution-io-ts-utils @kbn/securitysolution-io-ts-alerting-types -> @kbn/securitysolution-io-ts-types, @kbn/securitysolution-io-ts-utils @kbn/securitysolution-io-ts-list-types -> @kbn/securitysolution-io-ts-types, @kbn/securitysolution-io-ts-utils ``` Short description and function of each (Also in each of their README.md): ``` @kbn/securitysolution-io-ts-utils, Smallest amount of utilities such as format, validate, etc... @kbn/securitysolution-io-ts-types, Base types such as to_number, to_string, etc... @kbn/securitysolution-io-ts-alerting-types, Alerting specific types such as severity, from, to, etc... @kbn/securitysolution-io-ts-list-types, list specific types such as exception lists, exception list types, etc... ``` ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- package.json | 3 + packages/BUILD.bazel | 3 + .../BUILD.bazel | 94 +++++++++++++++++++ .../README.md | 8 ++ .../jest.config.js | 13 +++ .../package.json | 9 ++ .../src/actions/index.ts | 0 .../src/constants/index.mock.ts | 0 .../src/constants/index.ts | 0 .../src/default_actions_array/index.ts | 0 .../default_export_file_name/index.test.ts | 2 +- .../src/default_export_file_name/index.ts | 0 .../src/default_from_string/index.test.ts | 2 +- .../src/default_from_string/index.ts | 0 .../src/default_interval_string/index.test.ts | 2 +- .../src/default_interval_string/index.ts | 0 .../src/default_language_string/index.test.ts | 2 +- .../src/default_language_string/index.ts | 0 .../default_max_signals_number/index.test.ts | 2 +- .../src/default_max_signals_number/index.ts | 0 .../src/default_page/index.test.ts | 2 +- .../src/default_page/index.ts | 2 +- .../src/default_per_page/index.test.ts | 2 +- .../src/default_per_page/index.ts | 2 +- .../default_risk_score_mapping_array/index.ts | 0 .../default_severity_mapping_array/index.ts | 0 .../src/default_threat_array/index.test.ts | 2 +- .../src/default_threat_array/index.ts | 0 .../src/default_throttle_null/index.test.ts | 2 +- .../src/default_throttle_null/index.ts | 0 .../src/default_to_string/index.test.ts | 2 +- .../src/default_to_string/index.ts | 0 .../src/default_uuid/index.test.ts | 2 +- .../src/default_uuid/index.ts | 25 +++++ .../src/from/index.ts | 2 +- .../src/index.ts | 40 ++++++++ .../src/language/index.ts | 0 .../src/max_signals/index.ts | 2 +- .../src/normalized_ml_job_id/index.ts | 2 +- .../references_default_array/index.test.ts | 14 +-- .../src/references_default_array/index.ts | 0 .../src/risk_score/index.test.ts | 2 +- .../src/risk_score/index.ts | 0 .../src/risk_score_mapping/index.ts | 3 +- .../src/saved_object_attributes/index.ts | 0 .../src/severity/index.ts | 0 .../src/severity_mapping/index.ts | 2 +- .../src/threat/index.ts | 0 .../src/threat_mapping/index.test.ts | 3 +- .../src/threat_mapping/index.ts | 8 +- .../src/threat_subtechnique/index.ts | 0 .../src/threat_tactic/index.ts | 0 .../src/threat_technique/index.ts | 0 .../src/throttle/index.ts | 0 .../tsconfig.json | 19 ++++ .../BUILD.bazel | 94 +++++++++++++++++++ .../README.md | 8 ++ .../jest.config.js | 13 +++ .../package.json | 9 ++ .../src}/comment/index.mock.ts | 2 +- .../src}/comment/index.test.ts | 4 +- .../src}/comment/index.ts | 12 +-- .../src/constants/index.mock.ts | 24 +++++ .../src/constants/index.ts | 16 ++++ .../src}/create_comment/index.mock.ts | 0 .../src}/create_comment/index.test.ts | 2 +- .../src}/create_comment/index.ts | 2 +- .../src/created_at/index.ts | 0 .../src/created_by/index.ts | 0 .../src}/default_comments_array/index.test.ts | 2 +- .../src}/default_comments_array/index.ts | 0 .../index.test.ts | 2 +- .../default_create_comments_array/index.ts | 0 .../src}/default_namespace/index.test.ts | 2 +- .../src}/default_namespace/index.ts | 0 .../default_namespace_array/index.test.ts | 2 +- .../src}/default_namespace_array/index.ts | 0 .../index.test.ts | 2 +- .../default_update_comments_array/index.ts | 0 .../src/default_version_number}/index.test.ts | 2 +- .../src/default_version_number}/index.ts | 0 .../src/description/index.ts | 0 .../src}/endpoint/entries/index.mock.ts | 0 .../src}/endpoint/entries/index.test.ts | 2 +- .../src}/endpoint/entries/index.ts | 0 .../src}/endpoint/entry_match/index.mock.ts | 2 +- .../src}/endpoint/entry_match/index.test.ts | 2 +- .../src}/endpoint/entry_match/index.ts | 3 +- .../endpoint/entry_match_any/index.mock.ts | 2 +- .../endpoint/entry_match_any/index.test.ts | 2 +- .../src}/endpoint/entry_match_any/index.ts | 8 +- .../endpoint/entry_match_wildcard/index.ts | 3 +- .../src}/endpoint/entry_nested/index.mock.ts | 2 +- .../src}/endpoint/entry_nested/index.test.ts | 2 +- .../src}/endpoint/entry_nested/index.ts | 2 +- .../src}/endpoint/index.ts | 0 .../non_empty_nested_entries_array/index.ts | 0 .../src}/entries/index.mock.ts | 0 .../src}/entries/index.test.ts | 2 +- .../src}/entries/index.ts | 0 .../src}/entries_exist/index.mock.ts | 2 +- .../src}/entries_exist/index.test.ts | 2 +- .../src}/entries_exist/index.ts | 2 +- .../src}/entries_list/index.mock.ts | 2 +- .../src}/entries_list/index.test.ts | 2 +- .../src}/entries_list/index.ts | 2 +- .../src}/entry_match/index.mock.ts | 2 +- .../src}/entry_match/index.test.ts | 2 +- .../src}/entry_match/index.ts | 2 +- .../src}/entry_match_any/index.mock.ts | 2 +- .../src}/entry_match_any/index.test.ts | 2 +- .../src}/entry_match_any/index.ts | 3 +- .../src}/entry_match_wildcard/index.mock.ts | 2 +- .../src}/entry_match_wildcard/index.test.ts | 2 +- .../src}/entry_match_wildcard/index.ts | 2 +- .../src}/entry_nested/index.mock.ts | 2 +- .../src}/entry_nested/index.test.ts | 2 +- .../src}/entry_nested/index.ts | 2 +- .../src}/exception_list/index.ts | 0 .../src}/exception_list_item_type/index.ts | 0 .../src/id/index.ts | 2 +- .../src}/index.ts | 15 ++- .../src}/item_id/index.ts | 2 +- .../src}/list_operator/index.ts | 0 .../src}/lists/index.mock.ts | 2 +- .../src}/lists/index.test.ts | 2 +- .../src}/lists/index.ts | 2 +- .../src}/lists_default_array/index.test.ts | 2 +- .../src}/lists_default_array/index.ts | 0 .../src/meta/index.ts | 0 .../src/name/index.ts | 0 .../non_empty_entries_array/index.test.ts | 2 +- .../src}/non_empty_entries_array/index.ts | 0 .../index.test.ts | 2 +- .../non_empty_nested_entries_array/index.ts | 0 .../src}/os_type/index.ts | 2 +- .../src/tags/index.ts | 2 +- .../src}/type/index.ts | 0 .../src}/update_comment/index.mock.ts | 2 +- .../src}/update_comment/index.test.ts | 2 +- .../src}/update_comment/index.ts | 4 +- .../src/updated_at/index.ts | 0 .../src/updated_by/index.ts | 0 .../src/version/index.ts | 2 +- .../tsconfig.json | 19 ++++ .../BUILD.bazel | 93 ++++++++++++++++++ .../README.md | 8 ++ .../jest.config.js | 13 +++ .../package.json | 9 ++ .../src/default_array/index.test.ts | 2 +- .../src/default_array/index.ts | 0 .../src/default_boolean_false/index.test.ts | 2 +- .../src/default_boolean_false/index.ts | 0 .../src/default_boolean_true/index.test.ts | 2 +- .../src/default_boolean_true/index.ts | 0 .../src/default_empty_string/index.test.ts | 2 +- .../src/default_empty_string/index.ts | 0 .../src/default_string_array/index.test.ts | 2 +- .../src/default_string_array/index.ts | 0 .../index.test.ts | 2 +- .../src/default_string_boolean_false/index.ts | 0 .../src/default_uuid/index.test.ts | 43 +++++++++ .../src/default_uuid/index.ts | 0 .../src/empty_string_array/index.test.ts | 2 +- .../src/empty_string_array/index.ts | 0 .../src/index.ts | 28 ++++++ .../src/iso_date_string/index.test.ts | 2 +- .../src/iso_date_string/index.ts | 0 .../src/non_empty_array/index.test.ts | 2 +- .../src/non_empty_array/index.ts | 0 .../index.test.ts | 2 +- .../index.ts | 0 .../src/non_empty_string/index.test.ts | 2 +- .../src/non_empty_string/index.ts | 0 .../src/non_empty_string_array/index.test.ts | 2 +- .../src/non_empty_string_array/index.ts | 0 .../src/only_false_allowed/index.test.ts | 2 +- .../src/only_false_allowed/index.ts | 0 .../src/operator/index.ts | 0 .../src/parse_schedule_dates/index.ts | 4 - .../src/positive_integer/index.test.ts | 2 +- .../src/positive_integer/index.ts | 0 .../index.test.ts | 2 +- .../index.ts | 0 .../src/string_to_positive_number/index.ts | 0 .../src/uuid/index.test.ts | 2 +- .../src/uuid/index.ts | 0 .../tsconfig.json | 19 ++++ .../README.md | 14 +-- .../src/default_version_number/index.test.ts | 65 ------------- .../src/default_version_number/index.ts | 25 ----- .../src/index.ts | 64 ------------- x-pack/plugins/lists/common/constants.mock.ts | 2 +- .../build_exceptions_filter.test.ts | 2 +- .../exceptions/build_exceptions_filter.ts | 2 +- .../plugins/lists/common/exceptions/utils.ts | 2 +- .../common/schemas/common/schemas.test.ts | 6 +- .../lists/common/schemas/common/schemas.ts | 6 +- .../create_endpoint_list_item_schema.test.ts | 8 +- .../create_endpoint_list_item_schema.ts | 4 +- .../create_exception_list_item_schema.test.ts | 8 +- .../create_exception_list_item_schema.ts | 4 +- .../request/create_exception_list_schema.ts | 4 +- .../request/create_list_item_schema.ts | 2 +- .../schemas/request/create_list_schema.ts | 2 +- .../delete_endpoint_list_item_schema.ts | 2 +- .../delete_exception_list_item_schema.ts | 2 +- .../request/delete_exception_list_schema.ts | 2 +- .../request/delete_list_item_schema.ts | 2 +- .../schemas/request/delete_list_schema.ts | 3 +- .../export_exception_list_query_schema.ts | 2 +- .../request/find_endpoint_list_item_schema.ts | 2 +- .../find_exception_list_item_schema.ts | 8 +- .../request/find_exception_list_schema.ts | 7 +- .../schemas/request/find_list_item_schema.ts | 2 +- .../schemas/request/find_list_schema.ts | 2 +- .../request/import_list_item_query_schema.ts | 2 +- .../schemas/request/patch_list_item_schema.ts | 2 +- .../schemas/request/patch_list_schema.ts | 2 +- .../request/read_endpoint_list_item_schema.ts | 2 +- .../read_exception_list_item_schema.ts | 2 +- .../request/read_exception_list_schema.ts | 2 +- .../schemas/request/read_list_item_schema.ts | 2 +- .../schemas/request/read_list_schema.ts | 2 +- .../update_endpoint_list_item_schema.ts | 2 +- .../update_exception_list_item_schema.ts | 2 +- .../request/update_exception_list_schema.ts | 2 +- .../request/update_list_item_schema.ts | 2 +- .../schemas/request/update_list_schema.ts | 2 +- .../response/exception_list_item_schema.ts | 2 +- .../schemas/response/exception_list_schema.ts | 2 +- .../schemas/response/list_item_schema.ts | 2 +- .../common/schemas/response/list_schema.ts | 2 +- .../common/schemas/types/comment.mock.ts | 2 +- .../schemas/types/create_comment.mock.ts | 2 +- .../common/schemas/types/entries.mock.ts | 2 +- .../common/schemas/types/entry_exists.mock.ts | 2 +- .../common/schemas/types/entry_list.mock.ts | 2 +- .../common/schemas/types/entry_match.mock.ts | 2 +- .../schemas/types/entry_match_any.mock.ts | 2 +- .../types/entry_match_wildcard.mock.ts | 2 +- .../common/schemas/types/entry_nested.mock.ts | 2 +- .../schemas/types/update_comment.mock.ts | 2 +- x-pack/plugins/lists/common/shared_exports.ts | 2 +- .../components/builder/entry_renderer.tsx | 2 +- .../builder/exception_item_renderer.tsx | 2 +- .../builder/exception_items_renderer.tsx | 2 +- .../exceptions/components/builder/helpers.ts | 3 +- .../public/exceptions/transforms.test.ts | 2 +- .../plugins/lists/public/exceptions/types.ts | 2 +- .../plugins/lists/public/exceptions/utils.ts | 2 +- x-pack/plugins/lists/public/lists/types.ts | 2 +- .../lists/server/routes/delete_list_route.ts | 3 +- .../plugins/lists/server/routes/validate.ts | 6 +- .../lists/server/saved_objects/migrations.ts | 2 +- .../index_es_list_item_schema.ts | 2 +- .../elastic_query/index_es_list_schema.ts | 2 +- .../update_es_list_item_schema.ts | 2 +- .../elastic_query/update_es_list_schema.ts | 2 +- .../search_es_list_item_schema.ts | 2 +- .../elastic_response/search_es_list_schema.ts | 2 +- .../exceptions_list_so_schema.ts | 2 +- .../exception_lists/create_exception_list.ts | 2 +- .../create_exception_list_item.ts | 2 +- .../exception_lists/delete_exception_list.ts | 2 +- .../delete_exception_list_item.ts | 2 +- .../delete_exception_list_items_by_list.ts | 2 +- .../exception_list_client_types.ts | 8 +- .../exception_lists/find_exception_list.ts | 2 +- .../find_exception_list_item.ts | 2 +- .../find_exception_list_items.ts | 5 +- .../exception_lists/get_exception_list.ts | 2 +- .../get_exception_list_item.ts | 2 +- .../exception_lists/update_exception_list.ts | 2 +- .../update_exception_list_item.ts | 2 +- .../server/services/exception_lists/utils.ts | 2 +- .../server/services/items/create_list_item.ts | 2 +- .../services/items/create_list_items_bulk.ts | 2 +- .../server/services/items/delete_list_item.ts | 2 +- .../items/delete_list_item_by_value.ts | 2 +- .../server/services/items/get_list_item.ts | 2 +- .../services/items/get_list_item_by_value.ts | 2 +- .../services/items/get_list_item_by_values.ts | 2 +- .../items/search_list_item_by_values.ts | 2 +- .../server/services/items/update_list_item.ts | 2 +- .../items/write_lines_to_bulk_list_items.ts | 2 +- .../server/services/lists/create_list.ts | 2 +- .../lists/create_list_if_it_does_not_exist.ts | 8 +- .../server/services/lists/delete_list.ts | 2 +- .../lists/server/services/lists/get_list.ts | 2 +- .../services/lists/list_client_types.ts | 2 +- .../server/services/lists/update_list.ts | 2 +- .../services/utils/find_source_type.test.ts | 2 +- .../server/services/utils/find_source_type.ts | 2 +- .../services/utils/find_source_value.ts | 2 +- .../utils/get_query_filter_from_type_value.ts | 2 +- ...sform_elastic_named_search_to_list_item.ts | 2 +- .../utils/transform_elastic_to_list_item.ts | 2 +- .../transform_list_item_to_elastic_query.ts | 2 +- .../add_exception_modal/index.test.tsx | 2 +- .../edit_exception_modal/index.test.tsx | 2 +- .../components/exceptions/helpers.test.tsx | 2 +- .../endpoint/lib/artifacts/lists.test.ts | 2 +- .../server/endpoint/lib/artifacts/lists.ts | 2 +- .../filters/filter_events_against_list.ts | 2 +- yarn.lock | 9 ++ 306 files changed, 897 insertions(+), 423 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/README.md create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/package.json rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/actions/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/constants/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/constants/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_actions_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_export_file_name/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_export_file_name/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_from_string/index.test.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_from_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_interval_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_interval_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_language_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_language_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_max_signals_number/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_max_signals_number/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_page/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_page/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_per_page/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_per_page/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_risk_score_mapping_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_severity_mapping_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_threat_array/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_threat_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_throttle_null/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_throttle_null/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_to_string/index.test.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_to_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/default_uuid/index.test.ts (95%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/from/index.ts (92%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/language/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/max_signals/index.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/normalized_ml_job_id/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/references_default_array/index.test.ts (77%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/references_default_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/risk_score/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/risk_score/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/risk_score_mapping/index.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/saved_object_attributes/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/severity/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/severity_mapping/index.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_mapping/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_mapping/index.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_subtechnique/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_tactic/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/threat_technique/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-alerting-types}/src/throttle/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json create mode 100644 packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel create mode 100644 packages/kbn-securitysolution-io-ts-list-types/README.md create mode 100644 packages/kbn-securitysolution-io-ts-list-types/jest.config.js create mode 100644 packages/kbn-securitysolution-io-ts-list-types/package.json rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/comment/index.mock.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/comment/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/comment/index.ts (77%) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/create_comment/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/create_comment/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/create_comment/index.ts (93%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/created_at/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/created_by/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_comments_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_comments_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_create_comments_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_create_comments_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace_array/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_namespace_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_update_comments_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/default_update_comments_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/deafult_version_number => kbn-securitysolution-io-ts-list-types/src/default_version_number}/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils/src/deafult_version_number => kbn-securitysolution-io-ts-list-types/src/default_version_number}/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/description/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entries/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entries/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entries/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match/index.mock.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match/index.ts (84%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_any/index.mock.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_any/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_any/index.ts (76%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_match_wildcard/index.ts (85%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_nested/index.mock.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_nested/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/entry_nested/index.ts (91%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/endpoint/non_empty_nested_entries_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries/index.mock.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_exist/index.mock.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_exist/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_exist/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_list/index.mock.ts (86%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_list/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entries_list/index.ts (91%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match/index.mock.ts (88%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_any/index.mock.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_any/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_any/index.ts (82%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_wildcard/index.mock.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_wildcard/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_match_wildcard/index.ts (91%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_nested/index.mock.ts (94%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_nested/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/entry_nested/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/exception_list/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/exception_list_item_type/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/id/index.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/index.ts (78%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/item_id/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/list_operator/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists/index.mock.ts (93%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists/index.ts (93%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists_default_array/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/lists_default_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/meta/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/name/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_entries_array/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_entries_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_nested_entries_array/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/non_empty_nested_entries_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/os_type/index.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/tags/index.ts (89%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/type/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/update_comment/index.mock.ts (92%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/update_comment/index.test.ts (98%) rename packages/{kbn-securitysolution-io-ts-utils/src/list_types => kbn-securitysolution-io-ts-list-types/src}/update_comment/index.ts (90%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/updated_at/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/updated_by/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-list-types}/src/version/index.ts (90%) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/tsconfig.json create mode 100644 packages/kbn-securitysolution-io-ts-types/BUILD.bazel create mode 100644 packages/kbn-securitysolution-io-ts-types/README.md create mode 100644 packages/kbn-securitysolution-io-ts-types/jest.config.js create mode 100644 packages/kbn-securitysolution-io-ts-types/package.json rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_false/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_false/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_true/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_boolean_true/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_empty_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_empty_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_array/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_boolean_false/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_string_boolean_false/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/default_uuid/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/empty_string_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/empty_string_array/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-types/src/index.ts rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/iso_date_string/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/iso_date_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_or_nullable_string_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_or_nullable_string_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string_array/index.test.ts (97%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/non_empty_string_array/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/only_false_allowed/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/only_false_allowed/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/operator/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/parse_schedule_dates/index.ts (85%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer_greater_than_zero/index.test.ts (96%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/positive_integer_greater_than_zero/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/string_to_positive_number/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/uuid/index.test.ts (95%) rename packages/{kbn-securitysolution-io-ts-utils => kbn-securitysolution-io-ts-types}/src/uuid/index.ts (100%) create mode 100644 packages/kbn-securitysolution-io-ts-types/tsconfig.json delete mode 100644 packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts delete mode 100644 packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts diff --git a/package.json b/package.json index c04face1233ae..b79724dbb63bc 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,9 @@ "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", + "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", + "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", + "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index c3d08ad49daea..a9c87043575fa 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -25,6 +25,9 @@ filegroup( "//packages/kbn-logging:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-constants:build", + "//packages/kbn-securitysolution-io-ts-types:build", + "//packages/kbn-securitysolution-io-ts-alerting-types:build", + "//packages/kbn-securitysolution-io-ts-list-types:build", "//packages/kbn-securitysolution-io-ts-utils:build", "//packages/kbn-securitysolution-utils:build", "//packages/kbn-securitysolution-es-utils:build", diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel new file mode 100644 index 0000000000000..ba7123d0c1f21 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-alerting-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-alerting-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/README.md b/packages/kbn-securitysolution-io-ts-alerting-types/README.md new file mode 100644 index 0000000000000..b8fa8234f2d85 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-alerting-types + +Types that are specific to the security solution alerting to be shared among plugins. + +Related packages are +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js new file mode 100644 index 0000000000000..6125b95a9bce5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-alerting-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/package.json b/packages/kbn-securitysolution-io-ts-alerting-types/package.json new file mode 100644 index 0000000000000..ac972e06c1dc9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-alerting-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts index 1f81f056386d7..f0fe7f44a6f3e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultExportFileName } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_export_file_name', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_export_file_name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts index c1261f514540b..ccfb7923a230c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultFromString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_from_string', () => { test('it should validate a from string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_from_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts index c4a0dc3664d0e..f5706677e6c5d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultIntervalString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_interval_string', () => { test('it should validate a interval string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_interval_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts index 072c541a808a3..82bd8607dae72 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Language } from '../language'; import { DefaultLanguageString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_language_string', () => { test('it should validate a string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_language_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts index bf703fa52d844..eb2af1dbea41a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultMaxSignalsNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { DEFAULT_MAX_SIGNALS } from '../constants'; describe('default_from_string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_max_signals_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts index 3bcad15a7ebb8..cca1c7e2774f4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_page', () => { test('it should validate a regular number greater than zero', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts index 056005b452a03..f9140be68ec8d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_page/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Types the DefaultPerPage as: diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts index f7361ba12a570..88e91986a65dd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultPerPage } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_per_page', () => { test('it should validate a regular number greater than zero', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts index 026642f91c08a..ea8f30c745062 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_per_page/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Types the DefaultPerPage as: diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_risk_score_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_severity_mapping_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts index ac86b5508ff14..5f1ef3fc61fab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Threats } from '../threat'; import { DefaultThreatArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_threat_null', () => { test('it should validate an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_threat_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts index 4b8877bd532c2..b92815d4fe828 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { Throttle } from '../throttle'; import { DefaultThrottleNull } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_throttle_null', () => { test('it should validate a throttle string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_throttle_null/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts index bcab8ebd5f17c..31c35c8319fab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultToString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_to_string', () => { test('it should validate a to string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_to_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts index d8cdff416037c..c471141a99a76 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultUuid } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_uuid', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts new file mode 100644 index 0000000000000..73bf807e92c43 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/default_uuid/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +/** + * Types the DefaultUuid as: + * - If null or undefined, then a default string uuid.v4() will be + * created otherwise it will be checked just against an empty string + */ +export const DefaultUuid = new t.Type( + 'DefaultUuid', + t.string.is, + (input, context): Either => + input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/from/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts index 963e2fa0444f0..3bf4592a581f5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts @@ -8,7 +8,7 @@ import { Either } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { parseScheduleDates } from '../parse_schedule_dates'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-types'; const stringValidator = (input: unknown): input is string => typeof input === 'string'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts new file mode 100644 index 0000000000000..639140be049f2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './actions'; +export * from './constants'; +export * from './default_actions_array'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_risk_score_mapping_array'; +export * from './default_severity_mapping_array'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './from'; +export * from './language'; +export * from './max_signals'; +export * from './normalized_ml_job_id'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './risk_score_mapping'; +export * from './saved_object_attributes'; +export * from './severity'; +export * from './severity_mapping'; +export * from './threat'; +export * from './threat_mapping'; +export * from './threat_subtechnique'; +export * from './threat_tactic'; +export * from './threat_technique'; +export * from './throttle'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/language/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts index 4c68cb01cf00f..83360234c65a1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; export const max_signals = PositiveIntegerGreaterThanZero; export type MaxSignals = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts index 6c7eb0ae33ab9..db26264c029cd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/normalized_ml_job_id/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; -import { NonEmptyArray } from '../non_empty_array'; +import { NonEmptyArray } from '@kbn/securitysolution-io-ts-types'; export const machine_learning_job_id_normalized = NonEmptyArray(t.string); export type MachineLearningJobIdNormalized = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts similarity index 77% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts index 41754a7ce0606..38fd27ac40fdf 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.test.ts @@ -8,13 +8,13 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { DefaultStringArray } from '../default_string_array'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { ReferencesDefaultArray } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_array', () => { test('it should validate an empty array', () => { const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -23,7 +23,7 @@ describe('default_string_array', () => { test('it should validate an array of strings', () => { const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); @@ -32,18 +32,18 @@ describe('default_string_array', () => { test('it should not validate an array with a number', () => { const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', + 'Invalid value "5" supplied to "referencesWithDefaultArray"', ]); expect(message.schema).toEqual({}); }); test('it should return a default array entry', () => { const payload = null; - const decoded = DefaultStringArray.decode(payload); + const decoded = ReferencesDefaultArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/references_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts index bca8b92134928..d341ca8b3b4f7 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { RiskScore } from '.'; describe('risk_score', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts index 1d7ca20e80b3b..b35b502811ec9 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts @@ -9,10 +9,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { operator } from '@kbn/securitysolution-io-ts-types'; import { RiskScore } from '../risk_score'; -import { operator } from '../operator'; - export const riskScoreOrUndefined = t.union([RiskScore, t.undefined]); export type RiskScoreOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/saved_object_attributes/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts index 9e7ee7d2831cd..1a3fd50039c29 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/severity_mapping/index.ts @@ -10,7 +10,7 @@ import * as t from 'io-ts'; -import { operator } from '../operator'; +import { operator } from '@kbn/securitysolution-io-ts-types'; import { severity } from '../severity'; export const severity_mapping_field = t.string; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts index 7f754fb2d87de..16fd1647e5bfc 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.test.ts @@ -16,8 +16,7 @@ import { ThreatMappingEntries, threat_mapping, } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; -import { exactCheck } from '../exact_check'; +import { foldLeftRight, getPaths, exactCheck } from '@kbn/securitysolution-io-ts-utils'; describe('threat_mapping', () => { describe('threatMappingEntries', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts index 4fc64fe1e0982..abee0d2baceb0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_mapping/index.ts @@ -9,10 +9,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; +import { + NonEmptyArray, + NonEmptyString, + PositiveIntegerGreaterThanZero, +} from '@kbn/securitysolution-io-ts-types'; import { language } from '../language'; -import { NonEmptyArray } from '../non_empty_array'; -import { NonEmptyString } from '../non_empty_string'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; export const threat_query = t.string; export type ThreatQuery = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_tactic/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts rename to packages/kbn-securitysolution-io-ts-alerting-types/src/throttle/index.ts diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json new file mode 100644 index 0000000000000..3411ce2c93d05 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-alerting-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel new file mode 100644 index 0000000000000..e9b806288addd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -0,0 +1,94 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-list-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-list-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-types", + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-list-types/README.md b/packages/kbn-securitysolution-io-ts-list-types/README.md new file mode 100644 index 0000000000000..090ede2ed7d62 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-list-types + +io-ts types that are specific to lists to be shared among plugins + +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-ts-utils +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-list-types/jest.config.js b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js new file mode 100644 index 0000000000000..0312733b6a02b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-list-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-list-types/package.json b/packages/kbn-securitysolution-io-ts-list-types/package.json new file mode 100644 index 0000000000000..74893e59855bc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-list-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts index 56440d628e4aa..380f7f13b6210 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.mock.ts @@ -7,7 +7,7 @@ */ import { Comment, CommentsArray } from '.'; -import { DATE_NOW, ID, USER } from '../../constants/index.mock'; +import { DATE_NOW, ID, USER } from '../constants/index.mock'; export const getCommentsMock = (): Comment => ({ comment: 'some old comment', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts index 0f0bfac5e2068..89e734a92fd04 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.test.ts @@ -17,8 +17,8 @@ import { CommentsArrayOrUndefined, commentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; -import { DATE_NOW } from '../../constants/index.mock'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { DATE_NOW } from '../constants/index.mock'; describe('Comment', () => { describe('comment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts similarity index 77% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts index 783d8606b8a96..3b8cc6cc6ce95 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/comment/index.ts @@ -8,12 +8,12 @@ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { created_at } from '../../created_at'; -import { created_by } from '../../created_by'; -import { id } from '../../id'; -import { updated_at } from '../../updated_at'; -import { updated_by } from '../../updated_by'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { created_at } from '../created_at'; +import { created_by } from '../created_by'; +import { id } from '../id'; +import { updated_at } from '../updated_at'; +import { updated_by } from '../updated_by'; export const comment = t.intersection([ t.exact( diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts new file mode 100644 index 0000000000000..d2107ae864f15 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export const ENTRY_VALUE = 'some host name'; +export const FIELD = 'host.name'; +export const MATCH = 'match'; +export const MATCH_ANY = 'match_any'; +export const OPERATOR = 'included'; +export const NESTED = 'nested'; +export const NESTED_FIELD = 'parent.field'; +export const LIST_ID = 'some-list-id'; +export const LIST = 'list'; +export const TYPE = 'ip'; +export const EXISTS = 'exists'; +export const WILDCARD = 'wildcard'; +export const USER = 'some user'; +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; + +// Exception List specific +export const ID = 'uuid_here'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts new file mode 100644 index 0000000000000..f86986fc328c5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list. + * + * TODO: Create a kbn-securitysolution-constants and add this to it. + * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts index 1ac605e232ea1..3baf0054221db 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.test.ts @@ -17,7 +17,7 @@ import { CreateCommentsArrayOrUndefined, createCommentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('CreateComment', () => { describe('createComment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts index 438f946e796d6..883675ce51f91 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/create_comment/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const createComment = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/created_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts index 5e667380e2adf..440c601876682 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { CommentsArray } from '../comment'; import { DefaultCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getCommentsArrayMock } from '../comment/index.mock'; describe('default_comments_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts index a4581fabbf6a9..de45fd9f300fa 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { CommentsArray } from '../comment'; import { DefaultCommentsArray } from '../default_comments_array'; import { getCommentsArrayMock } from '../comment/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_create_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts index 1decca0de6c50..21e8c375b3d01 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultNamespace } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_namespace', () => { test('it should validate "single"', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts index 8bc7a16b96097..b02a3b96a5a3d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_namespace_array', () => { test('it should validate "null" single item as an array with a "single" value', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_namespace_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts index f52baa49530ec..fa6613538b18e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { UpdateCommentsArray } from '../update_comment'; import { DefaultUpdateCommentsArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getUpdateCommentsArrayMock } from '../update_comment/index.mock'; describe('default_update_comments_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_update_comments_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts index f77903d2d030d..fd7b12123b6bb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultVersionNumber } from '../default_version_number'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_version_number', () => { test('it should validate a version number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/description/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/description/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts index f5cb89ee79607..09f1740567bc1 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.test.ts @@ -14,7 +14,7 @@ import { nonEmptyEndpointEntriesArray, NonEmptyEndpointEntriesArray, } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; import { getEndpointEntriesArrayMock } from './index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts index 7104406c4869c..17a1a083d73d8 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.mock.ts @@ -7,7 +7,7 @@ */ import { EndpointEntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts index cc0423fc119c7..fc3a2dded177d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointEntryMatchMock } from './index.mock'; import { EndpointEntryMatch, endpointEntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../../entry_match/index.mock'; describe('endpointEntryMatch', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts similarity index 84% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts index 83e2a0f61bb4a..07a1fc58a3d54 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match/index.ts @@ -7,8 +7,7 @@ */ import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatch = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts index 95bd6008f1d7c..13fb16d73457d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.mock.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; import { EndpointEntryMatchAny } from '.'; export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts index 0fd878986d5a2..cf64647772519 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointEntryMatchAnyMock } from './index.mock'; import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock'; describe('endpointEntryMatchAny', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts similarity index 76% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts index b39a428bb49dd..23c15767a511c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_any/index.ts @@ -7,9 +7,11 @@ */ import * as t from 'io-ts'; -import { nonEmptyOrNullableStringArray } from '../../../non_empty_or_nullable_string_array'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { + NonEmptyString, + nonEmptyOrNullableStringArray, + operatorIncluded, +} from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatchAny = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts similarity index 85% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts index b66c5a2588eef..2697f3edc3db4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_match_wildcard/index.ts @@ -7,8 +7,7 @@ */ import * as t from 'io-ts'; -import { operatorIncluded } from '../../../operator'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString, operatorIncluded } from '@kbn/securitysolution-io-ts-types'; export const endpointEntryMatchWildcard = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts index f59e29c8ce526..31d983ba58fe3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.mock.ts @@ -7,7 +7,7 @@ */ import { EndpointEntryNested } from '.'; -import { FIELD, NESTED } from '../../../constants/index.mock'; +import { FIELD, NESTED } from '../../constants/index.mock'; import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts index 03c02f67b71ad..f8e54e4956527 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EndpointEntryNested, endpointEntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEndpointEntryNestedMock } from './index.mock'; import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts index 249dcc9077b34..bd4c90d851a90 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/entry_nested/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array'; export const endpointEntryNested = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/endpoint/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.mock.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts index b6e448f94ce6a..f68fea35e6fdf 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { entriesArray, entriesArrayOrUndefined, entry } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; import { getEntryListMock } from '../entries_list/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts index 0882883f4d239..ad2164a3862eb 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryExists } from '.'; -import { EXISTS, FIELD, OPERATOR } from '../../constants/index.mock'; +import { EXISTS, FIELD, OPERATOR } from '../constants/index.mock'; export const getEntryExistsMock = (): EntryExists => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts index db4edb54dfc29..05451b11de7a6 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryExistsMock } from './index.mock'; import { entriesExists, EntryExists } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesExists', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts index f8f1ddecc9ff9..6d65d458583bd 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_exist/index.ts @@ -8,8 +8,8 @@ import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; -import { NonEmptyString } from '../../non_empty_string'; export const entriesExists = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts similarity index 86% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts index c4afb28f5ac54..2349b9d5ab2b3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryList } from '.'; -import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants/index.mock'; +import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../constants/index.mock'; export const getEntryListMock = (): EntryList => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts index 2be3803c356de..5b72242777875 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.test.ts @@ -11,7 +11,7 @@ import { left } from 'fp-ts/lib/Either'; import { getEntryListMock } from './index.mock'; import { entriesList, EntryList } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesList', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts index b386ca35d2bbb..61d3c7b156fd2 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entries_list/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { type } from '../type'; import { listOperator as operator } from '../list_operator'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts similarity index 88% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts index 4fdd8d915fe04..38c9f0f922c46 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatch } from '.'; -import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../constants/index.mock'; export const getEntryMatchMock = (): EntryMatch => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts index 744c74c1223df..bff65ad7f6bec 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchMock } from './index.mock'; import { entriesMatch, EntryMatch } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatch', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts index cab6d0dd4a07f..4f04e01cf8f63 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; export const entriesMatch = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts index 0022b00c604b0..efaf23fe1e784 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatchAny } from '.'; -import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../constants/index.mock'; export const getEntryMatchAnyMock = (): EntryMatchAny => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts index 60fc4cdc26005..c0eb017fdab54 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchAnyMock } from './index.mock'; import { entriesMatchAny, EntryMatchAny } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatchAny', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts similarity index 82% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts index 0add9a610f30b..86e97c579a02c 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_any/index.ts @@ -8,9 +8,8 @@ import * as t from 'io-ts'; +import { NonEmptyString, nonEmptyOrNullableStringArray } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; -import { nonEmptyOrNullableStringArray } from '../../non_empty_or_nullable_string_array'; -import { NonEmptyString } from '../../non_empty_string'; export const entriesMatchAny = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts index 9810fe5e9875b..f81a8c6cba2ef 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryMatchWildcard } from '.'; -import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants/index.mock'; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../constants/index.mock'; export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ field: FIELD, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts index d9170dd60ab40..8a5a152ce7e65 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryMatchWildcardMock } from './index.mock'; import { entriesMatchWildcard, EntryMatchWildcard } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('entriesMatchWildcard', () => { test('it should validate an entry', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts similarity index 91% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts index aab5ba5e8e32c..ea1953b983d45 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_match_wildcard/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { listOperator as operator } from '../list_operator'; export const entriesMatchWildcard = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts similarity index 94% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts index acde4443cccb7..05f42cdf69bc0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.mock.ts @@ -7,7 +7,7 @@ */ import { EntryNested } from '.'; -import { NESTED, NESTED_FIELD } from '../../constants/index.mock'; +import { NESTED, NESTED_FIELD } from '../constants/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; import { getEntryMatchExcludeMock, getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from '../entry_match_any/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts index b6bbc4dbef4a3..b21737535fd77 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEntryNestedMock } from './index.mock'; import { entriesNested, EntryNested } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts index ff224dd836a19..f5ac68cc98702 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/entry_nested/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { nonEmptyNestedEntriesArray } from '../non_empty_nested_entries_array'; export const entriesNested = t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/exception_list_item_type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/id/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts index 7b187d7730f73..5952bd2eda21f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/id/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const id = NonEmptyString; export type Id = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts similarity index 78% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/index.ts index 9dd58e2a5a177..1a1c1c3314821 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts @@ -5,13 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + export * from './comment'; +export * from './constants'; export * from './create_comment'; +export * from './created_at'; +export * from './created_by'; export * from './default_comments_array'; export * from './default_create_comments_array'; export * from './default_namespace'; export * from './default_namespace_array'; export * from './default_update_comments_array'; +export * from './default_version_number'; +export * from './description'; export * from './endpoint'; export * from './entries'; export * from './entries_exist'; @@ -22,12 +28,19 @@ export * from './entry_match_wildcard'; export * from './entry_nested'; export * from './exception_list'; export * from './exception_list_item_type'; +export * from './id'; export * from './item_id'; +export * from './list_operator'; export * from './lists'; export * from './lists_default_array'; +export * from './meta'; +export * from './name'; export * from './non_empty_entries_array'; export * from './non_empty_nested_entries_array'; -export * from './list_operator'; export * from './os_type'; +export * from './tags'; export * from './type'; export * from './update_comment'; +export * from './updated_at'; +export * from './updated_by'; +export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts index 171db8fd60fd1..dcb03884eadab 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/item_id/index.ts @@ -9,7 +9,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; export const item_id = NonEmptyString; export type ItemId = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/list_operator/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/list_operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts index c6f54b57d937b..e9f34c4cf789f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.mock.ts @@ -7,7 +7,7 @@ */ import { List, ListArray } from '.'; -import { ENDPOINT_LIST_ID } from '../../constants'; +import { ENDPOINT_LIST_ID } from '../constants'; export const getListMock = (): List => ({ id: 'some_uuid', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts index 77d5e72ef8bc8..88dcc1ced8607 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { getEndpointListMock, getListArrayMock, getListMock } from './index.mock'; import { List, list, ListArray, listArray, ListArrayOrUndefined, listArrayOrUndefined } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('Lists', () => { describe('list', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts similarity index 93% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts index 1bd1806564856..7881a6bb3322e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists/index.ts @@ -7,9 +7,9 @@ */ import * as t from 'io-ts'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { exceptionListType } from '../exception_list'; import { namespaceType } from '../default_namespace'; -import { NonEmptyString } from '../../non_empty_string'; export const list = t.exact( t.type({ diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts index 03d16d8e1b5ca..58a52d26aa34f 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultListArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getListArrayMock } from '../lists/index.mock'; describe('lists_default_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/lists_default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/meta/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/name/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/name/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts index 11e6e54b344a9..98976f3cd6d21 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EntriesArray } from '../entries'; import { nonEmptyEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts index 95b74a6d4fe43..8ac958577f8d7 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.test.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EntriesArray } from '../entries'; import { nonEmptyNestedEntriesArray } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getEntryMatchMock } from '../entry_match/index.mock'; import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; import { getEntryExistsMock } from '../entries_exist/index.mock'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/non_empty_nested_entries_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts index 5ff60e05817d5..b7fa544c956ee 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/os_type/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { DefaultArray } from '../../default_array'; +import { DefaultArray } from '@kbn/securitysolution-io-ts-types'; export const osType = t.keyof({ linux: null, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts similarity index 89% rename from packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts index 48bcca0551352..f0f23d9e4717d 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/tags/index.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { DefaultStringArray } from '../default_string_array'; +import { DefaultStringArray } from '@kbn/securitysolution-io-ts-types'; export const tags = DefaultStringArray; export type Tags = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts similarity index 92% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts index 3b5cb256b28bf..e9a56119dcc20 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.mock.ts @@ -7,7 +7,7 @@ */ import { UpdateComment, UpdateCommentsArray } from '.'; -import { ID } from '../../constants/index.mock'; +import { ID } from '../constants/index.mock'; export const getUpdateCommentMock = (): UpdateComment => ({ comment: 'some comment', diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts similarity index 98% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts index a6fc285f05465..8dd0301c54dd8 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.test.ts @@ -17,7 +17,7 @@ import { UpdateCommentsArrayOrUndefined, updateCommentsArrayOrUndefined, } from '.'; -import { foldLeftRight, getPaths } from '../../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('CommentsUpdate', () => { describe('updateComment', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts index 496ff07c5616f..5499690c97716 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/update_comment/index.ts @@ -7,8 +7,8 @@ */ import * as t from 'io-ts'; -import { NonEmptyString } from '../../non_empty_string'; -import { id } from '../../id'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { id } from '../id'; export const updateComment = t.intersection([ t.exact( diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_at/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/updated_by/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts similarity index 90% rename from packages/kbn-securitysolution-io-ts-utils/src/version/index.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts index 38cb47ebce53e..97a81b546c841 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; /** * Note this is just a positive number, but we use it as a type here which is still ok. diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json new file mode 100644 index 0000000000000..d926653a4230b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-list-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel new file mode 100644 index 0000000000000..0a21f5ed94f01 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -0,0 +1,93 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-types" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-types" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-securitysolution-io-ts-utils", + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-types/README.md b/packages/kbn-securitysolution-io-ts-types/README.md new file mode 100644 index 0000000000000..552c663d819e3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/README.md @@ -0,0 +1,8 @@ +# kbn-securitysolution-io-ts-types + +Generic io-ts types that are not specific to any particular domain for use with other packages or across different plugins/domains + +Related packages are: +* kbn-securitysolution-io-ts-utils +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-alerting-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-types/jest.config.js b/packages/kbn-securitysolution-io-ts-types/jest.config.js new file mode 100644 index 0000000000000..18d31eaa75219 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-types'], +}; diff --git a/packages/kbn-securitysolution-io-ts-types/package.json b/packages/kbn-securitysolution-io-ts-types/package.json new file mode 100644 index 0000000000000..0381a6d24a136 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-types", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts index 82fa884b1c577..4ca45e7de3377 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.test.ts @@ -11,7 +11,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; const testSchema = t.keyof({ valid: true, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts index bddf9cc0747ea..c87a67ec4e5d4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_boolean_false', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts index a05fb586c2e92..3ec33fda392e4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultBooleanTrue } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_boolean_true', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_boolean_true/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts index 5bdc9b298649e..02fb74510d604 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultEmptyString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_empty_string', () => { test('it should validate a regular string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts index c7137d9c56b0d..7b1f217f55ad5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_array', () => { test('it should validate an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts index 2443e8f71fecd..3e96c942de74a 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { DefaultStringBooleanFalse } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('default_string_boolean_false', () => { test('it should validate a boolean false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_string_boolean_false/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts new file mode 100644 index 0000000000000..c471141a99a76 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultUuid } from '.'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts index 86ffba6eeb60a..5b7863947cad4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { EmptyStringArray, EmptyStringArrayEncoded } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('empty_string_array', () => { test('it should validate "null" and create an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts new file mode 100644 index 0000000000000..8b5a4d9e4de9a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -0,0 +1,28 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './default_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_uuid'; +export * from './empty_string_array'; +export * from './iso_date_string'; +export * from './non_empty_array'; +export * from './non_empty_or_nullable_string_array'; +export * from './non_empty_string'; +export * from './non_empty_string_array'; +export * from './operator'; +export * from './only_false_allowed'; +export * from './parse_schedule_dates'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './string_to_positive_number'; +export * from './uuid'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts index 4b73ed1b136dc..e70a738d7336e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { IsoDateString } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/iso_date_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts index 0ea7eb5539ba9..0586195360142 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.test.ts @@ -11,7 +11,7 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { NonEmptyArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; const testSchema = t.keyof({ valid: true, diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts index fb2e91862d91e..355bd9d20061e 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { nonEmptyOrNullableStringArray } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('nonEmptyOrNullableStringArray', () => { test('it should FAIL validation when given an empty array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_or_nullable_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts index 15c8ced8c915f..ae3b8cd9acad5 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { NonEmptyString } from '.'; describe('non_empty_string', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts similarity index 97% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts index 9fec36f46dd27..f56fa7faed2a4 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.test.ts @@ -8,7 +8,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { NonEmptyStringArray } from '.'; describe('non_empty_string_array', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/non_empty_string_array/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/non_empty_string_array/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts index 7f06ec2153a50..de05872c0dc31 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { OnlyFalseAllowed } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('only_false_allowed', () => { test('it should validate a boolean false as false', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts b/packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/only_false_allowed/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts b/packages/kbn-securitysolution-io-ts-types/src/operator/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/operator/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts similarity index 85% rename from packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts index a2cc15d82391c..d6a99b5fbf880 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts @@ -9,10 +9,6 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; -/** - * TODO: Move this to kbn-securitysolution-utils - * @deprecated Use the parseScheduleDates from the kbn-securitysolution-utils. - */ export const parseScheduleDates = (time: string): moment.Moment | null => { const isValidDateString = !isNaN(Date.parse(time)); const isValidInput = isValidDateString || time.trim().startsWith('now'); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts index c6c841b746089..deea8951a3d39 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PositiveInteger } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts similarity index 96% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts index 4655207a6448e..4ea6fe920cf14 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { PositiveIntegerGreaterThanZero } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts b/packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/positive_integer_greater_than_zero/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts b/packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/string_to_positive_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts similarity index 95% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts index e8214ac60313f..4333fab102d44 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.test.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; import { UUID } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts b/packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/uuid/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json new file mode 100644 index 0000000000000..42a059439ecb5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-types/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-utils/README.md b/packages/kbn-securitysolution-io-ts-utils/README.md index 908651b50b80a..146f965391aa0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/README.md +++ b/packages/kbn-securitysolution-io-ts-utils/README.md @@ -1,10 +1,12 @@ # kbn-securitysolution-io-ts-utils -Temporary location for all the io-ts-utils from security solutions. This is a lift-and-shift, where -we are moving them here for phase 1. +Very small set of utilities for io-ts which we use across plugins within security solutions such as securitysolution, lists, cases, etc... +This folder should remain small and concise since it is pulled into front end and the more files we add the more weight will be added to all +of the plugins. Also, any new dependencies added to this will add weight here and the other plugins, so be careful of what is added here. -Phase 2 is deprecating across plugins any copied code or sharing of io-ts utils that are now in here. +You might consider making another package instead and putting a dependency on this one if needed, instead. -Phase 3 is replacing those deprecated types with the ones in here. - -Phase 4+ is (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project \ No newline at end of file +Related packages are +* kbn-securitysolution-io-ts-alerting-types +* kbn-securitysolution-io-ts-list-types +* kbn-securitysolution-io-ts-types \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts deleted file mode 100644 index b9e9a3ff367e4..0000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { DefaultVersionNumber } from '.'; -import { foldLeftRight, getPaths } from '../test_utils'; - -describe('default_version_number', () => { - test('it should validate a version number', () => { - const payload = 5; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a 0', () => { - const payload = 0; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a -1', () => { - const payload = -1; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 1', () => { - const payload = null; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts deleted file mode 100644 index 245ff9d0db7dd..0000000000000 --- a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { version, Version } from '../version'; - -/** - * Types the DefaultVersionNumber as: - * - If null or undefined, then a default of the number 1 will be used - */ -export const DefaultVersionNumber = new t.Type( - 'DefaultVersionNumber', - version.is, - (input, context): Either => - input == null ? t.success(1) : version.validate(input, context), - t.identity -); - -export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts index 1a18293393af5..c21096e497134 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -7,71 +7,7 @@ */ export * from './format_errors'; -export * from './actions'; -export * from './constants'; -export * from './created_at'; -export * from './created_by'; -export * from './default_version_number'; -export * from './default_actions_array'; -export * from './default_array'; -export * from './default_boolean_false'; -export * from './default_boolean_true'; -export * from './default_empty_string'; -export * from './default_export_file_name'; -export * from './default_from_string'; -export * from './default_interval_string'; -export * from './default_language_string'; -export * from './default_max_signals_number'; -export * from './default_page'; -export * from './default_per_page'; -export * from './default_risk_score_mapping_array'; -export * from './default_severity_mapping_array'; -export * from './default_string_array'; -export * from './default_string_boolean_false'; -export * from './default_threat_array'; -export * from './default_throttle_null'; -export * from './default_to_string'; -export * from './default_uuid'; -export * from './default_version_number'; -export * from './description'; -export * from './empty_string_array'; export * from './exact_check'; export * from './format_errors'; -export * from './from'; -export * from './id'; -export * from './iso_date_string'; -export * from './language'; -export * from './list_types'; -export * from './max_signals'; -export * from './meta'; -export * from './name'; -export * from './non_empty_array'; -export * from './non_empty_or_nullable_string_array'; -export * from './non_empty_string'; -export * from './non_empty_string_array'; -export * from './normalized_ml_job_id'; -export * from './only_false_allowed'; -export * from './operator'; -export * from './parse_schedule_dates'; -export * from './positive_integer'; -export * from './positive_integer_greater_than_zero'; -export * from './references_default_array'; -export * from './risk_score'; -export * from './risk_score_mapping'; -export * from './saved_object_attributes'; -export * from './severity'; -export * from './severity_mapping'; -export * from './string_to_positive_number'; -export * from './tags'; export * from './test_utils'; -export * from './threat'; -export * from './threat_mapping'; -export * from './threat_subtechnique'; -export * from './threat_tactic'; -export * from './threat_technique'; -export * from './throttle'; -export * from './updated_at'; -export * from './updated_by'; -export * from './uuid'; export * from './validate'; -export * from './version'; diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 90f4825b97d43..325ed48113966 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -13,7 +13,7 @@ import { EntryMatch, EntryNested, OsTypeArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const DATE_NOW = '2020-04-20T15:25:31.830Z'; export const OLD_DATE_RELATIVE_TO_DATE_NOW = '2020-04-19T15:25:31.830Z'; diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts index fa073b3b4cfb6..ae0cfbfbfc425 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-list-types'; import { getEntryMatchExcludeMock, getEntryMatchMock } from '../schemas/types/entry_match.mock'; import { diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts index 0fa069ba51013..eda81f91cd983 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts @@ -15,7 +15,7 @@ import { entriesMatch, entriesMatchAny, entriesNested, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { Filter } from '../../../../../src/plugins/data/common'; import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../schemas'; diff --git a/x-pack/plugins/lists/common/exceptions/utils.ts b/x-pack/plugins/lists/common/exceptions/utils.ts index 689687e44256a..f5881c1d3cbf4 100644 --- a/x-pack/plugins/lists/common/exceptions/utils.ts +++ b/x-pack/plugins/lists/common/exceptions/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index 2b007f01b56eb..c83691ead2ee6 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -11,15 +11,13 @@ import { ExceptionListTypeEnum, ListOperatorEnum as OperatorEnum, Type, - exactCheck, exceptionListType, - foldLeftRight, - getPaths, listOperator as operator, osType, osTypeArrayOrUndefined, type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('Common schemas', () => { describe('operator', () => { diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index eb84ee07981f3..612b7ea559e4a 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -8,18 +8,20 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { DefaultNamespace, NonEmptyString } from '@kbn/securitysolution-io-ts-utils'; +import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespace } from '@kbn/securitysolution-io-ts-list-types'; /** * @deprecated Directly use the type from the package and not from here */ export { + DefaultNamespace, Type, OsType, OsTypeArray, listOperator as operator, NonEmptyEntriesArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const list_id = NonEmptyString; export type ListId = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts index 30f3acc8a164a..e6287a87c86ef 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.test.ts @@ -7,12 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - CommentsArray, - exactCheck, - foldLeftRight, - getPaths, -} from '@kbn/securitysolution-io-ts-utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts index af58c61dbaf9f..322e31aacd040 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_endpoint_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { CreateCommentsArray, DefaultCreateCommentsArray, - DefaultUuid, EntriesArray, OsTypeArray, Tags, @@ -20,7 +19,8 @@ import { nonEmptyEndpointEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ItemId } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts index 1bb58d6195e7c..7e8d16663cf5d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts @@ -7,12 +7,8 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - CommentsArray, - exactCheck, - foldLeftRight, - getPaths, -} from '@kbn/securitysolution-io-ts-utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { getCreateCommentsArrayMock } from '../types/create_comment.mock'; import { getCommentsMock } from '../types/comment.mock'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts index da5630ef3f002..d37c7f7aa67b2 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { CreateCommentsArray, DefaultCreateCommentsArray, - DefaultUuid, EntriesArray, NamespaceType, OsTypeArray, @@ -21,7 +20,8 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ItemId, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 42955ddbd7017..91b3a98bdd5ac 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import { - DefaultUuid, DefaultVersionNumber, DefaultVersionNumberDecoded, NamespaceType, @@ -19,7 +18,8 @@ import { name, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; import { ListId, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts index 867b441960a2c..d11bd03ced916 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 8ac36cc3ad28e..5fa9da0cdc597 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -14,7 +14,7 @@ import { meta, name, type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializer, serializer } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts index b8ff0834e8fb8..0b714885437a8 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts index cc188bf52d75c..5c6fc9c158b3b 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts index b816c08beb363..2d1d00a6759cf 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts index 5b4aa63d2d090..9cb46b3e36f45 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, valueOrUndefined } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts index 003dfdc6bd466..0d6bbc73a2571 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -6,7 +6,8 @@ */ import * as t from 'io-ts'; -import { DefaultStringBooleanFalse, id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; +import { DefaultStringBooleanFalse } from '@kbn/securitysolution-io-ts-types'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts index d3c18f0d1c485..47bb1b70ad8b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_exception_list_query_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts index 13f45a070b2b7..06b28ea6cbb4e 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts index 4abceb4b3592d..d92bfbec02f5a 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts @@ -7,13 +7,15 @@ import * as t from 'io-ts'; import { - DefaultNamespaceArray, - DefaultNamespaceArrayTypeDecoded, EmptyStringArray, EmptyStringArrayDecoded, NonEmptyStringArray, StringToPositiveNumber, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-types'; +import { + DefaultNamespaceArray, + DefaultNamespaceArrayTypeDecoded, +} from '@kbn/securitysolution-io-ts-list-types'; import { sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts index ea5b5c5aafdb6..6cf31c56ea599 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts @@ -6,11 +6,8 @@ */ import * as t from 'io-ts'; -import { - DefaultNamespaceArray, - NamespaceTypeArray, - StringToPositiveNumber, -} from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespaceArray, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts index 6adf53d0eda86..e0d072780bbf8 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts index bf6a68d97a58e..4d929d581370c 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-utils'; +import { StringToPositiveNumber } from '@kbn/securitysolution-io-ts-types'; import { cursor, filter, sort_field, sort_order } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index 85644ff556443..cef803ffa5e45 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { type } from '@kbn/securitysolution-io-ts-utils'; +import { type } from '@kbn/securitysolution-io-ts-list-types'; import { RequiredKeepUndefined } from '../../types'; import { deserializer, list_id, serializer } from '../common/schemas'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts index edea4f161f248..2989919421a3c 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index 144bf9c0f28a0..eea4ba9fc87d7 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name } from '@kbn/securitysolution-io-ts-utils'; +import { description, id, meta, name } from '@kbn/securitysolution-io-ts-list-types'; import { _version, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts index 116c70012c17e..3f221b473f432 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_endpoint_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts index a0bd46b30d2f6..9094296e56196 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { item_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts index fc8a6ee43a5a2..9a361e04900ed 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { NamespaceType, id } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts index 450719f42ad4a..0bfa99ee078a1 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; import { list_id, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts index e07e2de1a4b80..5d850b19c4d11 100644 --- a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id } from '@kbn/securitysolution-io-ts-utils'; +import { id } from '@kbn/securitysolution-io-ts-list-types'; export const readListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts index d9e602419d61d..011ff24b7fa22 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_endpoint_list_item_schema.ts @@ -20,7 +20,7 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts index f3b87c5ff5925..1c751dd3a8c83 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts @@ -21,7 +21,7 @@ import { nonEmptyEntriesArray, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index c8b354eff4d9e..c58c1c253a8c4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -17,7 +17,7 @@ import { name, osTypeArrayOrUndefined, tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _version, list_id, namespace_type, version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts index 84916f15a59f6..f24902a12d3b7 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { id, meta } from '@kbn/securitysolution-io-ts-utils'; +import { id, meta } from '@kbn/securitysolution-io-ts-list-types'; import { _version, value } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index 6f520d399d577..230853e69fae4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-utils'; +import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-list-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts index 769cfb3548ced..0b6f8a7640529 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts @@ -20,7 +20,7 @@ import { tags, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 880c2d4f89e4f..7bfc2af9863e2 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -18,7 +18,7 @@ import { tags, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts index 1f105afac8b44..3f11718bc42e6 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -14,7 +14,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index 58abe94772ff6..21504d64fdeaa 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -16,7 +16,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { _versionOrUndefined, diff --git a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts index 75b4b6a431ac3..5963cb4947a85 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { Comment, CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { DATE_NOW, ID, USER } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts index 2d8dd7b462258..868c43fe5d6da 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { CreateComment, CreateCommentsArray } from '@kbn/securitysolution-io-ts-list-types'; export const getCreateCommentsMock = (): CreateComment => ({ comment: 'some comments', diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index ee43a0b26ad54..caa62c55c93bb 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { getEntryMatchMock } from './entry_match.mock'; import { getEntryMatchAnyMock } from './entry_match_any.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts index 3e26d261f44ca..6165184d2a404 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryExists } from '@kbn/securitysolution-io-ts-utils'; +import { EntryExists } from '@kbn/securitysolution-io-ts-list-types'; import { EXISTS, FIELD, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts index 7eadfcdf3454c..1cdc86d95ed88 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryList } from '@kbn/securitysolution-io-ts-utils'; +import { EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts index bc0eb3b5c4f85..efcd1e0877d1b 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatch } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatch } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts index 74c3abbaa5881..60613fc72baba 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchAny } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchAny } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts index 320664bd2f833..17e0cbd25901c 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-utils'; +import { EntryMatchWildcard } from '@kbn/securitysolution-io-ts-list-types'; import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts index f1d0a2bc76926..2497c3d4c3ce2 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { NESTED, NESTED_FIELD } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts index dea0f9a08fc4c..783b595850bc5 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-utils'; +import { UpdateComment, UpdateCommentsArray } from '@kbn/securitysolution-io-ts-list-types'; import { ID } from '../../constants.mock'; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 38eb5aeee8cd2..bc9d0ca8d7b94 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -42,7 +42,7 @@ export { osTypeArray, OsTypeArray, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export { ListSchema, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 28d7469d18910..0ece28d409bd5 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 5f094a64c3660..94c3bff8f4cf9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 646803f2e6794..4ec152e155e39 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 6d3bdd09c93ea..18d607d6807fc 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -7,7 +7,8 @@ import uuid from 'uuid'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray, validate } from '@kbn/securitysolution-io-ts-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/lists/public/exceptions/transforms.test.ts b/x-pack/plugins/lists/public/exceptions/transforms.test.ts index c5c43b16d6428..b2a1efc1d2c1d 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.test.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { Entry, EntryMatch, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../common/schemas/response/exception_list_item_schema'; import { UpdateExceptionListItemSchema } from '../../common/schemas/request/update_exception_list_item_schema'; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index a2842d81a7292..0cad700b2b598 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { CreateExceptionListItemSchema, diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts index 6324fdf1df420..c840a25b2a103 100644 --- a/x-pack/plugins/lists/public/exceptions/utils.ts +++ b/x-pack/plugins/lists/public/exceptions/utils.ts @@ -6,7 +6,7 @@ */ import { get } from 'lodash/fp'; -import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts index 6708620439803..ad82a63163ce3 100644 --- a/x-pack/plugins/lists/public/lists/types.ts +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { HttpStart } from '../../../../../src/core/public'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 033c49aa7b235..78235584bc0cd 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { EntriesArray, validate } from '@kbn/securitysolution-io-ts-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { ListsPluginRouter } from '../types'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index 005d9e85f4853..2577770cf32ef 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -8,14 +8,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; +import { exactCheck, formatErrors, validate } from '@kbn/securitysolution-io-ts-utils'; import { NamespaceType, NonEmptyEntriesArray, - exactCheck, - formatErrors, nonEmptyEndpointEntriesArray, - validate, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 316c5f1311774..485bd493f309e 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -13,7 +13,7 @@ import { OsTypeArray, entriesNested, entry, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { ExceptionListSoSchema } from '../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts index 696434a616c53..42788c15736b7 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_item_schema.ts @@ -12,7 +12,7 @@ import { metaOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { esDataTypeUnion } from '../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts index c69abaf785dec..4383e93346291 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts @@ -15,7 +15,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts index 1f49943a910bc..383b6f339bb58 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_item_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-utils'; +import { metaOrUndefined, updated_at, updated_by } from '@kbn/securitysolution-io-ts-list-types'; import { esDataTypeUnion } from '../common/schemas'; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts index fbeac92c66bdd..fe73d0fb9207f 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/update_es_list_schema.ts @@ -12,7 +12,7 @@ import { nameOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; export const updateEsListSchema = t.exact( t.type({ diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts index 8ac88a1610ea7..c787f70bfa675 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_item_schema.ts @@ -12,7 +12,7 @@ import { metaOrUndefined, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { binaryOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts index a060ebda04a46..536269b9c0ae2 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts @@ -15,7 +15,7 @@ import { type, updated_at, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { deserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts index f6d2e891a60d0..c1f480e50c8f7 100644 --- a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts +++ b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts @@ -19,7 +19,7 @@ import { osTypeArray, tags, updated_by, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { immutableOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index ef4ceb2f12922..5f2587fc1e986 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -14,7 +14,7 @@ import { Name, NamespaceType, Tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, Immutable, ListId, Version } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 5f88244171f6a..0bcc888a4c313 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -17,7 +17,7 @@ import { NamespaceType, OsTypeArray, Tags, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, ItemId, ListId } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts index afe9106e28d82..201cb9544a8f3 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, ListIdOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts index d0e1d2283cc6f..9f735fd51c7f2 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { Id, IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, ItemIdOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index d9ec08b818f2d..b08872eac8e01 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; import { ListId } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 0954a55d44dcc..576b0c4d25aa0 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -10,7 +10,6 @@ import { CreateCommentsArray, Description, DescriptionOrUndefined, - EmptyStringArrayDecoded, EntriesArray, ExceptionListItemType, ExceptionListItemTypeOrUndefined, @@ -23,12 +22,15 @@ import { NameOrUndefined, NamespaceType, NamespaceTypeArray, - NonEmptyStringArrayDecoded, OsTypeArray, Tags, TagsOrUndefined, UpdateCommentsArray, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; +import { + EmptyStringArrayDecoded, + NonEmptyStringArrayDecoded, +} from '@kbn/securitysolution-io-ts-types'; import { FilterOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 82ea5a4f104c5..dfe7a97d0b2f3 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectType } from '../../../common/types'; import { diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index cb9c16ffe3c7b..b75520614150b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { FilterOrUndefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index 6721aff5b0c1e..ad4646a57a5ca 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -6,12 +6,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { Id, NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { EmptyStringArrayDecoded, - Id, - NamespaceTypeArray, NonEmptyStringArrayDecoded, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-types'; import { SavedObjectType, diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index 342e03160b45b..928190efbf531 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract, diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index cf469baa46370..be612868abe48 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectsClientContract, diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index 69d9b87227bca..3daa2e9157b5d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -15,7 +15,7 @@ import { NamespaceType, OsTypeArray, TagsOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListSchema, diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 041008a06f3df..0d9ba8d8fefcc 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -17,7 +17,7 @@ import { OsTypeArray, TagsOrUndefined, UpdateCommentsArrayOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema, diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 1322f153bf3bd..12fe8eabd4f6a 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -16,7 +16,7 @@ import { UpdateCommentsArrayOrUndefined, exceptionListItemType, exceptionListType, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { SavedObjectType, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index 3c51f56c7916a..ebeef3e90933d 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; -import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { IdOrUndefined, MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 5928260ab94ac..00956a7c3c3fa 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { ElasticsearchClient } from 'kibana/server'; -import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { transformListItemToElasticQuery } from '../utils'; import { DeserializerOrUndefined, SerializerOrUndefined } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index 4fcb2656d2ba7..c08e683aafa1c 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index ccbe8d6fe7925..1adcf45e85748 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index aca8deac24817..a1653cb31ce16 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema } from '../../../common/schemas'; import { transformElasticToListItem } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index 083dca2ea9410..a190f9388bef3 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 5a4d55172af23..0fcb958940d9b 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts index d6d8f66770653..2b525fde6a428 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchListItemArraySchema } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 91c38dd3f331c..4f1a19430aeda 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-utils'; +import { Id, MetaOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemSchema, _VersionOrUndefined } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 8a05e4667a290..b3ce823f9ac29 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -8,7 +8,7 @@ import { Readable } from 'stream'; import { ElasticsearchClient } from 'kibana/server'; -import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-utils'; +import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 6b0954f3fcc9d..d139ef3ea4bb1 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -13,7 +13,7 @@ import { MetaOrUndefined, Name, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { encodeHitVersion } from '../utils/encode_hit_version'; import { diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts index 483810a9b1c43..71094a5ab49de 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -6,7 +6,13 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Description, Id, MetaOrUndefined, Name, Type } from '@kbn/securitysolution-io-ts-utils'; +import { + Description, + Id, + MetaOrUndefined, + Name, + Type, +} from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 0e140544fa47d..a215044b92b4c 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListSchema } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index a248f81449bfc..7ff17bc2ee553 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { Id } from '@kbn/securitysolution-io-ts-utils'; +import { Id } from '@kbn/securitysolution-io-ts-list-types'; import { ListSchema } from '../../../common/schemas'; import { transformElasticToList } from '../utils/transform_elastic_to_list'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index b684511ff679c..b4fe52019ec7b 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -17,7 +17,7 @@ import { Name, NameOrUndefined, Type, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined, diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 4917fec7397ea..374c3cd0e2def 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -11,7 +11,7 @@ import { Id, MetaOrUndefined, NameOrUndefined, -} from '@kbn/securitysolution-io-ts-utils'; +} from '@kbn/securitysolution-io-ts-list-types'; import { decodeVersion } from '../utils/decode_version'; import { encodeHitVersion } from '../utils/encode_hit_version'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts index e408f7d33b548..80b10142d553a 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { getSearchEsListItemMock } from '../../schemas/elastic_response/search_es_list_item_schema.mock'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_type.ts b/x-pack/plugins/lists/server/services/utils/find_source_type.ts index 00a6985b2c751..e69eecbbe3129 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_type.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_type.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type, type } from '@kbn/securitysolution-io-ts-utils'; +import { Type, type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/find_source_value.ts b/x-pack/plugins/lists/server/services/utils/find_source_value.ts index c12f4bdfcdb9f..7990481c3e3db 100644 --- a/x-pack/plugins/lists/server/services/utils/find_source_value.ts +++ b/x-pack/plugins/lists/server/services/utils/find_source_value.ts @@ -6,7 +6,7 @@ */ import Mustache from 'mustache'; -import { type } from '@kbn/securitysolution-io-ts-utils'; +import { type } from '@kbn/securitysolution-io-ts-list-types'; import { DeserializerOrUndefined } from '../../../common/schemas'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts index 0ece97b21d5b7..6a30cb5d6a847 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -6,7 +6,7 @@ */ import { isEmpty, isObject } from 'lodash/fp'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; export type QueryFilterType = [ { term: Record }, diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index f02ae17fa0293..902fc17039792 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SearchListItemArraySchema } from '../../../common/schemas'; import { SearchEsListItemSchema } from '../../schemas/elastic_response'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 3e27bd24517e4..1cbf72e8eb653 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListItemArraySchema } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts index 32eb885871cb1..fc97bef54b0a6 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '@kbn/securitysolution-io-ts-utils'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { SerializerOrUndefined } from '../../../common/schemas'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index d659f557ee751..5ec8999d20518 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -21,7 +21,7 @@ import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_e import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../../lists/common'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index 64ef1dead7e75..ab6d4b401bb41 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -20,7 +20,7 @@ import { import { useAddOrUpdateException } from '../use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { getRulesEqlSchemaMock, getRulesSchemaMock, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 0560b790e4047..907b30fcaa879 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -56,7 +56,7 @@ import { ENTRIES_WITH_IDS, OLD_DATE_RELATIVE_TO_DATE_NOW, } from '../../../../../lists/common/constants.mock'; -import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { CreateExceptionListItemSchema, ExceptionListItemSchema, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index ee6962f7e9535..5b4aed35bbc7c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -9,7 +9,7 @@ import { ExceptionListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-utils'; +import { EntriesArray, EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { buildArtifact, getEndpointExceptionList, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7a5b906860f10..f3bc195b5a896 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import { deflate } from 'zlib'; -import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-utils'; +import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { validate } from '../../../../common/validate'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 58df4b3f11412..f50f0b521ed76 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { entriesList } from '@kbn/securitysolution-io-ts-utils'; +import { entriesList } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; diff --git a/yarn.lock b/yarn.lock index b8b4e54d25dcc..4857c7c908293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2707,6 +2707,15 @@ uid "" "@kbn/securitysolution-es-utils@link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module": +"@kbn/securitysolution-io-ts-alerting-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module": + version "0.0.0" + uid "" + +"@kbn/securitysolution-io-ts-list-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module": + version "0.0.0" + uid "" + +"@kbn/securitysolution-io-ts-types@link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module": version "0.0.0" uid "" From ad4fcd29e6ada82dcce0c305d7050309c9d4641a Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 16:37:28 -0600 Subject: [PATCH 047/186] Removes circular deps for lists in tooling and bumps down byte limit for lists (#100082) ## Summary * Removes circular deps exception for lists * Bumps down byte limit for lists now that we have decreased the page bytes to be under 200kb --- packages/kbn-optimizer/limits.yml | 2 +- src/dev/run_find_plugins_with_circular_deps.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 448b5ad650da5..5748984c7bc6e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -46,7 +46,7 @@ pageLoadAssetSize: lens: 96624 licenseManagement: 41817 licensing: 29004 - lists: 280504 + lists: 200000 logstash: 53548 management: 46112 maps: 80000 diff --git a/src/dev/run_find_plugins_with_circular_deps.ts b/src/dev/run_find_plugins_with_circular_deps.ts index a737bc6a73004..4ce71b24332c1 100644 --- a/src/dev/run_find_plugins_with_circular_deps.ts +++ b/src/dev/run_find_plugins_with_circular_deps.ts @@ -19,9 +19,7 @@ interface Options { type CircularDepList = Set; -const allowedList: CircularDepList = new Set([ - 'x-pack/plugins/lists -> x-pack/plugins/security_solution', -]); +const allowedList: CircularDepList = new Set([]); run( async ({ flags, log }) => { From c604ee8862c7a7f66546e2707cecd6553f359003 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 13 May 2021 17:26:12 -0600 Subject: [PATCH 048/186] Updates the monorepo-packages list (#100096) ## Summary Updates the monorepo-packages list --- docs/developer/getting-started/monorepo-packages.asciidoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 7265cd415949c..92dc2a1a24377 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -82,12 +82,14 @@ yarn kbn watch-bazel - @kbn/legacy-logging - @kbn/logging - @kbn/securitysolution-constants -- @kbn/securitysolution-utils - @kbn/securitysolution-es-utils +- kbn/securitysolution-io-ts-alerting-types +- kbn/securitysolution-io-ts-list-types +- kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils +- @kbn/securitysolution-utils - @kbn/std - @kbn/telemetry-utils - @kbn/tinymath - @kbn/utility-types - @kbn/utils - From aab9806ca56ab7b848267ded97c43911138650ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 14 May 2021 10:24:08 +0200 Subject: [PATCH 049/186] Disable contextMenu when event is not event.kind=event (#100027) --- .../timelines/components/timeline/body/actions/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 015c4c0b45949..0824dea0803ba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -115,6 +115,11 @@ const ActionsComponent: React.FC = ({ ); const eventType = getEventType(ecsData); + const isEventContextMenuEnabled = useMemo( + () => isEventFilteringEnabled && !!ecsData.event?.kind && ecsData.event?.kind[0] === 'event', + [ecsData.event?.kind, isEventFilteringEnabled] + ); + return ( <> {showCheckboxes && ( @@ -197,7 +202,7 @@ const ActionsComponent: React.FC = ({ key="alert-context-menu" ecsRowData={ecsData} timelineId={timelineId} - disabled={eventType !== 'signal' && (!isEventFilteringEnabled || eventType !== 'raw')} + disabled={eventType !== 'signal' && !isEventContextMenuEnabled} refetch={refetch} onRuleChange={onRuleChange} /> From f2aa5b13d4099083c04a271395ea996d669473f5 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 14 May 2021 08:31:03 -0400 Subject: [PATCH 050/186] Introduce capabilities provider and switcher to file upload plugin (#96593) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../file_upload/server/capabilities.test.ts | 267 ++++++++++++++++++ .../file_upload/server/capabilities.ts | 47 +++ .../file_upload/server/check_privileges.ts | 55 ++++ x-pack/plugins/file_upload/server/plugin.ts | 3 + x-pack/plugins/file_upload/server/routes.ts | 32 +-- x-pack/plugins/security/server/index.ts | 1 + 6 files changed, 382 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/file_upload/server/capabilities.test.ts create mode 100644 x-pack/plugins/file_upload/server/capabilities.ts create mode 100644 x-pack/plugins/file_upload/server/check_privileges.ts diff --git a/x-pack/plugins/file_upload/server/capabilities.test.ts b/x-pack/plugins/file_upload/server/capabilities.test.ts new file mode 100644 index 0000000000000..2fc666c837961 --- /dev/null +++ b/x-pack/plugins/file_upload/server/capabilities.test.ts @@ -0,0 +1,267 @@ +/* + * 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 { setupCapabilities } from './capabilities'; +import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; +import { Capabilities, CoreStart } from 'kibana/server'; +import { securityMock } from '../../security/server/mocks'; + +describe('setupCapabilities', () => { + it('registers a capabilities provider for the file upload feature', () => { + const coreSetup = coreMock.createSetup(); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); + const [provider] = coreSetup.capabilities.registerProvider.mock.calls[0]; + expect(provider()).toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": true, + }, + } + `); + }); + + it('registers a capabilities switcher that returns unaltered capabilities when security is disabled', async () => { + const coreSetup = coreMock.createSetup(); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + }); + + it('registers a capabilities switcher that returns unaltered capabilities when default capabilities are requested', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, true)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).not.toHaveBeenCalled(); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); + + it('registers a capabilities switcher that disables capabilities for underprivileged users', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: false }); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": false, + }, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); + }); + + it('registers a capabilities switcher that enables capabilities for privileged users', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + + const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: true }); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request); + }); + + it('registers a capabilities switcher that disables capabilities for unauthenticated requests', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(true); + const mockCheckPrivileges = jest + .fn() + .mockRejectedValue(new Error('this should not have been called')); + security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } }); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "fileUpload": Object { + "show": false, + }, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); + + it('registers a capabilities switcher that skips privilege check for requests not using rbac', async () => { + const coreSetup = coreMock.createSetup(); + const security = securityMock.createStart(); + security.authz.mode.useRbacForRequest.mockReturnValue(false); + coreSetup.getStartServices.mockResolvedValue([ + (undefined as unknown) as CoreStart, + { security }, + undefined, + ]); + setupCapabilities(coreSetup); + + expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1); + const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0]; + + const capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + fileUpload: { + show: true, + }, + } as Capabilities; + + const request = httpServerMock.createKibanaRequest(); + + await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "fileUpload": Object { + "show": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1); + expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request); + expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/file_upload/server/capabilities.ts b/x-pack/plugins/file_upload/server/capabilities.ts new file mode 100644 index 0000000000000..17880b98150d6 --- /dev/null +++ b/x-pack/plugins/file_upload/server/capabilities.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 { CoreSetup } from 'kibana/server'; +import { checkFileUploadPrivileges } from './check_privileges'; +import { StartDeps } from './types'; + +export const setupCapabilities = ( + core: Pick, 'capabilities' | 'getStartServices'> +) => { + core.capabilities.registerProvider(() => { + return { + fileUpload: { + show: true, + }, + }; + }); + + core.capabilities.registerSwitcher(async (request, capabilities, useDefaultCapabilities) => { + if (useDefaultCapabilities) { + return capabilities; + } + const [, { security }] = await core.getStartServices(); + + // Check the bare minimum set of privileges required to get some utility out of this feature + const { hasImportPermission } = await checkFileUploadPrivileges({ + authorization: security?.authz, + request, + checkCreateIndexPattern: true, + checkHasManagePipeline: false, + }); + + if (!hasImportPermission) { + return { + fileUpload: { + show: false, + }, + }; + } + + return capabilities; + }); +}; diff --git a/x-pack/plugins/file_upload/server/check_privileges.ts b/x-pack/plugins/file_upload/server/check_privileges.ts new file mode 100644 index 0000000000000..42cc53f693fec --- /dev/null +++ b/x-pack/plugins/file_upload/server/check_privileges.ts @@ -0,0 +1,55 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { AuthorizationServiceSetup, CheckPrivilegesPayload } from '../../security/server'; + +interface Deps { + request: KibanaRequest; + authorization?: Pick< + AuthorizationServiceSetup, + 'mode' | 'actions' | 'checkPrivilegesDynamicallyWithRequest' + >; + checkHasManagePipeline: boolean; + checkCreateIndexPattern: boolean; + indexName?: string; +} + +export const checkFileUploadPrivileges = async ({ + request, + authorization, + checkHasManagePipeline, + checkCreateIndexPattern, + indexName, +}: Deps) => { + const requiresAuthz = authorization?.mode.useRbacForRequest(request) ?? false; + + if (!authorization || !requiresAuthz) { + return { hasImportPermission: true }; + } + + if (!request.auth.isAuthenticated) { + return { hasImportPermission: false }; + } + + const checkPrivilegesPayload: CheckPrivilegesPayload = { + elasticsearch: { + cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], + index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, + }, + }; + if (checkCreateIndexPattern) { + checkPrivilegesPayload.kibana = [ + authorization.actions.savedObject.get('index-pattern', 'create'), + ]; + } + + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(request); + const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); + + return { hasImportPermission: checkPrivilegesResp.hasAllRequested }; +}; diff --git a/x-pack/plugins/file_upload/server/plugin.ts b/x-pack/plugins/file_upload/server/plugin.ts index 5a4b59fe4f5e6..80fe041207110 100644 --- a/x-pack/plugins/file_upload/server/plugin.ts +++ b/x-pack/plugins/file_upload/server/plugin.ts @@ -13,6 +13,7 @@ import { initFileUploadTelemetry } from './telemetry'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { UI_SETTING_MAX_FILE_SIZE, MAX_FILE_SIZE } from '../common'; import { StartDeps } from './types'; +import { setupCapabilities } from './capabilities'; interface SetupDeps { usageCollection: UsageCollectionSetup; @@ -28,6 +29,8 @@ export class FileUploadPlugin implements Plugin { async setup(coreSetup: CoreSetup, plugins: SetupDeps) { fileUploadRoutes(coreSetup, this._logger); + setupCapabilities(coreSetup); + coreSetup.uiSettings.register({ [UI_SETTING_MAX_FILE_SIZE]: { name: i18n.translate('xpack.fileUpload.maxFileSizeUiSetting.name', { diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index 847a57afb391c..3033f8300712c 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -22,8 +22,8 @@ import { analyzeFile } from './analyze_file'; import { updateTelemetry } from './telemetry'; import { importFileBodySchema, importFileQuerySchema, analyzeFileQuerySchema } from './schemas'; -import { CheckPrivilegesPayload } from '../../security/server'; import { StartDeps } from './types'; +import { checkFileUploadPrivileges } from './check_privileges'; function importData( client: IScopedClusterClient, @@ -60,29 +60,15 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge const [, pluginsStart] = await coreSetup.getStartServices(); const { indexName, checkCreateIndexPattern, checkHasManagePipeline } = request.query; - const authorizationService = pluginsStart.security?.authz; - const requiresAuthz = authorizationService?.mode.useRbacForRequest(request) ?? false; - - if (!authorizationService || !requiresAuthz) { - return response.ok({ body: { hasImportPermission: true } }); - } - - const checkPrivilegesPayload: CheckPrivilegesPayload = { - elasticsearch: { - cluster: checkHasManagePipeline ? ['manage_pipeline'] : [], - index: indexName ? { [indexName]: ['create', 'create_index'] } : {}, - }, - }; - if (checkCreateIndexPattern) { - checkPrivilegesPayload.kibana = [ - authorizationService.actions.savedObject.get('index-pattern', 'create'), - ]; - } - - const checkPrivileges = authorizationService.checkPrivilegesDynamicallyWithRequest(request); - const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload); + const { hasImportPermission } = await checkFileUploadPrivileges({ + authorization: pluginsStart.security?.authz, + request, + indexName, + checkCreateIndexPattern, + checkHasManagePipeline, + }); - return response.ok({ body: { hasImportPermission: checkPrivilegesResp.hasAllRequested } }); + return response.ok({ body: { hasImportPermission } }); } catch (e) { logger.warn(`Unable to check import permission, error: ${e.message}`); return response.ok({ body: { hasImportPermission: false } }); diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 087cf8f4f8ee8..e50ab66a92547 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -27,6 +27,7 @@ export type { GrantAPIKeyResult, } from './authentication'; export type { CheckPrivilegesPayload } from './authorization'; +export type AuthorizationServiceSetup = SecurityPluginStart['authz']; export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; From 94a1e59319b589cf8732363dc0320bfd55e80db4 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 14 May 2021 15:41:37 +0200 Subject: [PATCH 051/186] [Lens] Remove separate mounting point for editor frame to use redux freely (#99892) remove separate mounting point for editor frame --- .../lens/public/app_plugin/app.test.tsx | 146 ++++++++-------- x-pack/plugins/lens/public/app_plugin/app.tsx | 160 +++++++++--------- .../lens/public/app_plugin/mounter.tsx | 1 - .../plugins/lens/public/app_plugin/types.ts | 4 - .../editor_frame_service/service.test.tsx | 89 ---------- .../public/editor_frame_service/service.tsx | 60 +++---- x-pack/plugins/lens/public/types.ts | 3 +- 7 files changed, 168 insertions(+), 295 deletions(-) delete mode 100644 x-pack/plugins/lens/public/editor_frame_service/service.test.tsx diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 87000865850e1..72b8bfa38491a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -45,7 +45,6 @@ import { import { LensAttributeService } from '../lens_attribute_service'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; -import { NativeRenderer } from '../native_renderer'; import moment from 'moment'; jest.mock('../editor_frame_service/editor_frame/expression_helpers'); @@ -72,8 +71,7 @@ const { TopNavMenu } = navigationStartMock.ui; function createMockFrame(): jest.Mocked { return { - mount: jest.fn(async (el, props) => {}), - unmount: jest.fn(() => {}), + EditorFrameContainer: jest.fn((props: EditorFrameProps) =>
), }; } @@ -308,13 +306,9 @@ describe('Lens App', () => { it('renders the editor frame', () => { const { frame } = mountWith({}); - - expect(frame.mount.mock.calls).toMatchInlineSnapshot(` + expect(frame.EditorFrameContainer.mock.calls).toMatchInlineSnapshot(` Array [ Array [ -
, Object { "dateRange": Object { "fromDate": "2021-01-10T04:00:00.000Z", @@ -333,6 +327,7 @@ describe('Lens App', () => { "searchSessionId": "sessionId-1", "showNoDataPopover": [Function], }, + Object {}, ], ] `); @@ -357,21 +352,20 @@ describe('Lens App', () => { const { component, frame } = mountWith({ services }); component.update(); - - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, query: { query: '', language: 'kuery' }, filters: [pinnedFilter], - }) + }), + {} ); expect(services.data.query.filterManager.getFilters).not.toHaveBeenCalled(); }); it('displays errors from the frame in a toast', () => { const { component, frame, services } = mountWith({}); - const onError = frame.mount.mock.calls[0][1].onError; + const onError = frame.EditorFrameContainer.mock.calls[0][0].onError; onError({ message: 'error' }); component.update(); expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); @@ -485,8 +479,7 @@ describe('Lens App', () => { }), {} ); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ doc: expect.objectContaining({ savedObjectId: defaultSavedObjectId, @@ -495,7 +488,8 @@ describe('Lens App', () => { filters: [{ query: { match_phrase: { src: 'test' } } }], }), }), - }) + }), + {} ); }); @@ -619,7 +613,7 @@ describe('Lens App', () => { expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); } - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ @@ -647,7 +641,7 @@ describe('Lens App', () => { }; const { component, frame } = mountWith({ services }); expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -662,7 +656,7 @@ describe('Lens App', () => { it('shows a save button that is enabled when the frame has provided its state and does not show save and return or save as', async () => { const { component, frame } = mountWith({}); expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -828,7 +822,7 @@ describe('Lens App', () => { .fn() .mockRejectedValue({ message: 'failed' }); const { component, props, frame } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -906,7 +900,7 @@ describe('Lens App', () => { .fn() .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); const { component, frame } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -940,7 +934,7 @@ describe('Lens App', () => { it('does not show the copy button on first save', async () => { const { component, frame } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -967,7 +961,7 @@ describe('Lens App', () => { it('should be disabled when no data is available', async () => { const { component, frame } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -981,7 +975,7 @@ describe('Lens App', () => { it('should disable download when not saveable', async () => { const { component, frame } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ @@ -1007,7 +1001,7 @@ describe('Lens App', () => { }; const { component, frame } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => onChange({ filterableIndexPatterns: [], @@ -1032,12 +1026,12 @@ describe('Lens App', () => { }), {} ); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, query: { query: '', language: 'kuery' }, - }) + }), + {} ); }); @@ -1049,7 +1043,7 @@ describe('Lens App', () => { }), {} ); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => { onChange({ filterableIndexPatterns: ['1'], @@ -1106,12 +1100,12 @@ describe('Lens App', () => { from: 'now-14d', to: 'now-7d', }); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' }, query: { query: 'new', language: 'lucene' }, - }) + }), + {} ); }); @@ -1125,11 +1119,11 @@ describe('Lens App', () => { ]) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ filters: [esFilters.buildExistsFilter(field, indexPattern)], - }) + }), + {} ); }); @@ -1142,11 +1136,11 @@ describe('Lens App', () => { }) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-1`, - }) + }), + {} ); // trigger again, this time changing just the query @@ -1157,11 +1151,11 @@ describe('Lens App', () => { }) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; @@ -1172,11 +1166,11 @@ describe('Lens App', () => { ]) ); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-3`, - }) + }), + {} ); }); }); @@ -1310,11 +1304,11 @@ describe('Lens App', () => { component.update(); act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); component.update(); - expect(frame.mount).toHaveBeenLastCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith( expect.objectContaining({ filters: [pinned], - }) + }), + {} ); }); }); @@ -1343,11 +1337,11 @@ describe('Lens App', () => { }); }); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1361,11 +1355,11 @@ describe('Lens App', () => { await act(async () => { await new Promise((r) => setTimeout(r, 0)); }); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `new-session-id`, - }) + }), + {} ); }); @@ -1387,11 +1381,11 @@ describe('Lens App', () => { component.update(); act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1416,16 +1410,14 @@ describe('Lens App', () => { it('does not update the searchSessionId when the state changes', () => { const { component, frame } = mountWith({}); act(() => { - (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( - mockUpdate - ); + component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); }); component.update(); - expect(frame.mount).not.toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1444,16 +1436,14 @@ describe('Lens App', () => { }); act(() => { - (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( - mockUpdate - ); + component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); }); component.update(); - expect(frame.mount).toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); @@ -1472,16 +1462,14 @@ describe('Lens App', () => { }); act(() => { - (component.find(NativeRenderer).prop('nativeProps') as EditorFrameProps).onChange( - mockUpdate - ); + component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); }); component.update(); - expect(frame.mount).not.toHaveBeenCalledWith( - expect.any(Element), + expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( expect.objectContaining({ searchSessionId: `sessionId-2`, - }) + }), + {} ); }); }); @@ -1513,7 +1501,7 @@ describe('Lens App', () => { }, }; const { component, frame, props } = mountWith({ services }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1533,7 +1521,7 @@ describe('Lens App', () => { it('should confirm when leaving with an unsaved doc', () => { const { component, frame, props } = mountWith({}); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1553,7 +1541,7 @@ describe('Lens App', () => { await act(async () => { component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1576,7 +1564,7 @@ describe('Lens App', () => { await act(async () => { component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], @@ -1596,7 +1584,7 @@ describe('Lens App', () => { await act(async () => { component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; + const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; act(() => onChange({ filterableIndexPatterns: [], diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 077456423ac4d..c172f36913c21 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -26,7 +26,6 @@ import { checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; import { injectFilterReferences } from '../persistence'; -import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { DataPublicPluginStart, @@ -82,7 +81,7 @@ export function App({ dashboardFeatureFlag, } = useKibana().services; - const startSession = useCallback(() => data.search.session.start(), [data]); + const startSession = useCallback(() => data.search.session.start(), [data.search.session]); const [state, setState] = useState(() => { return { @@ -95,26 +94,28 @@ export function App({ isLoading: Boolean(initialInput), indexPatternsForTopNav: [], isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp), - isSaveModalVisible: false, - indicateNoData: false, isSaveable: false, searchSessionId: startSession(), }; }); + // Used to show a popover that guides the user towards changing the date range when no data is available. + const [indicateNoData, setIndicateNoData] = useState(false); + const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); + const { lastKnownDoc } = state; const showNoDataPopover = useCallback(() => { - setState((prevState) => ({ ...prevState, indicateNoData: true })); - }, [setState]); + setIndicateNoData(true); + }, [setIndicateNoData]); useEffect(() => { - if (state.indicateNoData) { - setState((prevState) => ({ ...prevState, indicateNoData: false })); + if (indicateNoData) { + setIndicateNoData(false); } }, [ - setState, - state.indicateNoData, + setIndicateNoData, + indicateNoData, state.query, state.filters, state.indexPatternsForTopNav, @@ -136,26 +137,6 @@ export function App({ [notifications.toasts] ); - const getLastKnownDocWithoutPinnedFilters = useCallback( - function () { - if (!lastKnownDoc) return undefined; - const [pinnedFilters, appFilters] = _.partition( - injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), - esFilters.isFilterPinned - ); - return pinnedFilters?.length - ? { - ...lastKnownDoc, - state: { - ...lastKnownDoc.state, - filters: appFilters, - }, - } - : lastKnownDoc; - }, - [lastKnownDoc] - ); - const getIsByValueMode = useCallback( () => Boolean( @@ -263,7 +244,10 @@ export function App({ // or when the user has configured something without saving if ( application.capabilities.visualize.save && - !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) && + !_.isEqual( + state.persistedDoc?.state, + getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state + ) && (state.isSaveable || state.persistedDoc) ) { return actions.confirm( @@ -283,7 +267,6 @@ export function App({ lastKnownDoc, state.isSaveable, state.persistedDoc, - getLastKnownDocWithoutPinnedFilters, application.capabilities.visualize.save, ]); @@ -374,7 +357,7 @@ export function App({ setState((s) => ({ ...s, isLoading: false, - persistedDoc: doc, + ...(!_.isEqual(state.persistedDoc, doc) ? { persistedDoc: doc } : null), lastKnownDoc: doc, query: doc.state.query, indexPatternsForTopNav: indexPatterns, @@ -403,8 +386,7 @@ export function App({ attributeService, redirectTo, chrome.recentlyAccessed, - state.persistedDoc?.savedObjectId, - state.persistedDoc?.state, + state.persistedDoc, ]); const tagsIds = @@ -435,7 +417,7 @@ export function App({ } const docToSave = { - ...getLastKnownDocWithoutPinnedFilters()!, + ...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!, description: saveProps.newDescription, title: saveProps.newTitle, references, @@ -522,9 +504,10 @@ export function App({ ); setState((s) => ({ ...s, - isSaveModalVisible: false, isLinkedToOriginatingApp: false, })); + + setIsSaveModalVisible(false); // remove editor state so the connection is still broken after reload stateTransfer.clearEditorState(APP_ID); @@ -540,14 +523,15 @@ export function App({ ...s, persistedDoc: newDoc, lastKnownDoc: newDoc, - isSaveModalVisible: false, isLinkedToOriginatingApp: false, })); + + setIsSaveModalVisible(false); } catch (e) { // eslint-disable-next-line no-console console.dir(e); trackUiEvent('save_failed'); - setState((s) => ({ ...s, isSaveModalVisible: false })); + setIsSaveModalVisible(false); } }; @@ -634,7 +618,7 @@ export function App({ }, showSaveModal: () => { if (savingToDashboardPermitted || savingToLibraryPermitted) { - setState((s) => ({ ...s, isSaveModalVisible: true })); + setIsSaveModalVisible(true); } }, cancel: () => { @@ -706,7 +690,7 @@ export function App({ query={state.query} dateRangeFrom={fromDate} dateRangeTo={toDate} - indicateNoData={state.indicateNoData} + indicateNoData={indicateNoData} /> {(!state.isLoading || state.persistedDoc) && ( { - setState((s) => ({ ...s, isSaveModalVisible: false })); + setIsSaveModalVisible(false); }} getAppNameFromId={() => getOriginatingAppName()} lastKnownDoc={lastKnownDoc} @@ -790,47 +774,44 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({ lastKnownDoc: React.MutableRefObject; activeData: React.MutableRefObject | undefined>; }) { + const { EditorFrameContainer } = editorFrame; return ( - { - if (isSaveable !== oldIsSaveable) { - setState((s) => ({ ...s, isSaveable })); - } - if (!_.isEqual(persistedDoc, doc) && !_.isEqual(lastKnownDoc.current, doc)) { - setState((s) => ({ ...s, lastKnownDoc: doc })); - } - if (!_.isEqual(activeDataRef.current, activeData)) { - setState((s) => ({ ...s, activeData })); - } + { + if (isSaveable !== oldIsSaveable) { + setState((s) => ({ ...s, isSaveable })); + } + if (!_.isEqual(persistedDoc, doc) && !_.isEqual(lastKnownDoc.current, doc)) { + setState((s) => ({ ...s, lastKnownDoc: doc })); + } + if (!_.isEqual(activeDataRef.current, activeData)) { + setState((s) => ({ ...s, activeData })); + } - // Update the cached index patterns if the user made a change to any of them - if ( - indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.some( - (id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) - ) - ) { - getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then( - ({ indexPatterns }) => { - if (indexPatterns) { - setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); - } + // Update the cached index patterns if the user made a change to any of them + if ( + indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.some( + (id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) + ) + ) { + getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then( + ({ indexPatterns }) => { + if (indexPatterns) { + setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); } - ); - } - }, + } + ); + } }} /> ); @@ -851,3 +832,20 @@ export async function getAllIndexPatterns( // return also the rejected ids in case we want to show something later on return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds }; } + +function getLastKnownDocWithoutPinnedFilters(doc?: Document) { + if (!doc) return undefined; + const [pinnedFilters, appFilters] = _.partition( + injectFilterReferences(doc.state?.filters || [], doc.references), + esFilters.isFilterPinned + ); + return pinnedFilters?.length + ? { + ...doc, + state: { + ...doc.state, + filters: appFilters, + }, + } + : doc; +} diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 5869151485a52..e6eb115562d37 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -230,7 +230,6 @@ export async function mountApp( ); return () => { data.search.session.clear(); - instance.unmount(); unmountComponentAtNode(params.element); unlistenParentHistory(); }; diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index b96b274c3c159..c9143542e67bf 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -45,10 +45,6 @@ export interface LensAppState { isLoading: boolean; persistedDoc?: Document; lastKnownDoc?: Document; - isSaveModalVisible: boolean; - - // Used to show a popover that guides the user towards changing the date range when no data is available. - indicateNoData: boolean; // index patterns used to determine which filters are available in the top nav. indexPatternsForTopNav: IndexPattern[]; diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx deleted file mode 100644 index 9174f4387293a..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 { EditorFrameService } from './service'; -import { coreMock } from 'src/core/public/mocks'; -import { - MockedSetupDependencies, - MockedStartDependencies, - createMockSetupDependencies, - createMockStartDependencies, -} from './mocks'; -import { CoreSetup } from 'kibana/public'; - -// mock away actual dependencies to prevent all of it being loaded -jest.mock('./embeddable/embeddable_factory', () => ({ - EmbeddableFactory: class Mock {}, -})); - -describe('editor_frame service', () => { - let pluginInstance: EditorFrameService; - let mountpoint: Element; - let pluginSetupDependencies: MockedSetupDependencies; - let pluginStartDependencies: MockedStartDependencies; - - beforeEach(() => { - pluginInstance = new EditorFrameService(); - mountpoint = document.createElement('div'); - pluginSetupDependencies = createMockSetupDependencies(); - pluginStartDependencies = createMockStartDependencies(); - }); - - afterEach(() => { - mountpoint.remove(); - }); - - it('should create an editor frame instance which mounts and unmounts', async () => { - await expect( - (async () => { - pluginInstance.setup( - coreMock.createSetup() as CoreSetup, - pluginSetupDependencies, - jest.fn() - ); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance(); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - showNoDataPopover: jest.fn(), - initialContext: { - indexPatternId: '1', - fieldName: 'test', - }, - searchSessionId: 'sessionId', - }); - instance.unmount(); - })() - ).resolves.toBeUndefined(); - }); - - it('should not have child nodes after unmount', async () => { - pluginInstance.setup( - coreMock.createSetup() as CoreSetup, - pluginSetupDependencies, - jest.fn() - ); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = await publicAPI.createInstance(); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - showNoDataPopover: jest.fn(), - searchSessionId: 'sessionId', - }); - instance.unmount(); - - expect(mountpoint.hasChildNodes()).toBe(false); - }); -}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 46dc326a015a8..f6500596ce5a0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -6,8 +6,6 @@ */ import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'kibana/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; @@ -123,47 +121,33 @@ export class EditorFrameService { public start(core: CoreStart, plugins: EditorFrameStartPlugins): EditorFrameStart { const createInstance = async (): Promise => { - let domElement: Element; const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ collectAsyncDefinitions(this.datasources), collectAsyncDefinitions(this.visualizations), ]); - const unmount = () => { - if (domElement) { - unmountComponentAtNode(domElement); - } - }; + const firstDatasourceId = Object.keys(resolvedDatasources)[0]; + const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; + + const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services'); + + const palettes = await plugins.charts.palettes.getPalettes(); return { - mount: async ( - element, - { - doc, - onError, - dateRange, - query, - filters, - savedQuery, - onChange, - showNoDataPopover, - initialContext, - searchSessionId, - } - ) => { - if (domElement !== element) { - unmount(); - } - domElement = element; - const firstDatasourceId = Object.keys(resolvedDatasources)[0]; - const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; - - const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services'); - - const palettes = await plugins.charts.palettes.getPalettes(); - - render( - + EditorFrameContainer: ({ + doc, + onError, + dateRange, + query, + filters, + savedQuery, + onChange, + showNoDataPopover, + initialContext, + searchSessionId, + }) => { + return ( +
- , - domElement +
); }, - unmount, }; }; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 51d679e7c40e5..9cde4eb8a1561 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -65,8 +65,7 @@ export interface EditorFrameProps { showNoDataPopover: () => void; } export interface EditorFrameInstance { - mount: (element: Element, props: EditorFrameProps) => Promise; - unmount: () => void; + EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement; } export interface EditorFrameSetup { From e8e0e64369dcf506c521b6aa4e1890e875d4caac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 14 May 2021 15:59:17 +0200 Subject: [PATCH 052/186] Change search bar placeholder and make it dynamic by props (#100049) --- .../management/components/search_bar/index.test.tsx | 2 +- .../public/management/components/search_bar/index.tsx | 7 +++---- .../pages/event_filters/view/event_filters_list_page.tsx | 8 +++++++- .../management/pages/trusted_apps/view/translations.ts | 7 +++++++ .../pages/trusted_apps/view/trusted_apps_page.tsx | 8 ++++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx index 6daea8e53282d..707a96938655a 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx @@ -22,7 +22,7 @@ describe('Search bar', () => { }); const getElement = (defaultValue: string = '') => ( - + ); it('should have a default value', () => { diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx index 0d4fcf8fec87b..3c92ab31680c2 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx @@ -11,10 +11,11 @@ import { i18n } from '@kbn/i18n'; export interface SearchBarProps { defaultValue?: string; + placeholder: string; onSearch(value: string): void; } -export const SearchBar = memo(({ defaultValue = '', onSearch }) => { +export const SearchBar = memo(({ defaultValue = '', onSearch, placeholder }) => { const [query, setQuery] = useState(defaultValue); const handleOnChangeSearchField = useCallback( @@ -28,9 +29,7 @@ export const SearchBar = memo(({ defaultValue = '', onSearch }) { {doesDataExist && ( <> - + { /> )} - + {doEntriesExist ? ( Date: Fri, 14 May 2021 16:04:44 +0200 Subject: [PATCH 053/186] Disable selection of filter status 'All' on AddToCaseAction (#99757) * Fix: Disable selection of filter status 'All' on AddToCaseAction * UI: Hide disabled statuses on AddToCaseAction * Refactor: Rename disabledStatuses to hiddenStatuses * Fix: Pick the first valid status for initialFilterOptions Previously it was always picking 'open', but it wouldn't work when hiddenStatuses contains "open". * Add missing test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/README.md | 2 +- .../all_cases/all_cases_generic.test.tsx | 70 +++++++++++++++++++ .../all_cases/all_cases_generic.tsx | 16 +++-- .../all_cases/selector_modal/index.test.tsx | 4 +- .../all_cases/selector_modal/index.tsx | 13 ++-- .../all_cases/status_filter.test.tsx | 15 ++-- .../components/all_cases/status_filter.tsx | 34 +++++---- .../components/all_cases/table_filters.tsx | 6 +- .../timeline_actions/add_to_case_action.tsx | 4 +- 9 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 14afe89829a68..5cb9d82436137 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -73,7 +73,7 @@ Arguments: |---|---| |alertData?|`Omit;` alert data to post to case |createCaseNavigation|`CasesNavigation` route configuration for create cases page -|disabledStatuses?|`CaseStatuses[];` array of disabled statuses +|hiddenStatuses?|`CaseStatuses[];` array of hidden statuses |onRowClick|(theCase?: Case | SubCase) => void; callback for row click, passing case in row |updateCase?|(theCase: Case | SubCase) => void; callback after case has been updated |userCanCrud|`boolean;` user permissions to crud diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx new file mode 100644 index 0000000000000..0e8d1da74b606 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { AllCasesGeneric } from './all_cases_generic'; + +import { TestProviders } from '../../common/mock'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useGetReporters } from '../../containers/use_get_reporters'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { StatusAll } from '../../containers/types'; +import { CaseStatuses } from '../../../common'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../containers/use_get_reporters'); +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/use_get_action_license'); +jest.mock('../../containers/api'); + +const createCaseNavigation = { href: '', onClick: jest.fn() }; + +const alertDataMock = { + type: 'alert', + rule: { + id: 'rule-id', + name: 'rule', + }, + index: 'index-id', + alertId: 'alert-id', +}; + +describe('AllCasesGeneric ', () => { + beforeEach(() => { + jest.resetAllMocks(); + (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags: jest.fn() }); + (useGetReporters as jest.Mock).mockReturnValue({ + reporters: ['casetester'], + respReporters: [{ username: 'casetester' }], + isLoading: true, + isError: false, + fetchReporters: jest.fn(), + }); + (useGetActionLicense as jest.Mock).mockReturnValue({ + actionLicense: null, + isLoading: false, + }); + }); + + it('renders the first available status when hiddenStatus is given', () => + act(async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="status-badge-in-progress"]`).exists()).toBeTruthy(); + })); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx index 83f38aab21aa4..36527bd96700b 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiProgress } from '@elastic/eui'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import { isEmpty, memoize } from 'lodash/fp'; +import { difference, head, isEmpty, memoize } from 'lodash/fp'; import styled, { css } from 'styled-components'; import classnames from 'classnames'; @@ -17,10 +17,12 @@ import { CaseStatuses, CaseType, CommentRequestAlertType, + CaseStatusWithAllStatus, CommentType, FilterOptions, SortFieldCase, SubCase, + caseStatuses, } from '../../../common'; import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations'; import { useGetActionLicense } from '../../containers/use_get_action_license'; @@ -59,7 +61,7 @@ interface AllCasesGenericProps { caseDetailsNavigation?: CasesNavigation; // if not passed, case name is not displayed as a link (Formerly dependant on isSelectorView) configureCasesNavigation?: CasesNavigation; // if not passed, header with nav is not displayed (Formerly dependant on isSelectorView) createCaseNavigation: CasesNavigation; - disabledStatuses?: CaseStatuses[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; isSelectorView?: boolean; onRowClick?: (theCase?: Case | SubCase) => void; updateCase?: (newCase: Case) => void; @@ -72,13 +74,17 @@ export const AllCasesGeneric = React.memo( caseDetailsNavigation, configureCasesNavigation, createCaseNavigation, - disabledStatuses, + hiddenStatuses = [], isSelectorView, onRowClick, updateCase, userCanCrud, }) => { const { actionLicense } = useGetActionLicense(); + const firstAvailableStatus = head(difference(caseStatuses, hiddenStatuses)); + const initialFilterOptions = + !isEmpty(hiddenStatuses) && firstAvailableStatus ? { status: firstAvailableStatus } : {}; + const { data, dispatchUpdateCaseProperty, @@ -90,7 +96,7 @@ export const AllCasesGeneric = React.memo( setFilters, setQueryParams, setSelectedCases, - } = useGetCases(); + } = useGetCases({}, initialFilterOptions); // Post Comment to Case const { postComment, isLoading: isCommentUpdating } = usePostComment(); @@ -288,7 +294,7 @@ export const AllCasesGeneric = React.memo( status: filterOptions.status, }} setFilterRefetch={setFilterRefetch} - disabledStatuses={disabledStatuses} + hiddenStatuses={hiddenStatuses} /> { index: 'index-id', alertId: 'alert-id', }, - disabledStatuses: [], + hiddenStatuses: [], updateCase, }; mount( @@ -73,7 +73,7 @@ describe('AllCasesSelectorModal', () => { expect.objectContaining({ alertData: fullProps.alertData, createCaseNavigation, - disabledStatuses: fullProps.disabledStatuses, + hiddenStatuses: fullProps.hiddenStatuses, isSelectorView: true, userCanCrud: fullProps.userCanCrud, updateCase, diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx index 0a83ef13e8ee6..d476d71d847a0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx @@ -8,7 +8,12 @@ import React, { useState, useCallback } from 'react'; import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { Case, CaseStatuses, CommentRequestAlertType, SubCase } from '../../../../common'; +import { + Case, + CaseStatusWithAllStatus, + CommentRequestAlertType, + SubCase, +} from '../../../../common'; import { CasesNavigation } from '../../links'; import * as i18n from '../../../common/translations'; import { AllCasesGeneric } from '../all_cases_generic'; @@ -16,7 +21,7 @@ import { AllCasesGeneric } from '../all_cases_generic'; export interface AllCasesSelectorModalProps { alertData?: Omit; createCaseNavigation: CasesNavigation; - disabledStatuses?: CaseStatuses[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; onRowClick: (theCase?: Case | SubCase) => void; updateCase?: (newCase: Case) => void; userCanCrud: boolean; @@ -32,7 +37,7 @@ const Modal = styled(EuiModal)` export const AllCasesSelectorModal: React.FC = ({ alertData, createCaseNavigation, - disabledStatuses, + hiddenStatuses, onRowClick, updateCase, userCanCrud, @@ -55,7 +60,7 @@ export const AllCasesSelectorModal: React.FC = ({ { }); }); - it('should disabled selected statuses', () => { + it('should not render hidden statuses', () => { const wrapper = mount( - + ); wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); - expect( - wrapper.find('button[data-test-subj="case-status-filter-open"]').prop('disabled') - ).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="case-status-filter-all"]`).exists()).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="case-status-filter-closed"]').exists()).toBeFalsy(); - expect( - wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').prop('disabled') - ).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').exists()).toBeTruthy(); expect( - wrapper.find('button[data-test-subj="case-status-filter-closed"]').prop('disabled') + wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').exists() ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx index 9fb00933f0307..7d02bf2c441d3 100644 --- a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx @@ -14,32 +14,30 @@ interface Props { stats: Record; selectedStatus: CaseStatusWithAllStatus; onStatusChanged: (status: CaseStatusWithAllStatus) => void; - disabledStatuses?: CaseStatusWithAllStatus[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; } const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatusChanged, - disabledStatuses = [], + hiddenStatuses = [], }) => { const caseStatuses = Object.keys(statuses) as CaseStatusWithAllStatus[]; - const options: Array> = [ - StatusAll, - ...caseStatuses, - ].map((status) => ({ - value: status, - inputDisplay: ( - - - - - {status !== StatusAll && {` (${stats[status]})`}} - - ), - disabled: disabledStatuses.includes(status), - 'data-test-subj': `case-status-filter-${status}`, - })); + const options: Array> = [StatusAll, ...caseStatuses] + .filter((status) => !hiddenStatuses.includes(status)) + .map((status) => ({ + value: status, + inputDisplay: ( + + + + + {status !== StatusAll && {` (${stats[status]})`}} + + ), + 'data-test-subj': `case-status-filter-${status}`, + })); return ( ) => void; initial: FilterOptions; setFilterRefetch: (val: () => void) => void; - disabledStatuses?: CaseStatuses[]; + hiddenStatuses?: CaseStatusWithAllStatus[]; } // Fix the width of the status dropdown to prevent hiding long text items @@ -56,7 +56,7 @@ const CasesTableFiltersComponent = ({ onFilterChanged, initial = defaultInitial, setFilterRefetch, - disabledStatuses, + hiddenStatuses, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map((r) => r.full_name ?? r.username ?? '') @@ -161,7 +161,7 @@ const CasesTableFiltersComponent = ({ selectedStatus={initial.status} onStatusChanged={onStatusChanged} stats={stats} - disabledStatuses={disabledStatuses} + hiddenStatuses={hiddenStatuses} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 1682b4b7e7dee..7379f5d6fd5dc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -16,7 +16,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { Case, CaseStatuses } from '../../../../../cases/common'; +import { Case, CaseStatuses, StatusAll } from '../../../../../cases/common'; import { APP_ID } from '../../../../common/constants'; import { Ecs } from '../../../../common/ecs'; import { SecurityPageName } from '../../../app/types'; @@ -240,7 +240,7 @@ const AddToCaseActionComponent: React.FC = ({ href: formatUrl(getCreateCaseUrl()), onClick: goToCreateCase, }, - disabledStatuses: [CaseStatuses.closed], + hiddenStatuses: [CaseStatuses.closed, StatusAll], onRowClick: onCaseClicked, updateCase: onCaseSuccess, userCanCrud: userPermissions?.crud ?? false, From 762f378165092c470adcfc3fde9629c929276ffa Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 14 May 2021 07:43:09 -0700 Subject: [PATCH 054/186] [Alerting] Enabling import of rules and connectors (#99857) * [Alerting] Enabling import of rules and connectors * changed export to set pending executionStatus for rule * fixed tests * added docs * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed docs * fixed docs * Update x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed test * fixed test Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/management/action-types.asciidoc | 7 ++ .../connectors-with-missing-secrets.png | Bin 0 -> 150439 bytes .../images/coonectors-import-banner.png | Bin 0 -> 62903 bytes .../alerting/images/rules-imported-banner.png | Bin 0 -> 78546 bytes docs/user/alerting/rule-management.asciidoc | 3 + .../saved_objects/get_import_warnings.test.ts | 2 +- .../saved_objects/get_import_warnings.ts | 7 +- .../actions/server/saved_objects/index.ts | 4 +- .../server/alerts_client/alerts_client.ts | 7 +- .../server/lib/alert_execution_status.ts | 7 ++ .../saved_objects/get_import_warnings.test.ts | 87 ++++++++++++++++++ .../saved_objects/get_import_warnings.ts | 37 ++++++++ .../alerting/server/saved_objects/index.ts | 11 ++- .../transform_rule_for_export.test.ts | 13 ++- .../transform_rule_for_export.ts | 10 +- .../connector_add_inline.tsx | 4 +- 16 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 docs/management/images/connectors-with-missing-secrets.png create mode 100644 docs/management/images/coonectors-import-banner.png create mode 100644 docs/user/alerting/images/rules-imported-banner.png create mode 100644 x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts create mode 100644 x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 6cdb1dbfa712e..ec5677bd04a6e 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -111,6 +111,13 @@ image::images/connector-select-type.png[Connector select type] === Importing and exporting connectors To import and export rules, use the <>. +After a successful import, the proper banner is displayed: +[role="screenshot"] +image::images/coonectors-import-banner.png[Connectors import banner, width=50%] + +If a connector is missing user sensitive information because of the import, a **Fix** button appears in the list view. +[role="screenshot"] +image::images/connectors-with-missing-secrets.png[Connectors with missing secrets] [float] [[create-connectors]] diff --git a/docs/management/images/connectors-with-missing-secrets.png b/docs/management/images/connectors-with-missing-secrets.png new file mode 100644 index 0000000000000000000000000000000000000000..ffc902d4a4768748eaf5784c90e133f01599f09b GIT binary patch literal 150439 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≪OO$ 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

T4vKTy} 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 FKL9D2;UoY6 literal 0 HcmV?d00001 diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index b15c46254b770..e47858f58cd1a 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -62,6 +62,9 @@ image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, === Importing and exporting rules To import and export rules, use the <>. +After the succesful import the proper banner will be displayed: +[role="screenshot"] +image::images/rules-imported-banner.png[Rules import banner, width=50%] [float] === Required permissions diff --git a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts index 7f635d6ec13a6..de4ccf5016379 100644 --- a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.test.ts @@ -48,7 +48,7 @@ describe('getImportWarnings', () => { const warnings = getImportWarnings( (savedObjectConnectors as unknown) as Array> ); - expect(warnings[0].message).toBe('1 connector has secrets that require updates.'); + expect(warnings[0].message).toBe('1 connector has sensitive information that require updates.'); }); it('does not return the warning message if all of the imported connectors do not have secrets to update', () => { diff --git a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts index 3be0a53e27c00..941d6d0fd6aaa 100644 --- a/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts +++ b/x-pack/plugins/actions/server/saved_objects/get_import_warnings.ts @@ -20,7 +20,7 @@ export function getImportWarnings( } const message = i18n.translate('xpack.actions.savedObjects.onImportText', { defaultMessage: - '{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} secrets that require updates.', + '{connectorsWithSecretsLength} {connectorsWithSecretsLength, plural, one {connector has} other {connectors have}} sensitive information that require updates.', values: { connectorsWithSecretsLength: connectorsWithSecrets.length, }, @@ -35,4 +35,7 @@ export function getImportWarnings( ]; } -export const GO_TO_CONNECTORS_BUTTON_LABLE = 'Go to connectors'; +export const GO_TO_CONNECTORS_BUTTON_LABLE = i18n.translate( + 'xpack.actions.savedObjects.goToConnectorsButtonText', + { defaultMessage: 'Go to connectors' } +); diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 761d475f797d1..9360bc919a2d5 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -36,8 +36,8 @@ export function setupSavedObjects( management: { defaultSearchField: 'name', importableAndExportable: true, - getTitle(obj) { - return `Connector: [${obj.attributes.name}]`; + getTitle(savedObject: SavedObject) { + return `Connector: [${savedObject.attributes.name}]`; }, onExport( context: SavedObjectsExportTransformContext, diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index f01341339da27..74a38bb8ba501 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -66,6 +66,7 @@ import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_a import { alertAuditEvent, AlertAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; +import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -300,11 +301,7 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], notifyWhen, - executionStatus: { - status: 'pending', - lastExecutionDate: new Date().toISOString(), - error: null, - }, + executionStatus: getAlertExecutionStatusPending(new Date().toISOString()), }; this.auditLogger?.log( diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts index 7fad08a3cd29e..47dfc659307a2 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts +++ b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts @@ -9,6 +9,7 @@ import { Logger } from 'src/core/server'; import { AlertTaskState, AlertExecutionStatus, RawAlertExecutionStatus } from '../types'; import { getReasonFromError } from './error_with_reason'; import { getEsErrorMessage } from './errors'; +import { AlertExecutionStatuses } from '../../common'; export function executionStatusFromState(state: AlertTaskState): AlertExecutionStatus { const instanceIds = Object.keys(state.alertInstances ?? {}); @@ -66,3 +67,9 @@ export function alertExecutionStatusFromRaw( return { lastExecutionDate: parsedDate, status }; } } + +export const getAlertExecutionStatusPending = (lastExecutionDate: string) => ({ + status: 'pending' as AlertExecutionStatuses, + lastExecutionDate, + error: null, +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts new file mode 100644 index 0000000000000..d76e151b8d47b --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from 'kibana/server'; +import { RawAlert } from '../types'; +import { getImportWarnings } from './get_import_warnings'; + +describe('getImportWarnings', () => { + it('return warning message with total imported rules that have to be enabled', () => { + const savedObjectRules = [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'rule-name1', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: 'me', + updatedBy: 'me', + apiKey: '4tndskbuhewotw4klrhgjewrt9u', + apiKeyOwner: 'me', + throttle: null, + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + scheduledTaskId: '2q5tjbf3q45twer', + }, + references: [], + }, + { + id: '2', + type: 'alert', + attributes: { + enabled: true, + name: 'rule-name2', + tags: [], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: 'me', + updatedBy: 'me', + apiKey: '4tndskbuhewotw4klrhgjewrt9u', + apiKeyOwner: 'me', + throttle: null, + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + scheduledTaskId: '123', + }, + references: [], + }, + ]; + const warnings = getImportWarnings( + (savedObjectRules as unknown) as Array> + ); + expect(warnings[0].message).toBe('2 rules must be enabled after the import.'); + }); + + it('return no warning messages if no rules were imported', () => { + const savedObjectRules = [] as Array>; + const warnings = getImportWarnings( + (savedObjectRules as unknown) as Array> + ); + expect(warnings.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts new file mode 100644 index 0000000000000..0058cd82c9d83 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/get_import_warnings.ts @@ -0,0 +1,37 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { SavedObject, SavedObjectsImportWarning } from 'kibana/server'; + +export function getImportWarnings( + rulesSavedObjects: Array> +): SavedObjectsImportWarning[] { + if (rulesSavedObjects.length === 0) { + return []; + } + const message = i18n.translate('xpack.alerting.savedObjects.onImportText', { + defaultMessage: + '{rulesSavedObjectsLength} {rulesSavedObjectsLength, plural, one {rule} other {rules}} must be enabled after the import.', + values: { + rulesSavedObjectsLength: rulesSavedObjects.length, + }, + }); + return [ + { + type: 'action_required', + message, + actionPath: '/app/management/insightsAndAlerting/triggersActions/rules', + buttonLabel: GO_TO_RULES_BUTTON_LABLE, + } as SavedObjectsImportWarning, + ]; +} + +export const GO_TO_RULES_BUTTON_LABLE = i18n.translate( + 'xpack.alerting.savedObjects.goToRulesButtonText', + { defaultMessage: 'Go to rules' } +); diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index f4a1c0386b54c..6b76fd97dc53b 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -14,6 +14,8 @@ import mappings from './mappings.json'; import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; import { transformRulesForExport } from './transform_rule_for_export'; +import { RawAlert } from '../types'; +import { getImportWarnings } from './get_import_warnings'; export { partiallyUpdateAlert } from './partially_update_alert'; export const AlertAttributesExcludedFromAAD = [ @@ -49,8 +51,13 @@ export function setupSavedObjects( mappings: mappings.alert, management: { importableAndExportable: true, - getTitle(obj) { - return `Rule: [${obj.attributes.name}]`; + getTitle(ruleSavedObject: SavedObject) { + return `Rule: [${ruleSavedObject.attributes.name}]`; + }, + onImport(ruleSavedObjects) { + return { + warnings: getImportWarnings(ruleSavedObjects), + }; }, onExport( context: SavedObjectsExportTransformContext, diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts index bf181e7299220..5997df2895761 100644 --- a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts @@ -6,7 +6,13 @@ */ import { transformRulesForExport } from './transform_rule_for_export'; - +jest.mock('../lib/alert_execution_status', () => ({ + getAlertExecutionStatusPending: () => ({ + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }), +})); describe('transform rule for export', () => { const date = new Date().toISOString(); const mockRules = [ @@ -84,6 +90,11 @@ describe('transform rule for export', () => { apiKey: null, apiKeyOwner: null, scheduledTaskId: null, + executionStatus: { + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, }, })) ); diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts index c33bbceaf8363..707bd84e948bf 100644 --- a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts @@ -6,13 +6,18 @@ */ import { SavedObject } from 'kibana/server'; +import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; import { RawAlert } from '../types'; export function transformRulesForExport(rules: SavedObject[]): Array> { - return rules.map((rule) => transformRuleForExport(rule as SavedObject)); + const exportDate = new Date().toISOString(); + return rules.map((rule) => transformRuleForExport(rule as SavedObject, exportDate)); } -function transformRuleForExport(rule: SavedObject): SavedObject { +function transformRuleForExport( + rule: SavedObject, + exportDate: string +): SavedObject { return { ...rule, attributes: { @@ -21,6 +26,7 @@ function transformRuleForExport(rule: SavedObject): SavedObject ); @@ -196,7 +196,7 @@ export const AddConnectorInline = ({ data-test-subj={`alertActionAccordionErrorTooltip`} content={ } From d5e53a1ee896dfa16312456ba2eda5e26ac2648b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 14 May 2021 16:56:31 +0200 Subject: [PATCH 055/186] Added missing padding to the popover title and footer in 'Test documents' popover (#99921) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test_pipeline/documents_dropdown/documents_dropdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx index 9607cd18f491b..c89c3f2495246 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx @@ -109,13 +109,13 @@ export const DocumentsDropdown: FunctionComponent = ({ > {(list) => ( <> - {i18nTexts.popoverTitle} + {i18nTexts.popoverTitle} {list} )} - + Date: Fri, 14 May 2021 12:19:31 -0400 Subject: [PATCH 056/186] [Observability] [Exploratory view] update v7 button styles (#100113) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../series_builder/columns/data_types_col.tsx | 8 ++++++-- .../series_builder/columns/report_types_col.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 9d15206db1e62..b64fad51e9778 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -40,7 +40,7 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { {dataTypes.map(({ id: dataTypeId, label }) => ( - {label} - + ))} @@ -63,3 +63,7 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { const FlexGroup = styled(EuiFlexGroup)` width: 100%; `; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 9c95b3874c242..bd82d1d1bd500 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -41,7 +41,7 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { {reportTypes.map(({ id: reportType, label }) => ( - {label} - + ))} @@ -84,3 +84,7 @@ export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( const FlexGroup = styled(EuiFlexGroup)` width: 100%; `; + +const Button = styled(EuiButton)` + will-change: transform; +`; From eeab570f85b0c126515b84183f6ea8f5d107c932 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Fri, 14 May 2021 10:37:16 -0700 Subject: [PATCH 057/186] Adds error from es call to nodes.info to the nodes version compatibility response message (#100005) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...gin-core-server.elasticsearchstatusmeta.md | 1 + ...csearchstatusmeta.nodesinforequesterror.md | 11 ++ ...n-core-server.nodesversioncompatibility.md | 1 + ...sioncompatibility.nodesinforequesterror.md | 11 ++ src/core/server/elasticsearch/status.test.ts | 115 +++++++++++++++- src/core/server/elasticsearch/status.ts | 7 +- src/core/server/elasticsearch/types.ts | 1 + .../version_check/ensure_es_version.test.ts | 123 +++++++++++++++++- .../version_check/ensure_es_version.ts | 38 ++++-- src/core/server/server.api.md | 4 + 10 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md index 2398410fa4b84..90aa2f0100d88 100644 --- a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.md @@ -16,5 +16,6 @@ export interface ElasticsearchStatusMeta | Property | Type | Description | | --- | --- | --- | | [incompatibleNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.incompatiblenodes.md) | NodesVersionCompatibility['incompatibleNodes'] | | +| [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) | NodesVersionCompatibility['nodesInfoRequestError'] | | | [warningNodes](./kibana-plugin-core-server.elasticsearchstatusmeta.warningnodes.md) | NodesVersionCompatibility['warningNodes'] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md new file mode 100644 index 0000000000000..1b46078a1a453 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ElasticsearchStatusMeta](./kibana-plugin-core-server.elasticsearchstatusmeta.md) > [nodesInfoRequestError](./kibana-plugin-core-server.elasticsearchstatusmeta.nodesinforequesterror.md) + +## ElasticsearchStatusMeta.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md index 6fcfacc3bc908..cbdac9d5455b0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.md @@ -18,5 +18,6 @@ export interface NodesVersionCompatibility | [isCompatible](./kibana-plugin-core-server.nodesversioncompatibility.iscompatible.md) | boolean | | | [kibanaVersion](./kibana-plugin-core-server.nodesversioncompatibility.kibanaversion.md) | string | | | [message](./kibana-plugin-core-server.nodesversioncompatibility.message.md) | string | | +| [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) | Error | | | [warningNodes](./kibana-plugin-core-server.nodesversioncompatibility.warningnodes.md) | NodeInfo[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md new file mode 100644 index 0000000000000..aa9421afed6e8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) > [nodesInfoRequestError](./kibana-plugin-core-server.nodesversioncompatibility.nodesinforequesterror.md) + +## NodesVersionCompatibility.nodesInfoRequestError property + +Signature: + +```typescript +nodesInfoRequestError?: Error; +``` diff --git a/src/core/server/elasticsearch/status.test.ts b/src/core/server/elasticsearch/status.test.ts index 6f21fc204a1c2..c1f7cf0e35892 100644 --- a/src/core/server/elasticsearch/status.test.ts +++ b/src/core/server/elasticsearch/status.test.ts @@ -54,7 +54,7 @@ describe('calculateStatus', () => { }); }); - it('changes to available with a differemnt message when isCompatible and warningNodes present', async () => { + it('changes to available with a different message when isCompatible and warningNodes present', async () => { expect( await calculateStatus$( of({ @@ -204,4 +204,117 @@ describe('calculateStatus', () => { ] `); }); + + it('emits status updates when node info request error changes', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. connect ECONNREFUSED', + nodesInfoRequestError: new Error('connect ECONNREFUSED'), + }); + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: connect ECONNREFUSED], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. connect ECONNREFUSED", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + ] + `); + }); + + it('changes to available when a request error is resolved', () => { + const nodeCompat$ = new Subject(); + + const statusUpdates: ServiceStatus[] = []; + const subscription = calculateStatus$(nodeCompat$).subscribe((status) => + statusUpdates.push(status) + ); + + nodeCompat$.next({ + isCompatible: false, + kibanaVersion: '1.1.1', + incompatibleNodes: [], + warningNodes: [], + message: 'Unable to retrieve version info. security_exception', + nodesInfoRequestError: new Error('security_exception'), + }); + nodeCompat$.next({ + isCompatible: true, + kibanaVersion: '1.1.1', + warningNodes: [], + incompatibleNodes: [], + }); + + subscription.unsubscribe(); + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "level": unavailable, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Waiting for Elasticsearch", + }, + Object { + "level": critical, + "meta": Object { + "incompatibleNodes": Array [], + "nodesInfoRequestError": [Error: security_exception], + "warningNodes": Array [], + }, + "summary": "Unable to retrieve version info. security_exception", + }, + Object { + "level": available, + "meta": Object { + "incompatibleNodes": Array [], + "warningNodes": Array [], + }, + "summary": "Elasticsearch is available", + }, + ] + `); + }); }); diff --git a/src/core/server/elasticsearch/status.ts b/src/core/server/elasticsearch/status.ts index 68a61b07f498e..23e44b71863f1 100644 --- a/src/core/server/elasticsearch/status.ts +++ b/src/core/server/elasticsearch/status.ts @@ -32,6 +32,7 @@ export const calculateStatus$ = ( message, incompatibleNodes, warningNodes, + nodesInfoRequestError, }): ServiceStatus => { if (!isCompatible) { return { @@ -40,7 +41,11 @@ export const calculateStatus$ = ( // Message should always be present, but this is a safe fallback message ?? `Some Elasticsearch nodes are not compatible with this version of Kibana`, - meta: { warningNodes, incompatibleNodes }, + meta: { + warningNodes, + incompatibleNodes, + ...(nodesInfoRequestError && { nodesInfoRequestError }), + }, }; } else if (warningNodes.length > 0) { return { diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 85678c21f03b0..8bbf665cbc096 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -179,6 +179,7 @@ export type InternalElasticsearchServiceStart = ElasticsearchServiceStart; export interface ElasticsearchStatusMeta { warningNodes: NodesVersionCompatibility['warningNodes']; incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; } /** diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts index 0e08fd2ddc4c5..70166704679fe 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -19,7 +19,8 @@ const mockLogger = mockLoggerFactory.get('mock logger'); const KIBANA_VERSION = '5.1.0'; const createEsSuccess = elasticsearchClientMock.createSuccessTransportRequestPromise; -const createEsError = elasticsearchClientMock.createErrorTransportRequestPromise; +const createEsErrorReturn = (err: any) => + elasticsearchClientMock.createErrorTransportRequestPromise(err); function createNodes(...versions: string[]): NodesInfo { const nodes = {} as any; @@ -102,6 +103,28 @@ describe('mapNodesVersionCompatibility', () => { `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` ); }); + + it('returns isCompatible=false without an extended message when a nodesInfoRequestError is not provided', async () => { + const result = mapNodesVersionCompatibility({ nodes: {} }, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeUndefined(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes."` + ); + }); + + it('returns isCompatible=false with an extended message when a nodesInfoRequestError is present', async () => { + const result = mapNodesVersionCompatibility( + { nodes: {}, nodesInfoRequestError: new Error('connection refused') }, + KIBANA_VERSION, + false + ); + expect(result.isCompatible).toBe(false); + expect(result.nodesInfoRequestError).toBeTruthy(); + expect(result.message).toMatchInlineSnapshot( + `"Unable to retrieve version information from Elasticsearch nodes. connection refused"` + ); + }); }); describe('pollEsNodesVersion', () => { @@ -119,10 +142,10 @@ describe('pollEsNodesVersion', () => { internalClient.nodes.info.mockImplementationOnce(() => createEsSuccess(infos)); }; const nodeInfosErrorOnce = (error: any) => { - internalClient.nodes.info.mockImplementationOnce(() => createEsError(error)); + internalClient.nodes.info.mockImplementationOnce(() => createEsErrorReturn(new Error(error))); }; - it('returns iscCompatible=false and keeps polling when a poll request throws', (done) => { + it('returns isCompatible=false and keeps polling when a poll request throws', (done) => { expect.assertions(3); const expectedCompatibilityResults = [false, false, true]; jest.clearAllMocks(); @@ -148,6 +171,100 @@ describe('pollEsNodesVersion', () => { }); }); + it('returns the error from a failed nodes.info call when a poll request throws', (done) => { + expect.assertions(2); + const expectedCompatibilityResults = [false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the error from a failed nodes.info call changed from the previous poll', (done) => { + expect.assertions(4); + const expectedCompatibilityResults = [false, false]; + const expectedMessageResults = [ + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error 2', + ]; + jest.clearAllMocks(); + + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore, same error message + nodeInfosErrorOnce('mock request error 2'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(2)) + .subscribe({ + next: (result) => { + expect(result.message).toBe(expectedMessageResults.shift()); + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns isCompatible=false and keeps polling when a poll request throws, only responding again if the error message has changed', (done) => { + expect.assertions(8); + const expectedCompatibilityResults = [false, false, true, false]; + const expectedMessageResults = [ + 'This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)', + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + "You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.2.0 @ http_address (ip), v5.1.1-Beta1 @ http_address (ip)", + 'Unable to retrieve version information from Elasticsearch nodes. mock request error', + ]; + jest.clearAllMocks(); + + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + nodeInfosErrorOnce('mock request error'); // emit + nodeInfosErrorOnce('mock request error'); // ignore + nodeInfosSuccessOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); // emit + nodeInfosErrorOnce('mock request error'); // emit + + pollEsNodesVersion({ + internalClient, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: (result) => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + expect(result.message).toBe(expectedMessageResults.shift()); + }, + complete: done, + error: done, + }); + }); + it('returns compatibility results', (done) => { expect.assertions(1); const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index fb7ef0583e4a4..43cd52f1b5721 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -49,6 +49,7 @@ export interface NodesVersionCompatibility { incompatibleNodes: NodeInfo[]; warningNodes: NodeInfo[]; kibanaVersion: string; + nodesInfoRequestError?: Error; } function getHumanizedNodeName(node: NodeInfo) { @@ -57,22 +58,28 @@ function getHumanizedNodeName(node: NodeInfo) { } export function mapNodesVersionCompatibility( - nodesInfo: NodesInfo, + nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }, kibanaVersion: string, ignoreVersionMismatch: boolean ): NodesVersionCompatibility { - if (Object.keys(nodesInfo.nodes ?? {}).length === 0) { + if (Object.keys(nodesInfoResponse.nodes ?? {}).length === 0) { + // Note: If the a nodesInfoRequestError is present, the message contains the nodesInfoRequestError.message as a suffix + let message = `Unable to retrieve version information from Elasticsearch nodes.`; + if (nodesInfoResponse.nodesInfoRequestError) { + message = message + ` ${nodesInfoResponse.nodesInfoRequestError.message}`; + } return { isCompatible: false, - message: 'Unable to retrieve version information from Elasticsearch nodes.', + message, incompatibleNodes: [], warningNodes: [], kibanaVersion, + nodesInfoRequestError: nodesInfoResponse.nodesInfoRequestError, }; } - const nodes = Object.keys(nodesInfo.nodes) + const nodes = Object.keys(nodesInfoResponse.nodes) .sort() // Sorting ensures a stable node ordering for comparison - .map((key) => nodesInfo.nodes[key]) + .map((key) => nodesInfoResponse.nodes[key]) .map((node) => Object.assign({}, node, { name: getHumanizedNodeName(node) })); // Aggregate incompatible ES nodes. @@ -112,7 +119,13 @@ export function mapNodesVersionCompatibility( kibanaVersion, }; } - +// Returns true if NodesVersionCompatibility nodesInfoRequestError is the same +function compareNodesInfoErrorMessages( + prev: NodesVersionCompatibility, + curr: NodesVersionCompatibility +): boolean { + return prev.nodesInfoRequestError?.message === curr.nodesInfoRequestError?.message; +} // Returns true if two NodesVersionCompatibility entries match function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; @@ -121,7 +134,8 @@ function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompati curr.incompatibleNodes.length === prev.incompatibleNodes.length && curr.warningNodes.length === prev.warningNodes.length && curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && - curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) && + compareNodesInfoErrorMessages(curr, prev) ); } @@ -141,14 +155,14 @@ export const pollEsNodesVersion = ({ }) ).pipe( map(({ body }) => body), - catchError((_err) => { - return of({ nodes: {} }); + catchError((nodesInfoRequestError) => { + return of({ nodes: {}, nodesInfoRequestError }); }) ); }), - map((nodesInfo: NodesInfo) => - mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + map((nodesInfoResponse: NodesInfo & { nodesInfoRequestError?: Error }) => + mapNodesVersionCompatibility(nodesInfoResponse, kibanaVersion, ignoreVersionMismatch) ), - distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions or if we return an error and that error changes ); }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4c12ca53b9098..f4c70d718bc87 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -976,6 +976,8 @@ export interface ElasticsearchStatusMeta { // (undocumented) incompatibleNodes: NodesVersionCompatibility['incompatibleNodes']; // (undocumented) + nodesInfoRequestError?: NodesVersionCompatibility['nodesInfoRequestError']; + // (undocumented) warningNodes: NodesVersionCompatibility['warningNodes']; } @@ -1727,6 +1729,8 @@ export interface NodesVersionCompatibility { // (undocumented) message?: string; // (undocumented) + nodesInfoRequestError?: Error; + // (undocumented) warningNodes: NodeInfo[]; } From 50ac01b64ed22f24e16f58b152637e2c6a353b69 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Fri, 14 May 2021 12:57:34 -0500 Subject: [PATCH 058/186] [Metrics UI] Replace date_histogram with date_range aggregation in threshold alert (#100004) * [Metrics UI] Replace date_histogram with date_range aggregation in threshold alert * Remove console.log * Fix rate aggregation and offset --- .../metric_threshold/lib/evaluate_alert.ts | 11 +++-- .../metric_threshold/lib/metric_query.ts | 46 +++++++++++++------ .../alerting/metric_threshold/test_mocks.ts | 2 +- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 87150aa134837..144ee6505c593 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -25,6 +25,7 @@ interface Aggregation { buckets: Array<{ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; doc_count: number; + to_as_string: string; key_as_string: string; }>; }; @@ -60,6 +61,7 @@ export const evaluateAlert = { if (!t || !c) return [false]; @@ -179,18 +181,21 @@ const getValuesFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state if (aggType === Aggregators.COUNT) { - return buckets.map((bucket) => ({ key: bucket.key_as_string, value: bucket.doc_count })); + return buckets.map((bucket) => ({ + key: bucket.to_as_string, + value: bucket.doc_count, + })); } if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { return buckets.map((bucket) => { const values = bucket.aggregatedValue?.values || []; const firstValue = first(values); if (!firstValue) return null; - return { key: bucket.key_as_string, value: firstValue.value }; + return { key: bucket.to_as_string, value: firstValue.value }; }); } return buckets.map((bucket) => ({ - key: bucket.key_as_string, + key: bucket.key_as_string ?? bucket.to_as_string, value: bucket.aggregatedValue?.value ?? null, })); } catch (e) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 42ba918694482..0e495c08cc9fd 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -37,11 +37,12 @@ export const getElasticsearchMetricQuery = ( } const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); + const intervalAsMS = intervalAsSeconds * 1000; const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit); // We need enough data for 5 buckets worth of data. We also need // to convert the intervalAsSeconds to milliseconds. - const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS; + const minimumFrom = to - intervalAsMS * MINIMUM_BUCKETS; const from = roundTimestamp( timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, @@ -49,6 +50,7 @@ export const getElasticsearchMetricQuery = ( ); const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield }); + const offsetInMS = parseInt(offset, 10) * 1000; const aggregations = aggType === Aggregators.COUNT @@ -65,20 +67,34 @@ export const getElasticsearchMetricQuery = ( }, }; - const baseAggs = { - aggregatedIntervals: { - date_histogram: { - field: timefield, - fixed_interval: interval, - offset, - extended_bounds: { - min: from, - max: to, - }, - }, - aggregations, - }, - }; + const baseAggs = + aggType === Aggregators.RATE + ? { + aggregatedIntervals: { + date_histogram: { + field: timefield, + fixed_interval: interval, + offset, + extended_bounds: { + min: from, + max: to, + }, + }, + aggregations, + }, + } + : { + aggregatedIntervals: { + date_range: { + field: timefield, + ranges: Array.from(Array(Math.floor((to - from) / intervalAsMS)), (_, i) => ({ + from: from + intervalAsMS * i + offsetInMS, + to: from + intervalAsMS * (i + 1) + offsetInMS, + })), + }, + aggregations, + }, + }; const aggs = groupBy ? { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 2d4f2b16c78a4..47da539afea19 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -13,7 +13,7 @@ const bucketsA = [ { doc_count: 3, aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, - key_as_string: new Date(1577858400000).toISOString(), + to_as_string: new Date(1577858400000).toISOString(), }, ]; From a3c7a4efa6793ff17a501bd3e646eaeb10bb5691 Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Fri, 14 May 2021 13:59:33 -0400 Subject: [PATCH 059/186] [Docs] fixing KibanaPageTemplate docs (#100104) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- dev_docs/building_blocks.mdx | 2 +- dev_docs/tutorials/kibana_page_template.mdx | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dev_docs/building_blocks.mdx b/dev_docs/building_blocks.mdx index 95851ea66b8cb..327492a20d5b8 100644 --- a/dev_docs/building_blocks.mdx +++ b/dev_docs/building_blocks.mdx @@ -74,7 +74,7 @@ Check out the Map Embeddable if you wish to embed a map in your application. All Kibana pages should use KibanaPageTemplate to setup their pages. It's a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements. -Check out for more implementation guidance. +Check out for more implementation guidance. **Github labels**: `EUI` diff --git a/dev_docs/tutorials/kibana_page_template.mdx b/dev_docs/tutorials/kibana_page_template.mdx index ec78fa49aa231..aa38890a8ac9e 100644 --- a/dev_docs/tutorials/kibana_page_template.mdx +++ b/dev_docs/tutorials/kibana_page_template.mdx @@ -1,13 +1,13 @@ --- -id: kibDevDocsKBLTutorial -slug: /kibana-dev-docs/tutorials/kibana-page-layout -title: KibanaPageLayout component +id: kibDevDocsKPTTutorial +slug: /kibana-dev-docs/tutorials/kibana-page-template +title: KibanaPageTemplate component summary: Learn how to create pages in Kibana date: 2021-03-20 tags: ['kibana', 'dev', 'ui', 'tutorials'] --- -`KibanaPageLayout` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. +`KibanaPageTemplate` is a thin wrapper around [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) that makes setting up common types of Kibana pages quicker and easier while also adhering to any Kibana-specific requirements and patterns. Refer to EUI's documentation on [EuiPageTemplate](https://elastic.github.io/eui/#/layout/page) for constructing page layouts. @@ -18,7 +18,7 @@ Use the `isEmptyState` prop for when there is no page content to show. For examp The default empty state uses any `pageHeader` info provided to populate an [`EuiEmptyPrompt`](https://elastic.github.io/eui/#/display/empty-prompt) and uses the `centeredBody` template type. ```tsx - + No data} body="You have no data. Would you like some of ours?" @@ -55,7 +55,7 @@ You can also provide a custom empty prompt to replace the pre-built one. You'll , ]} /> - + ``` ![Screenshot of demo custom empty state code. Shows the Kibana navigation bars and a centered empty state with the a level 1 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](../assets/kibana_custom_empty_state.png) @@ -65,7 +65,7 @@ You can also provide a custom empty prompt to replace the pre-built one. You'll When passing both a `pageHeader` configuration and `isEmptyState`, the component will render the proper template (`centeredContent`). Be sure to reduce the heading level within your child empty prompt to `

`. ```tsx -, ]} /> - + ``` ![Screenshot of demo custom empty state code with a page header. Shows the Kibana navigation bars, a level 1 heading "Dashboards", and a centered empty state with the a level 2 heading "No data", body text "You have no data. Would you like some of ours?", and a button that says "Get sample data".](../assets/kibana_header_and_empty_state.png) From d98feba884642ab907938069faf0f4d882a6767f Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 14 May 2021 14:10:18 -0400 Subject: [PATCH 060/186] [APM][RUM] adjust data types for uiFilters and range in APM requests (#99257) * update has_rum_data api query types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts | 6 +++++- x-pack/plugins/apm/server/routes/rum_client.ts | 7 ++++++- x-pack/plugins/observability/server/utils/queries.ts | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 8de2e4e1cca42..87136fc0538a6 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -14,7 +14,11 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeQuery } from '../../../server/utils/queries'; import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; -export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) { +export async function hasRumData({ + setup, +}: { + setup: Setup & Partial; +}) { try { const { start, end } = setup; diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index c723f2c266ca9..bf58c7fcf39b2 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import { jsonRt } from '@kbn/io-ts-utils'; +import { isoToEpochRt } from '@kbn/io-ts-utils'; import { LocalUIFilterName } from '../../common/ui_filter'; import { Setup, @@ -264,7 +265,11 @@ const rumJSErrors = createApmServerRoute({ const rumHasDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: t.partial({ - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.partial({ + uiFilters: t.string, + start: isoToEpochRt, + end: isoToEpochRt, + }), }), options: { tags: ['access:apm'] }, handler: async (resources) => { diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index 584719532ddee..9e1c110e77587 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -8,7 +8,7 @@ import { QueryContainer } from '@elastic/elasticsearch/api/types'; import { esKuery } from '../../../../../src/plugins/data/server'; -export function rangeQuery(start: number, end: number, field = '@timestamp'): QueryContainer[] { +export function rangeQuery(start?: number, end?: number, field = '@timestamp'): QueryContainer[] { return [ { range: { From bb645ef42a5214d0bdfdb373bab01dcdfa5fd5c7 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 14 May 2021 14:33:46 -0400 Subject: [PATCH 061/186] [Uptime] Fix overview flaky tests (#99781) * add retry logic and add describe.only to prepare for flaky test runner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/time_picker.ts | 5 +- .../test/functional/apps/uptime/overview.ts | 57 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index e52bb41e14c15..cfe250831e06c 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -26,6 +26,7 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const log = getService('log'); const find = getService('find'); const browser = getService('browser'); + const retry = getService('retry'); const testSubjects = getService('testSubjects'); const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); @@ -68,7 +69,9 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo } private async getTimePickerPanel() { - return await find.byCssSelector('div.euiPopover__panel-isOpen'); + return await retry.try(async () => { + return await find.byCssSelector('div.euiPopover__panel-isOpen'); + }); } private async waitPanelIsGone(panelElement: WebElementWrapper) { diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index eefb516eeb8f7..1e52accfde1a3 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -26,6 +26,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { beforeEach(async () => { await uptime.goToRoot(); await uptime.setDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); + await uptime.resetFilters(); }); @@ -59,40 +60,46 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('pagination is cleared when filter criteria changes', async () => { await uptime.changePage('next'); - await uptime.pageHasExpectedIds([ - '0010-down', - '0011-up', - '0012-up', - '0013-up', - '0014-up', - '0015-intermittent', - '0016-up', - '0017-up', - '0018-up', - '0019-up', - ]); + await retry.try(async () => { + await uptime.pageHasExpectedIds([ + '0010-down', + '0011-up', + '0012-up', + '0013-up', + '0014-up', + '0015-intermittent', + '0016-up', + '0017-up', + '0018-up', + '0019-up', + ]); + }); // there should now be pagination data in the URL await uptime.pageUrlContains('pagination'); await uptime.setStatusFilter('up'); - await uptime.pageHasExpectedIds([ - '0000-intermittent', - '0001-up', - '0002-up', - '0003-up', - '0004-up', - '0005-up', - '0006-up', - '0007-up', - '0008-up', - '0009-up', - ]); + await retry.try(async () => { + await uptime.pageHasExpectedIds([ + '0000-intermittent', + '0001-up', + '0002-up', + '0003-up', + '0004-up', + '0005-up', + '0006-up', + '0007-up', + '0008-up', + '0009-up', + ]); + }); // ensure that pagination is removed from the URL await uptime.pageUrlContains('pagination', false); }); it('clears pagination parameters when size changes', async () => { await uptime.changePage('next'); - await uptime.pageUrlContains('pagination'); + await retry.try(async () => { + await uptime.pageUrlContains('pagination'); + }); await uptime.setMonitorListPageSize(50); // the pagination parameter should be cleared after a size change await new Promise((resolve) => setTimeout(resolve, 1000)); From 30e4902c6d4fd4eca5e9a3c74f27a37f3b357c4f Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Fri, 14 May 2021 14:40:24 -0400 Subject: [PATCH 062/186] [Security Solution] Interim Host Isolation Case Commenting (#100092) --- x-pack/plugins/cases/server/client/client.ts | 13 +++++++ x-pack/plugins/cases/server/client/mocks.ts | 1 + x-pack/plugins/cases/server/client/types.ts | 5 +++ x-pack/plugins/cases/server/index.ts | 16 +++++++- x-pack/plugins/cases/server/plugin.ts | 10 ++++- .../endpoint/endpoint_app_context_services.ts | 18 +++++++++ .../server/endpoint/mocks.ts | 3 ++ .../endpoint/routes/actions/isolation.ts | 38 +++++++++++++++++++ .../security_solution/server/plugin.ts | 3 ++ 9 files changed, 104 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 3bd25b6b61bc5..d6d153f01e008 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -17,6 +17,7 @@ import { CasesClientGetUserActions, CasesClientGetAlerts, CasesClientPush, + CasesClientGetCasesByAlert, } from './types'; import { create } from './cases/create'; import { update } from './cases/update'; @@ -247,4 +248,16 @@ export class CasesClientHandler implements CasesClient { }); } } + + public async getCaseIdsByAlertId(args: CasesClientGetCasesByAlert) { + try { + return this._caseService.getCaseIdsByAlertId({ + client: this._savedObjectsClient, + alertId: args.alertId, + }); + } catch (error) { + this.logger.error(`Failed to get case using alert id: ${args.alertId}: ${error}`); + throw error; + } + } } diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 4c0f89cf77a67..cb6ef678b6cc2 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -31,6 +31,7 @@ export const createExternalCasesClientMock = (): CasesClientPluginContractMock = getUserActions: jest.fn(), update: jest.fn(), updateAlertsStatus: jest.fn(), + getCaseIdsByAlertId: jest.fn(), }); export const createCasesClientWithMockSavedObjectsClient = async ({ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 3311b7ac6f921..ca4e8790bf2b0 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -83,6 +83,10 @@ export interface ConfigureFields { connectorType: string; } +export interface CasesClientGetCasesByAlert { + alertId: string; +} + /** * Defines the fields necessary to update an alert's status. */ @@ -106,6 +110,7 @@ export interface CasesClient { push(args: CasesClientPush): Promise; update(args: CasesPatchRequest): Promise; updateAlertsStatus(args: CasesClientUpdateAlertsStatus): Promise; + getCaseIdsByAlertId(args: CasesClientGetCasesByAlert): Promise; } export interface MappingsClient { diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 628a39ba77489..40c823b42771f 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -5,7 +5,14 @@ * 2.0. */ -import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; +import { + KibanaRequest, + PluginConfigDescriptor, + PluginInitializerContext, + RequestHandlerContext, +} from 'kibana/server'; +import { CasesClient } from './client'; +export { CasesClient } from './client'; import { ConfigType, ConfigSchema } from './config'; import { CasePlugin } from './plugin'; @@ -18,3 +25,10 @@ export const config: PluginConfigDescriptor = { }; export const plugin = (initializerContext: PluginInitializerContext) => new CasePlugin(initializerContext); + +export interface PluginStartContract { + getCasesClientWithRequestAndContext( + context: RequestHandlerContext, + request: KibanaRequest + ): CasesClient; +} diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 407d6583e5f3f..fda98356e181c 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; +import { + IContextProvider, + KibanaRequest, + Logger, + PluginInitializerContext, + RequestHandlerContext, +} from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -128,7 +134,7 @@ export class CasePlugin { this.log.debug(`Starting Case Workflow`); const getCasesClientWithRequestAndContext = async ( - context: CasesRequestHandlerContext, + context: RequestHandlerContext, request: KibanaRequest ) => { const user = await this.caseService!.getUser({ request }); diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index aebed0723c3b5..2a64e533efad4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -12,6 +12,10 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import { ExceptionListClient } from '../../../lists/server'; +import { + CasesClient, + PluginStartContract as CasesPluginStartContract, +} from '../../../cases/server'; import { SecurityPluginStart } from '../../../security/server'; import { AgentService, @@ -41,6 +45,7 @@ import { ExperimentalFeatures, parseExperimentalConfigValue, } from '../../common/experimental_features'; +import { SecuritySolutionRequestHandlerContext } from '../types'; export interface MetadataService { queryStrategy( @@ -98,6 +103,7 @@ export type EndpointAppContextServiceStartContract = Partial< savedObjectsStart: SavedObjectsServiceStart; licenseService: LicenseService; exceptionListsClient: ExceptionListClient | undefined; + cases: CasesPluginStartContract | undefined; }; /** @@ -114,6 +120,7 @@ export class EndpointAppContextService { private config: ConfigType | undefined; private license: LicenseService | undefined; public security: SecurityPluginStart | undefined; + private cases: CasesPluginStartContract | undefined; private experimentalFeatures: ExperimentalFeatures | undefined; @@ -127,6 +134,7 @@ export class EndpointAppContextService { this.config = dependencies.config; this.license = dependencies.licenseService; this.security = dependencies.security; + this.cases = dependencies.cases; this.experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental); @@ -191,4 +199,14 @@ export class EndpointAppContextService { } return this.license; } + + public async getCasesClient( + req: KibanaRequest, + context: SecuritySolutionRequestHandlerContext + ): Promise { + if (!this.cases) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.cases.getCasesClientWithRequestAndContext(context, req); + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 23ea6cc29c3d2..d8be1cc8de200 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -87,6 +87,9 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< >(), exceptionListsClient: listMock.getExceptionListClient(), packagePolicyService: createPackagePolicyServiceMock(), + cases: { + getCasesClientWithRequestAndContext: jest.fn(), + }, }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 2471eef2bc14d..09d26a20f1095 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import { RequestHandler } from 'src/core/server'; import uuid from 'uuid'; import { TypeOf } from '@kbn/config-schema'; +import { CommentType } from '../../../../../cases/common'; import { HostIsolationRequestSchema } from '../../../../common/endpoint/schema/actions'; import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; @@ -104,6 +105,20 @@ export const isolationRequestHandler = function ( } agentIDs = [...new Set(agentIDs)]; // dedupe + // convert any alert IDs into cases + let caseIDs: string[] = req.body.case_ids?.slice() || []; + if (req.body.alert_ids && req.body.alert_ids.length > 0) { + const newIDs: string[][] = await Promise.all( + req.body.alert_ids.map(async (a: string) => + (await endpointContext.service.getCasesClient(req, context)).getCaseIdsByAlertId({ + alertId: a, + }) + ) + ); + caseIDs = caseIDs.concat(...newIDs); + } + caseIDs = [...new Set(caseIDs)]; + // create an Action ID and dispatch it to ES & Fleet Server const esClient = context.core.elasticsearch.client.asCurrentUser; const actionID = uuid.v4(); @@ -140,6 +155,29 @@ export const isolationRequestHandler = function ( }, }); } + + const commentLines: string[] = []; + + commentLines.push(`${isolate ? 'I' : 'Uni'}solate action was sent to the following Agents:`); + // lines of markdown links, inside a code block + + commentLines.push( + `${agentIDs.map((a) => `- [${a}](/app/fleet#/fleet/agents/${a})`).join('\n')}` + ); + if (req.body.comment) { + commentLines.push(`\n\nWith Comment:\n> ${req.body.comment}`); + } + + caseIDs.forEach(async (caseId) => { + (await endpointContext.service.getCasesClient(req, context)).addComment({ + caseId, + comment: { + comment: commentLines.join('\n'), + type: CommentType.user, + }, + }); + }); + return res.ok({ body: { action: actionID, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index efeabc844a810..aa37a0dc1f627 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -27,6 +27,7 @@ import { PluginSetupContract as AlertingSetup, PluginStartContract as AlertPluginStartContract, } from '../../alerting/server'; +import { PluginStartContract as CasesPluginStartContract } from '../../cases/server'; import { SecurityPluginSetup as SecuritySetup, SecurityPluginStart } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; @@ -101,6 +102,7 @@ export interface StartPlugins { taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; security: SecurityPluginStart; + cases?: CasesPluginStartContract; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -412,6 +414,7 @@ export class Plugin implements IPlugin Date: Fri, 14 May 2021 14:46:17 -0400 Subject: [PATCH 063/186] Sharing saved objects phase 3 (#94383) --- api_docs/spaces.json | 2 +- .../core/public/kibana-plugin-core-public.md | 2 + ...blic.savedobjectreferencewithcontext.id.md | 13 + ...treferencewithcontext.inboundreferences.md | 17 + ...vedobjectreferencewithcontext.ismissing.md | 13 + ...-public.savedobjectreferencewithcontext.md | 25 + ....savedobjectreferencewithcontext.spaces.md | 13 + ...cewithcontext.spaceswithmatchingaliases.md | 13 + ...ic.savedobjectreferencewithcontext.type.md | 13 + ...collectmultinamespacereferencesresponse.md | 20 + ...ultinamespacereferencesresponse.objects.md | 11 + ...ver.isavedobjectspointintimefinder.find.md | 2 +- ...e-server.isavedobjectspointintimefinder.md | 4 +- .../core/server/kibana-plugin-core-server.md | 12 +- ...jectexportbaseoptions.includenamespaces.md | 13 + ...ore-server.savedobjectexportbaseoptions.md | 1 + ...rver.savedobjectreferencewithcontext.id.md | 13 + ...treferencewithcontext.inboundreferences.md | 17 + ...vedobjectreferencewithcontext.ismissing.md | 13 + ...-server.savedobjectreferencewithcontext.md | 25 + ....savedobjectreferencewithcontext.spaces.md | 13 + ...cewithcontext.spaceswithmatchingaliases.md | 13 + ...er.savedobjectreferencewithcontext.type.md | 13 + ...rver.savedobjectsaddtonamespacesoptions.md | 20 - ...edobjectsaddtonamespacesoptions.refresh.md | 13 - ...edobjectsaddtonamespacesoptions.version.md | 13 - ...ver.savedobjectsaddtonamespacesresponse.md | 19 - ...jectsaddtonamespacesresponse.namespaces.md | 13 - ...rver.savedobjectsclient.addtonamespaces.md | 27 - ...sclient.collectmultinamespacereferences.md | 25 + ...edobjectsclient.createpointintimefinder.md | 4 +- ...savedobjectsclient.deletefromnamespaces.md | 27 - ...a-plugin-core-server.savedobjectsclient.md | 4 +- ....savedobjectsclient.updateobjectsspaces.md | 27 + ...ollectmultinamespacereferencesobject.id.md | 11 + ...tscollectmultinamespacereferencesobject.md | 23 + ...lectmultinamespacereferencesobject.type.md | 11 + ...scollectmultinamespacereferencesoptions.md | 20 + ...multinamespacereferencesoptions.purpose.md | 13 + ...collectmultinamespacereferencesresponse.md | 20 + ...ultinamespacereferencesresponse.objects.md | 11 + ...savedobjectsdeletefromnamespacesoptions.md | 19 - ...ectsdeletefromnamespacesoptions.refresh.md | 13 - ...avedobjectsdeletefromnamespacesresponse.md | 19 - ...deletefromnamespacesresponse.namespaces.md | 13 - ....savedobjectsrepository.addtonamespaces.md | 27 - ...ository.collectmultinamespacereferences.md | 25 + ...jectsrepository.createpointintimefinder.md | 4 +- ...dobjectsrepository.deletefromnamespaces.md | 27 - ...ugin-core-server.savedobjectsrepository.md | 4 +- ...edobjectsrepository.updateobjectsspaces.md | 27 + ...savedobjectsserializer.rawtosavedobject.md | 4 +- ...avedobjectsupdateobjectsspacesobject.id.md | 13 + ...r.savedobjectsupdateobjectsspacesobject.md | 21 + ...edobjectsupdateobjectsspacesobject.type.md | 13 + ....savedobjectsupdateobjectsspacesoptions.md | 20 + ...jectsupdateobjectsspacesoptions.refresh.md | 13 + ...savedobjectsupdateobjectsspacesresponse.md | 20 + ...ectsupdateobjectsspacesresponse.objects.md | 11 + ...updateobjectsspacesresponseobject.error.md | 13 + ...ctsupdateobjectsspacesresponseobject.id.md | 13 + ...bjectsupdateobjectsspacesresponseobject.md | 23 + ...pdateobjectsspacesresponseobject.spaces.md | 13 + ...supdateobjectsspacesresponseobject.type.md | 13 + ...rver.indexpatternsserviceprovider.start.md | 4 +- ...plugin-plugins-data-server.plugin.start.md | 4 +- src/core/public/index.ts | 2 + src/core/public/public.api.md | 20 + src/core/public/saved_objects/index.ts | 2 + src/core/server/index.ts | 12 +- .../export/saved_objects_exporter.test.ts | 23 + .../export/saved_objects_exporter.ts | 9 +- src/core/server/saved_objects/export/types.ts | 6 + .../migrations/core/document_migrator.test.ts | 4 + .../migrations/core/document_migrator.ts | 1 + .../integration_tests/rewriting_id.test.ts | 2 + .../object_types/registration.ts | 11 +- .../saved_objects/object_types/types.ts | 36 + .../saved_objects/serialization/serializer.ts | 4 +- .../server/saved_objects/service/index.ts | 8 + ...ct_multi_namespace_references.test.mock.ts | 21 + ...collect_multi_namespace_references.test.ts | 444 ++++++++++ .../lib/collect_multi_namespace_references.ts | 310 +++++++ .../service/lib/included_fields.test.ts | 136 +-- .../service/lib/included_fields.ts | 25 +- .../server/saved_objects/service/lib/index.ts | 14 + .../service/lib/internal_utils.test.ts | 243 ++++++ .../service/lib/internal_utils.ts | 143 +++ .../service/lib/point_in_time_finder.ts | 9 +- .../service/lib/repository.mock.ts | 4 +- .../service/lib/repository.test.js | 660 +++----------- .../service/lib/repository.test.mock.ts | 30 + .../saved_objects/service/lib/repository.ts | 368 ++------ .../lib/update_objects_spaces.test.mock.ts | 29 + .../service/lib/update_objects_spaces.test.ts | 453 ++++++++++ .../service/lib/update_objects_spaces.ts | 315 +++++++ .../service/saved_objects_client.mock.ts | 4 +- .../service/saved_objects_client.test.js | 49 +- .../service/saved_objects_client.ts | 116 +-- src/core/server/server.api.md | 100 ++- src/core/server/types.ts | 4 + src/plugins/data/server/server.api.md | 4 +- src/plugins/spaces_oss/public/api.ts | 4 +- .../apis/saved_objects/migrations.ts | 2 + ...ypted_saved_objects_client_wrapper.test.ts | 76 ++ .../encrypted_saved_objects_client_wrapper.ts | 50 +- .../job_spaces_list/job_spaces_list.tsx | 18 +- .../services/ml_api_service/saved_objects.ts | 19 +- .../models/data_recognizer/data_recognizer.ts | 5 +- x-pack/plugins/ml/server/routes/apidoc.json | 3 +- .../plugins/ml/server/routes/saved_objects.ts | 60 +- .../ml/server/routes/schemas/saved_objects.ts | 3 +- .../ml/server/saved_objects/service.ts | 74 +- .../security/server/audit/audit_events.ts | 14 + .../saved_objects/ensure_authorized.test.ts | 226 +++++ .../server/saved_objects/ensure_authorized.ts | 165 ++++ ...saved_objects_client_wrapper.test.mocks.ts | 17 + ...ecure_saved_objects_client_wrapper.test.ts | 816 +++++++++++++----- .../secure_saved_objects_client_wrapper.ts | 468 +++++++--- x-pack/plugins/spaces/common/index.ts | 2 +- .../share_to_space_flyout_internal.test.tsx | 62 +- .../share_to_space_flyout_internal.tsx | 85 +- .../spaces_manager/spaces_manager.mock.ts | 4 +- .../public/spaces_manager/spaces_manager.ts | 23 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 159 +++- .../lib/copy_to_spaces/copy_to_spaces.ts | 21 +- .../copy_to_spaces/resolve_copy_conflicts.ts | 4 + .../external/get_shareable_references.test.ts | 144 ++++ .../api/external/get_shareable_references.ts | 42 + .../server/routes/api/external/index.ts | 6 +- .../api/external/share_to_space.test.ts | 252 ------ .../routes/api/external/share_to_space.ts | 77 -- .../external/update_objects_spaces.test.ts | 176 ++++ .../api/external/update_objects_spaces.ts | 70 ++ .../spaces_saved_objects_client.test.ts | 125 +-- .../spaces_saved_objects_client.ts | 87 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../apis/ml/saved_objects/can_delete_job.ts | 2 +- .../apis/ml/saved_objects/index.ts | 3 +- .../ml/saved_objects/remove_job_from_space.ts | 104 --- ..._job_to_space.ts => update_jobs_spaces.ts} | 27 +- x-pack/test/functional/services/ml/api.ts | 17 +- .../saved_objects/spaces/data.json | 3 + .../saved_objects/spaces/data.json | 70 ++ .../common/suites/copy_to_space.ts | 114 ++- .../common/suites/get_shareable_references.ts | 270 ++++++ .../common/suites/share_add.ts | 110 --- .../common/suites/share_remove.ts | 109 --- .../common/suites/update_objects_spaces.ts | 142 +++ .../apis/get_shareable_references.ts | 86 ++ .../security_and_spaces/apis/index.ts | 4 +- .../security_and_spaces/apis/share_add.ts | 144 ---- .../security_and_spaces/apis/share_remove.ts | 104 --- .../apis/update_objects_spaces.ts | 170 ++++ .../apis/get_shareable_references.ts | 62 ++ .../spaces_only/apis/index.ts | 4 +- .../spaces_only/apis/share_add.ts | 100 --- .../spaces_only/apis/share_remove.ts | 110 --- .../spaces_only/apis/update_objects_spaces.ts | 143 +++ 160 files changed, 6574 insertions(+), 3270 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md create mode 100644 src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts create mode 100644 src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts create mode 100644 src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts create mode 100644 src/core/server/saved_objects/service/lib/internal_utils.test.ts create mode 100644 src/core/server/saved_objects/service/lib/internal_utils.ts create mode 100644 src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts create mode 100644 src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts create mode 100644 src/core/server/saved_objects/service/lib/update_objects_spaces.ts create mode 100644 x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts create mode 100644 x-pack/plugins/security/server/saved_objects/ensure_authorized.ts create mode 100644 x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts delete mode 100644 x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts rename x-pack/test/api_integration/apis/ml/saved_objects/{assign_job_to_space.ts => update_jobs_spaces.ts} (85%) create mode 100644 x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts delete mode 100644 x-pack/test/spaces_api_integration/common/suites/share_add.ts delete mode 100644 x-pack/test/spaces_api_integration/common/suites/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts create mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts delete mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts delete mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts create mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts delete mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts delete mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts create mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts diff --git a/api_docs/spaces.json b/api_docs/spaces.json index d53b69d5bd6b5..940bbcf88a484 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1867,7 +1867,7 @@ "section": "def-server.SavedObjectsRepository", "text": "SavedObjectsRepository" }, - ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" + ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"collectMultiNamespaceReferences\" | \"updateObjectsSpaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" ], "source": { "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts", diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b868a7f8216df..5280d85f3d3b3 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -103,12 +103,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttributes](./kibana-plugin-core-public.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | | [SavedObjectError](./kibana-plugin-core-public.savedobjecterror.md) | | | [SavedObjectReference](./kibana-plugin-core-public.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. | | [SavedObjectsBaseOptions](./kibana-plugin-core-public.savedobjectsbaseoptions.md) | | | [SavedObjectsBatchResponse](./kibana-plugin-core-public.savedobjectsbatchresponse.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-public.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkCreateOptions](./kibana-plugin-core-public.savedobjectsbulkcreateoptions.md) | | | [SavedObjectsBulkUpdateObject](./kibana-plugin-core-public.savedobjectsbulkupdateobject.md) | | | [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-public.savedobjectsbulkupdateoptions.md) | | +| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. | | [SavedObjectsCreateOptions](./kibana-plugin-core-public.savedobjectscreateoptions.md) | | | [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) | | | [SavedObjectsFindOptionsReference](./kibana-plugin-core-public.savedobjectsfindoptionsreference.md) | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md new file mode 100644 index 0000000000000..10e01d7e7a931 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) + +## SavedObjectReferenceWithContext.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md new file mode 100644 index 0000000000000..722b11f0c7ba9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) + +## SavedObjectReferenceWithContext.inboundReferences property + +References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation + +Signature: + +```typescript +inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md new file mode 100644 index 0000000000000..8a4b378850764 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) + +## SavedObjectReferenceWithContext.isMissing property + +Whether or not this object or reference is missing + +Signature: + +```typescript +isMissing?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md new file mode 100644 index 0000000000000..a79fa96695e36 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) + +## SavedObjectReferenceWithContext interface + +A returned input object or one of its references, with additional context. + +Signature: + +```typescript +export interface SavedObjectReferenceWithContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-public.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-public.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing](./kibana-plugin-core-public.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md new file mode 100644 index 0000000000000..9140e94721f1e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [spaces](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaces.md) + +## SavedObjectReferenceWithContext.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md new file mode 100644 index 0000000000000..02b0c9c0949df --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [spacesWithMatchingAliases](./kibana-plugin-core-public.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) + +## SavedObjectReferenceWithContext.spacesWithMatchingAliases property + +The space(s) that legacy URL aliases matching this type/id exist in + +Signature: + +```typescript +spacesWithMatchingAliases?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md new file mode 100644 index 0000000000000..d2e341627153c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectreferencewithcontext.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-public.savedobjectreferencewithcontext.md) > [type](./kibana-plugin-core-public.savedobjectreferencewithcontext.type.md) + +## SavedObjectReferenceWithContext.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md new file mode 100644 index 0000000000000..a6e0a274008a6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse interface + +The response when object references are collected. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md new file mode 100644 index 0000000000000..66a7a19d18288 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.md) > [objects](./kibana-plugin-core-public.savedobjectscollectmultinamespacereferencesresponse.objects.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectReferenceWithContext[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md index 1755ff40c2bc0..29d4668becffc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.find.md @@ -9,5 +9,5 @@ An async generator which wraps calls to `savedObjectsClient.find` and iterates o Signature: ```typescript -find: () => AsyncGenerator; +find: () => AsyncGenerator>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md index 4686df18e0134..950d6c078654c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.isavedobjectspointintimefinder.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface ISavedObjectsPointInTimeFinder +export interface ISavedObjectsPointInTimeFinder ``` ## Properties @@ -16,5 +16,5 @@ export interface ISavedObjectsPointInTimeFinder | Property | Type | Description | | --- | --- | --- | | [close](./kibana-plugin-core-server.isavedobjectspointintimefinder.close.md) | () => Promise<void> | Closes the Point-In-Time associated with this finder instance.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | -| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | +| [find](./kibana-plugin-core-server.isavedobjectspointintimefinder.find.md) | () => AsyncGenerator<SavedObjectsFindResponse<T, A>> | An async generator which wraps calls to savedObjectsClient.find and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage size. | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 3a9118a9c56bd..d638b84224e23 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -144,8 +144,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectMigrationContext](./kibana-plugin-core-server.savedobjectmigrationcontext.md) | Migration context provided when invoking a [migration handler](./kibana-plugin-core-server.savedobjectmigrationfn.md) | | [SavedObjectMigrationMap](./kibana-plugin-core-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions, and they cannot exceed the current Kibana version.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-core-server.savedobjectreference.md) | A reference to another saved object. | -| [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) | | -| [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) | | +| [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) | A returned input object or one of its references, with additional context. | | [SavedObjectsBaseOptions](./kibana-plugin-core-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) | | | [SavedObjectsBulkGetObject](./kibana-plugin-core-server.savedobjectsbulkgetobject.md) | | @@ -158,13 +157,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) | | +| [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) | An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the namespaceType: 'multi' or namespaceType: 'multi-isolated' option).Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the namespaceType: 'multi'). | +| [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) | Options for collecting references. | +| [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) | The response when object references are collected. | | [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsCreatePointInTimeFinderDependencies](./kibana-plugin-core-server.savedobjectscreatepointintimefinderdependencies.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | -| [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) | | -| [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | @@ -208,6 +208,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsType](./kibana-plugin-core-server.savedobjectstype.md) | | | [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) | Configuration options for the [type](./kibana-plugin-core-server.savedobjectstype.md)'s management section. | | [SavedObjectsTypeMappingDefinition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | +| [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) | An object that should have its spaces updated. | +| [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) | Options for the update operation. | +| [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) | The response when objects' spaces are updated. | +| [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) | Details about a specific object's update result. | | [SavedObjectsUpdateOptions](./kibana-plugin-core-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-core-server.savedobjectsupdateresponse.md) | | | [SearchResponse](./kibana-plugin-core-server.searchresponse.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md new file mode 100644 index 0000000000000..8ac532c601efc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectExportBaseOptions](./kibana-plugin-core-server.savedobjectexportbaseoptions.md) > [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) + +## SavedObjectExportBaseOptions.includeNamespaces property + +Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. + +Signature: + +```typescript +includeNamespaces?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md index 0e8fa73039d40..cd0c352086425 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectexportbaseoptions.md @@ -16,6 +16,7 @@ export interface SavedObjectExportBaseOptions | Property | Type | Description | | --- | --- | --- | | [excludeExportDetails](./kibana-plugin-core-server.savedobjectexportbaseoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) to the end of the export stream. | +| [includeNamespaces](./kibana-plugin-core-server.savedobjectexportbaseoptions.includenamespaces.md) | boolean | Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP route for exports. | | [includeReferencesDeep](./kibana-plugin-core-server.savedobjectexportbaseoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | | [namespace](./kibana-plugin-core-server.savedobjectexportbaseoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | | [request](./kibana-plugin-core-server.savedobjectexportbaseoptions.request.md) | KibanaRequest | The http request initiating the export. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md new file mode 100644 index 0000000000000..7ef1a2fb1bd41 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) + +## SavedObjectReferenceWithContext.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md new file mode 100644 index 0000000000000..058c27032d065 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) + +## SavedObjectReferenceWithContext.inboundReferences property + +References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation + +Signature: + +```typescript +inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md new file mode 100644 index 0000000000000..d46d5a6bf2a0a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) + +## SavedObjectReferenceWithContext.isMissing property + +Whether or not this object or reference is missing + +Signature: + +```typescript +isMissing?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md new file mode 100644 index 0000000000000..1f8b33c6e94e8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) + +## SavedObjectReferenceWithContext interface + +A returned input object or one of its references, with additional context. + +Signature: + +```typescript +export interface SavedObjectReferenceWithContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectreferencewithcontext.id.md) | string | The ID of the referenced object | +| [inboundReferences](./kibana-plugin-core-server.savedobjectreferencewithcontext.inboundreferences.md) | Array<{
type: string;
id: string;
name: string;
}> | References to this object; note that this does not contain \_all inbound references everywhere for this object\_, it only contains inbound references for the scope of this operation | +| [isMissing](./kibana-plugin-core-server.savedobjectreferencewithcontext.ismissing.md) | boolean | Whether or not this object or reference is missing | +| [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) | string[] | The space(s) that legacy URL aliases matching this type/id exist in | +| [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md new file mode 100644 index 0000000000000..2c2114103b29a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [spaces](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaces.md) + +## SavedObjectReferenceWithContext.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md new file mode 100644 index 0000000000000..07f4158a84950 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [spacesWithMatchingAliases](./kibana-plugin-core-server.savedobjectreferencewithcontext.spaceswithmatchingaliases.md) + +## SavedObjectReferenceWithContext.spacesWithMatchingAliases property + +The space(s) that legacy URL aliases matching this type/id exist in + +Signature: + +```typescript +spacesWithMatchingAliases?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md new file mode 100644 index 0000000000000..118d9744e4276 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectreferencewithcontext.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectReferenceWithContext](./kibana-plugin-core-server.savedobjectreferencewithcontext.md) > [type](./kibana-plugin-core-server.savedobjectreferencewithcontext.type.md) + +## SavedObjectReferenceWithContext.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md deleted file mode 100644 index 711588bdd608c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) - -## SavedObjectsAddToNamespacesOptions interface - - -Signature: - -```typescript -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | -| [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) | string | An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md deleted file mode 100644 index c0a1008ab5331..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.refresh.md) - -## SavedObjectsAddToNamespacesOptions.refresh property - -The Elasticsearch Refresh setting for this operation - -Signature: - -```typescript -refresh?: MutatingOperationRefreshSetting; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md deleted file mode 100644 index 9432b4bf80da6..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesOptions](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.md) > [version](./kibana-plugin-core-server.savedobjectsaddtonamespacesoptions.version.md) - -## SavedObjectsAddToNamespacesOptions.version property - -An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. - -Signature: - -```typescript -version?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md deleted file mode 100644 index 306f502f0b0b3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) - -## SavedObjectsAddToNamespacesResponse interface - - -Signature: - -```typescript -export interface SavedObjectsAddToNamespacesResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) | string[] | The namespaces the object exists in after this operation is complete. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md deleted file mode 100644 index 4fc2e376304d4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsAddToNamespacesResponse](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.md) > [namespaces](./kibana-plugin-core-server.savedobjectsaddtonamespacesresponse.namespaces.md) - -## SavedObjectsAddToNamespacesResponse.namespaces property - -The namespaces the object exists in after this operation is complete. - -Signature: - -```typescript -namespaces: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md deleted file mode 100644 index 567390faba9b2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) - -## SavedObjectsClient.addToNamespaces() method - -Adds namespaces to a SavedObject - -Signature: - -```typescript -addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsAddToNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md new file mode 100644 index 0000000000000..155167d32a738 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) + +## SavedObjectsClient.collectMultiNamespaceReferences() method + +Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. + +Signature: + +```typescript +collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md index 8afd963464574..39d09807e4f3b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md @@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call ` Signature: ```typescript -createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; ``` ## Parameters @@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, Returns: -`ISavedObjectsPointInTimeFinder` +`ISavedObjectsPointInTimeFinder` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md deleted file mode 100644 index 18ef5c3e6350c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) - -## SavedObjectsClient.deleteFromNamespaces() method - -Removes namespaces from a SavedObject - -Signature: - -```typescript -deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsDeleteFromNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index 95c2251f72c90..2e293889b1794 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -25,20 +25,20 @@ The constructor for this class is marked as internal. Third-party code should no | Method | Modifiers | Description | | --- | --- | --- | -| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.addtonamespaces.md) | | Adds namespaces to a SavedObject | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md).Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | +| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsclient.collectmultinamespacereferences.md) | | Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.The generator wraps calls to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | -| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | | [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsClient.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsclient.createpointintimefinder.md) method. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | +| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md new file mode 100644 index 0000000000000..7ababbbe1f535 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsclient.updateobjectsspaces.md) + +## SavedObjectsClient.updateObjectsSpaces() method + +Updates one or more objects to add and/or remove them from specified spaces. + +Signature: + +```typescript +updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsUpdateObjectsSpacesObject[] | | +| spacesToAdd | string[] | | +| spacesToRemove | string[] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md new file mode 100644 index 0000000000000..21522a0f32d6d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) > [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md new file mode 100644 index 0000000000000..e675658f2bf76 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject interface + +An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the `namespaceType: 'multi'` or `namespaceType: 'multi-isolated'` option). + +Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with the `namespaceType: 'multi'`). + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.id.md) | string | | +| [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md new file mode 100644 index 0000000000000..c376a9e4258c8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesObject](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.md) > [type](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesobject.type.md) + +## SavedObjectsCollectMultiNamespaceReferencesObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md new file mode 100644 index 0000000000000..9311a66269753 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) + +## SavedObjectsCollectMultiNamespaceReferencesOptions interface + +Options for collecting references. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) | 'collectMultiNamespaceReferences' | 'updateObjectsSpaces' | Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md new file mode 100644 index 0000000000000..a36301a6451bc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesOptions](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.md) > [purpose](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesoptions.purpose.md) + +## SavedObjectsCollectMultiNamespaceReferencesOptions.purpose property + +Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' + +Signature: + +```typescript +purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md new file mode 100644 index 0000000000000..bc72e73994468 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse interface + +The response when object references are collected. + +Signature: + +```typescript +export interface SavedObjectsCollectMultiNamespaceReferencesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) | SavedObjectReferenceWithContext[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md new file mode 100644 index 0000000000000..4b5707d7228a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCollectMultiNamespaceReferencesResponse](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.md) > [objects](./kibana-plugin-core-server.savedobjectscollectmultinamespacereferencesresponse.objects.md) + +## SavedObjectsCollectMultiNamespaceReferencesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectReferenceWithContext[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md deleted file mode 100644 index 8a2afe6656fa4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) - -## SavedObjectsDeleteFromNamespacesOptions interface - - -Signature: - -```typescript -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md deleted file mode 100644 index 1175b79bc1abd..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesOptions](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesoptions.refresh.md) - -## SavedObjectsDeleteFromNamespacesOptions.refresh property - -The Elasticsearch Refresh setting for this operation - -Signature: - -```typescript -refresh?: MutatingOperationRefreshSetting; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md deleted file mode 100644 index 6021c8866f018..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) - -## SavedObjectsDeleteFromNamespacesResponse interface - - -Signature: - -```typescript -export interface SavedObjectsDeleteFromNamespacesResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) | string[] | The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md deleted file mode 100644 index 9600a9e891380..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsDeleteFromNamespacesResponse](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.md) > [namespaces](./kibana-plugin-core-server.savedobjectsdeletefromnamespacesresponse.namespaces.md) - -## SavedObjectsDeleteFromNamespacesResponse.namespaces property - -The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. - -Signature: - -```typescript -namespaces: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md deleted file mode 100644 index 4b69b10318ed3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [addToNamespaces](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) - -## SavedObjectsRepository.addToNamespaces() method - -Adds one or more namespaces to a given multi-namespace saved object. This method and \[`deleteFromNamespaces`\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. - -Signature: - -```typescript -addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsAddToNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md new file mode 100644 index 0000000000000..450cd14a20524 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [collectMultiNamespaceReferences](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) + +## SavedObjectsRepository.collectMultiNamespaceReferences() method + +Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. + +Signature: + +```typescript +collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsCollectMultiNamespaceReferencesObject[] | | +| options | SavedObjectsCollectMultiNamespaceReferencesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md index 5d9d2857f6e0b..c92a1986966fd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md @@ -15,7 +15,7 @@ Once you have retrieved all of the results you need, it is recommended to call ` Signature: ```typescript -createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; +createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; ``` ## Parameters @@ -27,7 +27,7 @@ createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, Returns: -`ISavedObjectsPointInTimeFinder` +`ISavedObjectsPointInTimeFinder` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md deleted file mode 100644 index d5ffb6d9ff9d8..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [deleteFromNamespaces](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) - -## SavedObjectsRepository.deleteFromNamespaces() method - -Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[`addToNamespaces`\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. - -Signature: - -```typescript -deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | -| id | string | | -| namespaces | string[] | | -| options | SavedObjectsDeleteFromNamespacesOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 00e6ed3aeddfc..191b125ef3f74 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -15,17 +15,16 @@ export declare class SavedObjectsRepository | Method | Modifiers | Description | | --- | --- | --- | -| [addToNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) | | Adds one or more namespaces to a given multi-namespace saved object. This method and \[deleteFromNamespaces\][SavedObjectsRepository.deleteFromNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [bulkCreate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkcreate.md) | | Creates multiple documents at once | | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | | [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using openPointInTimeForType.Only use this API if you have an advanced use case that's not solved by the [SavedObjectsRepository.createPointInTimeFinder()](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) method. | +| [collectMultiNamespaceReferences(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.collectmultinamespacereferences.md) | | Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace type. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [createPointInTimeFinder(findOptions, dependencies)](./kibana-plugin-core-server.savedobjectsrepository.createpointintimefinder.md) | | Returns a [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) to help page through large sets of saved objects. We strongly recommend using this API for any find queries that might return more than 1000 saved objects, however this API is only intended for use in server-side "batch" processing of objects where you are collecting all objects in memory or streaming them back to the client.Do NOT use this API in a route handler to facilitate paging through saved objects on the client-side unless you are streaming all of the results back to the client at once. Because the returned generator is stateful, you cannot rely on subsequent http requests retrieving new pages from the same Kibana server in multi-instance deployments.This generator wraps calls to [SavedObjectsRepository.find()](./kibana-plugin-core-server.savedobjectsrepository.find.md) and iterates over multiple pages of results using _pit and search_after. This will open a new Point-In-Time (PIT), and continue paging until a set of results is received that's smaller than the designated perPage.Once you have retrieved all of the results you need, it is recommended to call close() to clean up the PIT and prevent Elasticsearch from consuming resources unnecessarily. This is only required if you are done iterating and have not yet paged through all of the results: the PIT will automatically be closed for you once you reach the last page of results, or if the underlying call to find fails for any reason. | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | -| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. | @@ -33,4 +32,5 @@ export declare class SavedObjectsRepository | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | +| [updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options)](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) | | Updates one or more objects to add and/or remove them from specified spaces. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md new file mode 100644 index 0000000000000..6914c1b46b829 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [updateObjectsSpaces](./kibana-plugin-core-server.savedobjectsrepository.updateobjectsspaces.md) + +## SavedObjectsRepository.updateObjectsSpaces() method + +Updates one or more objects to add and/or remove them from specified spaces. + +Signature: + +```typescript +updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsUpdateObjectsSpacesObject[] | | +| spacesToAdd | string[] | | +| spacesToRemove | string[] | | +| options | SavedObjectsUpdateObjectsSpacesOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md index 3fc386f263141..d71db9caf6a3b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsserializer.rawtosavedobject.md @@ -9,7 +9,7 @@ Converts a document from the format that is stored in elasticsearch to the saved Signature: ```typescript -rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; +rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; ``` ## Parameters @@ -21,5 +21,5 @@ rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptio Returns: -`SavedObjectSanitizedDoc` +`SavedObjectSanitizedDoc` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md new file mode 100644 index 0000000000000..dac110ac4f475 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) > [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) + +## SavedObjectsUpdateObjectsSpacesObject.id property + +The type of the object to update + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md new file mode 100644 index 0000000000000..847e40a8896b4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) + +## SavedObjectsUpdateObjectsSpacesObject interface + +An object that should have its spaces updated. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.id.md) | string | The type of the object to update | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) | string | The ID of the object to update | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md new file mode 100644 index 0000000000000..2e54d1636c5e9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.md) > [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesobject.type.md) + +## SavedObjectsUpdateObjectsSpacesObject.type property + +The ID of the object to update + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md new file mode 100644 index 0000000000000..49ee013c5d2da --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) + +## SavedObjectsUpdateObjectsSpacesOptions interface + +Options for the update operation. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) | MutatingOperationRefreshSetting | The Elasticsearch Refresh setting for this operation | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md new file mode 100644 index 0000000000000..3d210f6ac51c7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesOptions](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.md) > [refresh](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesoptions.refresh.md) + +## SavedObjectsUpdateObjectsSpacesOptions.refresh property + +The Elasticsearch Refresh setting for this operation + +Signature: + +```typescript +refresh?: MutatingOperationRefreshSetting; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md new file mode 100644 index 0000000000000..bf53277887bda --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) + +## SavedObjectsUpdateObjectsSpacesResponse interface + +The response when objects' spaces are updated. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) | SavedObjectsUpdateObjectsSpacesResponseObject[] | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md new file mode 100644 index 0000000000000..13328e2aed094 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponse](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.md) > [objects](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponse.objects.md) + +## SavedObjectsUpdateObjectsSpacesResponse.objects property + +Signature: + +```typescript +objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md new file mode 100644 index 0000000000000..7d7ac4ada884d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.error property + +Included if there was an error updating this object's spaces + +Signature: + +```typescript +error?: SavedObjectError; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md new file mode 100644 index 0000000000000..28a81ee5dfd6a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.id property + +The ID of the referenced object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md new file mode 100644 index 0000000000000..03802278ee5a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject interface + +Details about a specific object's update result. + +Signature: + +```typescript +export interface SavedObjectsUpdateObjectsSpacesResponseObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.error.md) | SavedObjectError | Included if there was an error updating this object's spaces | +| [id](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.id.md) | string | The ID of the referenced object | +| [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) | string[] | The space(s) that the referenced object exists in | +| [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) | string | The type of the referenced object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md new file mode 100644 index 0000000000000..52b1ca187925c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [spaces](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.spaces.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.spaces property + +The space(s) that the referenced object exists in + +Signature: + +```typescript +spaces: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md new file mode 100644 index 0000000000000..da0bbb1088507 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUpdateObjectsSpacesResponseObject](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.md) > [type](./kibana-plugin-core-server.savedobjectsupdateobjectsspacesresponseobject.type.md) + +## SavedObjectsUpdateObjectsSpacesResponseObject.type property + +The type of the referenced object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 118b0104fbee6..7559695a0a331 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index f4404521561d2..dd1f3806c1408 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }` diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 17ba37d075b78..7d2a585084758 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -144,6 +144,8 @@ export type { SavedObjectsImportSimpleWarning, SavedObjectsImportActionRequiredWarning, SavedObjectsImportWarning, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from './saved_objects'; export { HttpFetchError } from './http'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4ea3b56c60a8f..129a7e565394f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1173,6 +1173,20 @@ export interface SavedObjectReference { type: string; } +// @public +export interface SavedObjectReferenceWithContext { + id: string; + inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; + isMissing?: boolean; + spaces: string[]; + spacesWithMatchingAliases?: string[]; + type: string; +} + // @public (undocumented) export interface SavedObjectsBaseOptions { namespace?: string; @@ -1240,6 +1254,12 @@ export class SavedObjectsClient { // @public export type SavedObjectsClientContract = PublicMethodsOf; +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + // (undocumented) + objects: SavedObjectReferenceWithContext[]; +} + // @public (undocumented) export interface SavedObjectsCreateOptions { coreMigrationVersion?: string; diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index e8aef50376841..cd75bc16f8362 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -39,6 +39,8 @@ export type { SavedObjectsImportSimpleWarning, SavedObjectsImportActionRequiredWarning, SavedObjectsImportWarning, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, } from '../../server/types'; export type { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ca328f17b2ae1..05408d839c0ae 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -320,12 +320,16 @@ export type { SavedObjectsResolveResponse, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, - SavedObjectsAddToNamespacesOptions, - SavedObjectsAddToNamespacesResponse, - SavedObjectsDeleteFromNamespacesOptions, - SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, SavedObjectsServiceStart, SavedObjectsServiceSetup, SavedObjectStatusMeta, diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 468a761781365..6bdb8003de49d 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -1149,6 +1149,29 @@ describe('getSortedObjectsForExport()', () => { ]); }); + test('return results including the `namespaces` attribute when includeNamespaces option is used', async () => { + const createSavedObject = (obj: any) => ({ ...obj, attributes: {}, references: [] }); + const objectResults = [ + createSavedObject({ type: 'multi', id: '1', namespaces: ['foo'] }), + createSavedObject({ type: 'multi', id: '2', namespaces: ['bar'] }), + createSavedObject({ type: 'other', id: '3' }), + ]; + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: objectResults, + }); + const exportStream = await exporter.exportByObjects({ + request, + objects: [ + { type: 'multi', id: '1' }, + { type: 'multi', id: '2' }, + { type: 'other', id: '3' }, + ], + includeNamespaces: true, + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual([...objectResults, expect.objectContaining({ exportedCount: 3 })]); + }); + test('includes nested dependencies when passed in', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 868efa872d643..8cd6934bf1af9 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -77,6 +77,7 @@ export class SavedObjectsExporter { return this.processObjects(objects, byIdAscComparator, { request: options.request, includeReferencesDeep: options.includeReferencesDeep, + includeNamespaces: options.includeNamespaces, excludeExportDetails: options.excludeExportDetails, namespace: options.namespace, }); @@ -99,6 +100,7 @@ export class SavedObjectsExporter { return this.processObjects(objects, comparator, { request: options.request, includeReferencesDeep: options.includeReferencesDeep, + includeNamespaces: options.includeNamespaces, excludeExportDetails: options.excludeExportDetails, namespace: options.namespace, }); @@ -111,6 +113,7 @@ export class SavedObjectsExporter { request, excludeExportDetails = false, includeReferencesDeep = false, + includeNamespaces = false, namespace, }: SavedObjectExportBaseOptions ) { @@ -139,9 +142,9 @@ export class SavedObjectsExporter { } // redact attributes that should not be exported - const redactedObjects = exportedObjects.map>( - ({ namespaces, ...object }) => object - ); + const redactedObjects = includeNamespaces + ? exportedObjects + : exportedObjects.map>(({ namespaces, ...object }) => object); const exportDetails: SavedObjectsExportResultDetails = { exportedCount: exportedObjects.length, diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index 4326943bd31ce..7891af6df5b1b 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -15,6 +15,12 @@ export interface SavedObjectExportBaseOptions { request: KibanaRequest; /** flag to also include all related saved objects in the export stream. */ includeReferencesDeep?: boolean; + /** + * Flag to also include namespace information in the export stream. By default, namespace information is not included in exported objects. + * This is only intended to be used internally during copy-to-space operations, and it is not exposed as an option for the external HTTP + * route for exports. + */ + includeNamespaces?: boolean; /** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */ excludeExportDetails?: boolean; /** optional namespace to override the namespace used by the savedObjectsClient. */ diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 45286f158edb1..71e5565ebcbef 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -982,6 +982,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:loud', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'loud', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1046,6 +1047,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:cute', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'cute', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1168,6 +1170,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:hungry', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'hungry', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', @@ -1240,6 +1243,7 @@ describe('DocumentMigrator', () => { id: 'foo-namespace:dog:pretty', type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: 'pretty', targetNamespace: 'foo-namespace', targetType: 'dog', targetId: 'uuidv5', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 4f58397866cfb..f30cfc53018db 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -560,6 +560,7 @@ function convertNamespaceType(doc: SavedObjectUnsanitizedDoc) { id: `${namespace}:${type}:${originId}`, type: LEGACY_URL_ALIAS_TYPE, attributes: { + sourceId: originId, targetNamespace: namespace, targetType: type, targetId: id, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 9f7e32c49ef15..4a1a2b414a642 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -194,6 +194,7 @@ describe('migration v2', () => { id: 'legacy-url-alias:spacex:foo:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newFooId, targetNamespace: 'spacex', targetType: 'foo', @@ -226,6 +227,7 @@ describe('migration v2', () => { id: 'legacy-url-alias:spacex:bar:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newBarId, targetNamespace: 'spacex', targetType: 'bar', diff --git a/src/core/server/saved_objects/object_types/registration.ts b/src/core/server/saved_objects/object_types/registration.ts index 149fc09ce401d..2b5f49123b2cf 100644 --- a/src/core/server/saved_objects/object_types/registration.ts +++ b/src/core/server/saved_objects/object_types/registration.ts @@ -13,10 +13,15 @@ const legacyUrlAliasType: SavedObjectsType = { name: LEGACY_URL_ALIAS_TYPE, namespaceType: 'agnostic', mappings: { - dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields - properties: {}, + dynamic: false, + properties: { + sourceId: { type: 'keyword' }, + targetType: { type: 'keyword' }, + disabled: { type: 'boolean' }, + // other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above) + }, }, - hidden: true, + hidden: false, }; /** diff --git a/src/core/server/saved_objects/object_types/types.ts b/src/core/server/saved_objects/object_types/types.ts index 6fca2ed59906b..9038d1a606067 100644 --- a/src/core/server/saved_objects/object_types/types.ts +++ b/src/core/server/saved_objects/object_types/types.ts @@ -7,13 +7,49 @@ */ /** + * A legacy URL alias is created for an object when it is converted from a single-namespace type to a multi-namespace type. This enables us + * to preserve functionality of existing URLs for objects whose IDs have been changed during the conversion process, by way of the new + * `SavedObjectsClient.resolve()` API. + * + * Legacy URL aliases are only created by the `DocumentMigrator`, and will always have a saved object ID as follows: + * + * ``` + * `${targetNamespace}:${targetType}:${sourceId}` + * ``` + * + * This predictable object ID allows aliases to be easily looked up during the resolve operation, and ensures that exactly one alias will + * exist for a given source per space. + * * @internal */ export interface LegacyUrlAlias { + /** + * The original ID of the object, before it was converted. + */ + sourceId: string; + /** + * The namespace that the object existed in when it was converted. + */ targetNamespace: string; + /** + * The type of the object when it was converted. + */ targetType: string; + /** + * The new ID of the object when it was converted. + */ targetId: string; + /** + * The last time this alias was used with `SavedObjectsClient.resolve()`. + */ lastResolved?: string; + /** + * How many times this alias was used with `SavedObjectsClient.resolve()`. + */ resolveCounter?: number; + /** + * If true, this alias is disabled and it will be ignored in `SavedObjectsClient.resolve()` and + * `SavedObjectsClient.collectMultiNamespaceReferences()`. + */ disabled?: boolean; } diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 4b955032939b3..9c91abcfe79c5 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -76,10 +76,10 @@ export class SavedObjectsSerializer { * @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format. * @param {SavedObjectsRawDocParseOptions} options - Options for parsing the raw document. */ - public rawToSavedObject( + public rawToSavedObject( doc: SavedObjectsRawDoc, options: SavedObjectsRawDocParseOptions = {} - ): SavedObjectSanitizedDoc { + ): SavedObjectSanitizedDoc { this.checkIsRawSavedObject(doc, options); // throws a descriptive error if the document is not a saved object const { namespaceTreatment = 'strict' } = options; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 8a66e6176d1f5..7b4ffcf2dd6cf 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -17,6 +17,14 @@ export type { SavedObjectsClientWrapperOptions, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts new file mode 100644 index 0000000000000..cbd1ac4a8eb8f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as InternalUtils from './internal_utils'; + +export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< + typeof InternalUtils['rawDocExistsInNamespace'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + rawDocExistsInNamespace: mockRawDocExistsInNamespace, + }; +}); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts new file mode 100644 index 0000000000000..00fc039ff005f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.test.ts @@ -0,0 +1,444 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { mockRawDocExistsInNamespace } from './collect_multi_namespace_references.test.mock'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { SavedObjectsSerializer } from '../../serialization'; +import type { + CollectMultiNamespaceReferencesParams, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, +} from './collect_multi_namespace_references'; +import { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import { savedObjectsPointInTimeFinderMock } from './point_in_time_finder.mock'; +import { savedObjectsRepositoryMock } from './repository.mock'; +import { PointInTimeFinder } from './point_in_time_finder'; +import { ISavedObjectsRepository } from './repository'; + +const SPACES = ['default', 'another-space']; +const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; + +const MULTI_NAMESPACE_OBJ_TYPE_1 = 'type-a'; +const MULTI_NAMESPACE_OBJ_TYPE_2 = 'type-b'; +const NON_MULTI_NAMESPACE_OBJ_TYPE = 'type-c'; +const MULTI_NAMESPACE_HIDDEN_OBJ_TYPE = 'type-d'; + +beforeEach(() => { + mockRawDocExistsInNamespace.mockReset(); + mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default +}); + +describe('collectMultiNamespaceReferences', () => { + let client: DeeplyMockedKeys; + let savedObjectsMock: jest.Mocked; + let createPointInTimeFinder: jest.MockedFunction< + CollectMultiNamespaceReferencesParams['createPointInTimeFinder'] + >; + let pointInTimeFinder: DeeplyMockedKeys; + + /** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `collectMultiNamespaceReferences` */ + function setup( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): CollectMultiNamespaceReferencesParams { + const registry = typeRegistryMock.create(); + registry.isMultiNamespace.mockImplementation( + (type) => + [ + MULTI_NAMESPACE_OBJ_TYPE_1, + MULTI_NAMESPACE_OBJ_TYPE_2, + MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, + ].includes(type) // NON_MULTI_NAMESPACE_TYPE is omitted + ); + registry.isShareable.mockImplementation( + (type) => [MULTI_NAMESPACE_OBJ_TYPE_1, MULTI_NAMESPACE_HIDDEN_OBJ_TYPE].includes(type) // MULTI_NAMESPACE_OBJ_TYPE_2 and NON_MULTI_NAMESPACE_TYPE are omitted + ); + client = elasticsearchClientMock.createElasticsearchClient(); + + const serializer = new SavedObjectsSerializer(registry); + savedObjectsMock = savedObjectsRepositoryMock.create(); + savedObjectsMock.find.mockResolvedValue({ + pit_id: 'foo', + saved_objects: [], + // the rest of these fields don't matter but are included for type safety + total: 0, + page: 1, + per_page: 100, + }); + createPointInTimeFinder = jest.fn(); + createPointInTimeFinder.mockImplementation((params) => { + pointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ savedObjectsMock })(params); + return pointInTimeFinder; + }); + return { + registry, + allowedTypes: [ + MULTI_NAMESPACE_OBJ_TYPE_1, + MULTI_NAMESPACE_OBJ_TYPE_2, + NON_MULTI_NAMESPACE_OBJ_TYPE, + ], // MULTI_NAMESPACE_HIDDEN_TYPE is omitted + client, + serializer, + getIndexForType: (type: string) => `index-for-${type}`, + createPointInTimeFinder, + objects, + options, + }; + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockMgetResults( + ...results: Array<{ + found: boolean; + references?: Array<{ type: string; id: string }>; + }> + ) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + docs: results.map((x) => { + const references = + x.references?.map(({ type, id }) => ({ type, id, name: 'ref-name' })) ?? []; + return x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { + namespaces: SPACES, + references, + }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + }; + }), + }) + ); + } + + function mockFindResults(...results: LegacyUrlAlias[]) { + savedObjectsMock.find.mockResolvedValueOnce({ + pit_id: 'foo', + saved_objects: results.map((attributes) => ({ + id: 'doesnt-matter', + type: LEGACY_URL_ALIAS_TYPE, + attributes, + references: [], + score: 0, // doesn't matter + })), + // the rest of these fields don't matter but are included for type safety + total: 0, + page: 1, + per_page: 100, + }); + } + + /** Asserts that mget is called for the given objects */ + function expectMgetArgs( + n: number, + ...objects: SavedObjectsCollectMultiNamespaceReferencesObject[] + ) { + const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); + expect(client.mget).toHaveBeenNthCalledWith(n, { body: { docs } }, expect.anything()); + } + + it('returns an empty array if no object args are passed in', async () => { + const params = setup([]); + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).not.toHaveBeenCalled(); + expect(result.objects).toEqual([]); + }); + + it('excludes args that have unsupported types', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' }; + const params = setup([obj1, obj2, obj3]); + mockMgetResults({ found: true }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // the non-multi-namespace type and the hidden type are excluded + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // even though they are excluded from the cluster call, obj2 and obj3 are included in the results + { ...obj2, spaces: [], inboundReferences: [] }, + { ...obj3, spaces: [], inboundReferences: [] }, + ]); + }); + + it('excludes references that have unsupported types', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: NON_MULTI_NAMESPACE_OBJ_TYPE, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_HIDDEN_OBJ_TYPE, id: 'id-3' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj2, obj3] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); + // obj2 and obj3 are not retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // obj2 and obj3 are excluded from the results + ]); + }); + + it('handles circular references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj1] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // obj1 is retrieved once, and it is not retrieved again in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj1 reflects the inbound reference to itself + ]); + }); + + it('handles a reference graph more than 20 layers deep (circuit-breaker)', async () => { + const type = MULTI_NAMESPACE_OBJ_TYPE_1; + const params = setup([{ type, id: 'id-1' }]); + for (let i = 1; i < 100; i++) { + mockMgetResults({ found: true, references: [{ type, id: `id-${i + 1}` }] }); + } + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + /Exceeded maximum reference graph depth/ + ); + expect(params.client.mget).toHaveBeenCalledTimes(20); + }); + + it('handles multiple inbound references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1, obj2]); + mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [obj3] }); // results for obj1 and obj2 + mockMgetResults({ found: true }); // results for obj3 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2); + expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: SPACES, inboundReferences: [] }, + { + ...obj3, + spaces: SPACES, + inboundReferences: [ + // obj3 reflects both inbound references + { ...obj1, name: 'ref-name' }, + { ...obj2, name: 'ref-name' }, + ], + }, + ]); + }); + + it('handles transitive references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1]); + mockMgetResults({ found: true, references: [obj2] }); // results for obj1 + mockMgetResults({ found: true, references: [obj3] }); // results for obj2 + mockMgetResults({ found: true }); // results for obj3 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(3); + expectMgetArgs(1, obj1); + expectMgetArgs(2, obj2); // obj2 is retrieved in a second cluster call + expectMgetArgs(3, obj3); // obj3 is retrieved in a third cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, // obj2 reflects the inbound reference + { ...obj3, spaces: SPACES, inboundReferences: [{ ...obj2, name: 'ref-name' }] }, // obj3 reflects the inbound reference + ]); + }); + + it('handles missing objects and missing references', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; // found, with missing references to obj4 and obj5 + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; // missing object (found, but doesn't exist in the current space)) + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; // missing object (not found + const obj4 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-4' }; // missing reference (found but doesn't exist in the current space) + const obj5 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-5' }; // missing reference (not found) + const params = setup([obj1, obj2, obj3]); + mockMgetResults({ found: true, references: [obj4, obj5] }, { found: true }, { found: false }); // results for obj1, obj2, and obj3 + mockMgetResults({ found: true }, { found: false }); // results for obj4 and obj5 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj1 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj2 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4 + + const result = await collectMultiNamespaceReferences(params); + expect(params.client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2, obj3); + expectMgetArgs(2, obj4, obj5); + expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3); + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + { ...obj2, spaces: [], inboundReferences: [], isMissing: true }, + { ...obj3, spaces: [], inboundReferences: [], isMissing: true }, + { ...obj4, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true }, + { ...obj5, spaces: [], inboundReferences: [{ ...obj1, name: 'ref-name' }], isMissing: true }, + ]); + }); + + it('handles the purpose="updateObjectsSpaces" option', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_2, id: 'id-3' }; + const params = setup([obj1, obj2], { purpose: 'updateObjectsSpaces' }); + mockMgetResults({ found: true, references: [obj3] }); // results for obj1 + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(1, obj1); // obj2 is excluded + // obj3 is not retrieved in a second cluster call + expect(result.objects).toEqual([ + { ...obj1, spaces: SPACES, inboundReferences: [] }, + // even though it is excluded from the cluster call, obj2 is included in the results + { ...obj2, spaces: [], inboundReferences: [] }, + // obj3 is excluded from the results + ]); + }); + + describe('legacy URL aliases', () => { + it('uses the PointInTimeFinder to search for legacy URL aliases', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const obj3 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-3' }; + const params = setup([obj1, obj2], {}); + mockMgetResults({ found: true, references: [obj3] }, { found: true, references: [] }); // results for obj1 and obj2 + mockMgetResults({ found: true, references: [] }); // results for obj3 + mockFindResults( + // mock search results for four aliases for obj1, and none for obj2 or obj3 + ...[1, 2, 3, 4].map((i) => ({ + sourceId: obj1.id, + targetId: 'doesnt-matter', + targetType: obj1.type, + targetNamespace: `space-${i}`, + })) + ); + + const result = await collectMultiNamespaceReferences(params); + expect(client.mget).toHaveBeenCalledTimes(2); + expectMgetArgs(1, obj1, obj2); + expectMgetArgs(2, obj3); // obj3 is retrieved in a second cluster call + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments; + expect(kueryFilterArgs).toHaveLength(2); + const typeAndIdFilters = kueryFilterArgs[1].arguments; + expect(typeAndIdFilters).toHaveLength(3); + [obj1, obj2, obj3].forEach(({ type, id }, i) => { + const typeAndIdFilter = typeAndIdFilters[i].arguments; + expect(typeAndIdFilter).toEqual([ + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: type }]), + }), + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: id }]), + }), + ]); + }); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + expect(result.objects).toEqual([ + { + ...obj1, + spaces: SPACES, + inboundReferences: [], + spacesWithMatchingAliases: ['space-1', 'space-2', 'space-3', 'space-4'], + }, + { ...obj2, spaces: SPACES, inboundReferences: [] }, + { ...obj3, spaces: SPACES, inboundReferences: [{ ...obj1, name: 'ref-name' }] }, + ]); + }); + + it('does not create a PointInTimeFinder if no objects are passed in', async () => { + const params = setup([]); + + await collectMultiNamespaceReferences(params); + expect(params.createPointInTimeFinder).not.toHaveBeenCalled(); + }); + + it('does not search for objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const obj2 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-2' }; + const params = setup([obj1, obj2]); + mockMgetResults({ found: true }, { found: false }); // results for obj1 and obj2 + + await collectMultiNamespaceReferences(params); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + + const kueryFilterArgs = createPointInTimeFinder.mock.calls[0][0].filter.arguments; + expect(kueryFilterArgs).toHaveLength(2); + const typeAndIdFilters = kueryFilterArgs[1].arguments; + expect(typeAndIdFilters).toHaveLength(1); + const typeAndIdFilter = typeAndIdFilters[0].arguments; + expect(typeAndIdFilter).toEqual([ + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: obj1.type }]), + }), + expect.objectContaining({ + arguments: expect.arrayContaining([{ type: 'literal', value: obj1.id }]), + }), + ]); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + }); + + it('does not search at all if all objects that have an empty spaces array (the object does not exist, or we are not sure)', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: false }); // results for obj1 + + await collectMultiNamespaceReferences(params); + expect(params.createPointInTimeFinder).not.toHaveBeenCalled(); + }); + + it('handles PointInTimeFinder.find errors', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true }); // results for obj1 + savedObjectsMock.find.mockRejectedValue(new Error('Oh no!')); + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + 'Failed to retrieve legacy URL aliases: Oh no!' + ); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); // we still close the point-in-time, even though the search failed + }); + + it('handles PointInTimeFinder.close errors', async () => { + const obj1 = { type: MULTI_NAMESPACE_OBJ_TYPE_1, id: 'id-1' }; + const params = setup([obj1]); + mockMgetResults({ found: true }); // results for obj1 + savedObjectsMock.closePointInTime.mockRejectedValue(new Error('Oh no!')); + + await expect(() => collectMultiNamespaceReferences(params)).rejects.toThrow( + 'Failed to retrieve legacy URL aliases: Oh no!' + ); + expect(createPointInTimeFinder).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.find).toHaveBeenCalledTimes(1); + expect(pointInTimeFinder.close).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts new file mode 100644 index 0000000000000..43923695f6548 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/collect_multi_namespace_references.ts @@ -0,0 +1,310 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// @ts-expect-error no ts +import { esKuery } from '../../es_query'; + +import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsSerializer } from '../../serialization'; +import type { SavedObject, SavedObjectsBaseOptions } from '../../types'; +import { getRootFields } from './included_fields'; +import { getSavedObjectFromSource, rawDocExistsInNamespace } from './internal_utils'; +import type { + ISavedObjectsPointInTimeFinder, + SavedObjectsCreatePointInTimeFinderOptions, +} from './point_in_time_finder'; +import type { RepositoryEsClient } from './repository_es_client'; + +/** + * When we collect an object's outbound references, we will only go a maximum of this many levels deep before we throw an error. + */ +const MAX_REFERENCE_GRAPH_DEPTH = 20; + +/** + * How many aliases to search for per page. This is smaller than the PointInTimeFinder's default of 1000. We specify 100 for the page count + * because this is a relatively unimportant operation, and we want to avoid blocking the Elasticsearch thread pool for longer than + * necessary. + */ +const ALIAS_SEARCH_PER_PAGE = 100; + +/** + * An object to collect references for. It must be a multi-namespace type (in other words, the object type must be registered with the + * `namespaceType: 'multiple'` or `namespaceType: 'multiple-isolated'` option). + * + * Note: if options.purpose is 'updateObjectsSpaces', it must be a shareable type (in other words, the object type must be registered with + * the `namespaceType: 'multiple'`). + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesObject { + id: string; + type: string; +} + +/** + * Options for collecting references. + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesOptions + extends SavedObjectsBaseOptions { + /** Optional purpose used to determine filtering and authorization checks; default is 'collectMultiNamespaceReferences' */ + purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +} + +/** + * A returned input object or one of its references, with additional context. + * + * @public + */ +export interface SavedObjectReferenceWithContext { + /** The type of the referenced object */ + type: string; + /** The ID of the referenced object */ + id: string; + /** The space(s) that the referenced object exists in */ + spaces: string[]; + /** + * References to this object; note that this does not contain _all inbound references everywhere for this object_, it only contains + * inbound references for the scope of this operation + */ + inboundReferences: Array<{ + /** The type of the object that has the inbound reference */ + type: string; + /** The ID of the object that has the inbound reference */ + id: string; + /** The name of the inbound reference */ + name: string; + }>; + /** Whether or not this object or reference is missing */ + isMissing?: boolean; + /** The space(s) that legacy URL aliases matching this type/id exist in */ + spacesWithMatchingAliases?: string[]; +} + +/** + * The response when object references are collected. + * + * @public + */ +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + objects: SavedObjectReferenceWithContext[]; +} + +/** + * Parameters for the collectMultiNamespaceReferences function. + * + * @internal + */ +export interface CollectMultiNamespaceReferencesParams { + registry: ISavedObjectTypeRegistry; + allowedTypes: string[]; + client: RepositoryEsClient; + serializer: SavedObjectsSerializer; + getIndexForType: (type: string) => string; + createPointInTimeFinder: ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions + ) => ISavedObjectsPointInTimeFinder; + objects: SavedObjectsCollectMultiNamespaceReferencesObject[]; + options?: SavedObjectsCollectMultiNamespaceReferencesOptions; +} + +/** + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + */ +export async function collectMultiNamespaceReferences( + params: CollectMultiNamespaceReferencesParams +): Promise { + const { createPointInTimeFinder, objects } = params; + if (!objects.length) { + return { objects: [] }; + } + + const { objectMap, inboundReferencesMap } = await getObjectsAndReferences(params); + const objectsWithContext = Array.from( + inboundReferencesMap.entries() + ).map(([referenceKey, referenceVal]) => { + const inboundReferences = Array.from(referenceVal.entries()).map(([objectKey, name]) => { + const { type, id } = parseKey(objectKey); + return { type, id, name }; + }); + const { type, id } = parseKey(referenceKey); + const object = objectMap.get(referenceKey); + const spaces = object?.namespaces ?? []; + return { type, id, spaces, inboundReferences, ...(object === null && { isMissing: true }) }; + }); + + const aliasesMap = await checkLegacyUrlAliases(createPointInTimeFinder, objectsWithContext); + const results = objectsWithContext.map((obj) => { + const key = getKey(obj); + const val = aliasesMap.get(key); + const spacesWithMatchingAliases = val && Array.from(val); + return { ...obj, spacesWithMatchingAliases }; + }); + + return { + objects: results, + }; +} + +/** + * Recursively fetches objects and their references, returning a map of the retrieved objects and a map of all inbound references. + */ +async function getObjectsAndReferences({ + registry, + allowedTypes, + client, + serializer, + getIndexForType, + objects, + options = {}, +}: CollectMultiNamespaceReferencesParams) { + const { namespace, purpose } = options; + const inboundReferencesMap = objects.reduce( + // Add the input objects to the references map so they are returned with the results, even if they have no inbound references + (acc, cur) => acc.set(getKey(cur), new Map()), + new Map>() + ); + const objectMap = new Map(); + + const rootFields = getRootFields(); + const makeBulkGetDocs = (objectsToGet: SavedObjectsCollectMultiNamespaceReferencesObject[]) => + objectsToGet.map(({ type, id }) => ({ + _id: serializer.generateRawId(undefined, type, id), + _index: getIndexForType(type), + _source: rootFields, // Optimized to only retrieve root fields (ignoring type-specific fields) + })); + const validObjectTypesFilter = ({ type }: SavedObjectsCollectMultiNamespaceReferencesObject) => + allowedTypes.includes(type) && + (purpose === 'updateObjectsSpaces' + ? registry.isShareable(type) + : registry.isMultiNamespace(type)); + + let bulkGetObjects = objects.filter(validObjectTypesFilter); + let count = 0; // this is a circuit-breaker to ensure we don't hog too many resources; we should never have an object graph this deep + while (bulkGetObjects.length) { + if (count >= MAX_REFERENCE_GRAPH_DEPTH) { + throw new Error( + `Exceeded maximum reference graph depth of ${MAX_REFERENCE_GRAPH_DEPTH} objects!` + ); + } + const bulkGetResponse = await client.mget( + { body: { docs: makeBulkGetDocs(bulkGetObjects) } }, + { ignore: [404] } + ); + const newObjectsToGet = new Set(); + for (let i = 0; i < bulkGetObjects.length; i++) { + // For every element in bulkGetObjects, there should be a matching element in bulkGetResponse.body.docs + const { type, id } = bulkGetObjects[i]; + const objectKey = getKey({ type, id }); + const doc = bulkGetResponse.body.docs[i]; + // @ts-expect-error MultiGetHit._source is optional + if (!doc.found || !rawDocExistsInNamespace(registry, doc, namespace)) { + objectMap.set(objectKey, null); + continue; + } + // @ts-expect-error MultiGetHit._source is optional + const object = getSavedObjectFromSource(registry, type, id, doc); + objectMap.set(objectKey, object); + for (const reference of object.references) { + if (!validObjectTypesFilter(reference)) { + continue; + } + const referenceKey = getKey(reference); + const referenceVal = inboundReferencesMap.get(referenceKey) ?? new Map(); + if (!referenceVal.has(objectKey)) { + inboundReferencesMap.set(referenceKey, referenceVal.set(objectKey, reference.name)); + } + if (!objectMap.has(referenceKey)) { + newObjectsToGet.add(referenceKey); + } + } + } + bulkGetObjects = Array.from(newObjectsToGet).map((key) => parseKey(key)); + count++; + } + + return { objectMap, inboundReferencesMap }; +} + +/** + * Fetches all legacy URL aliases that match the given objects, returning a map of the matching aliases and what space(s) they exist in. + */ +async function checkLegacyUrlAliases( + createPointInTimeFinder: ( + findOptions: SavedObjectsCreatePointInTimeFinderOptions + ) => ISavedObjectsPointInTimeFinder, + objects: SavedObjectReferenceWithContext[] +) { + const filteredObjects = objects.filter(({ spaces }) => spaces.length !== 0); + if (!filteredObjects.length) { + return new Map>(); + } + const filter = createAliasKueryFilter(filteredObjects); + const finder = createPointInTimeFinder({ + type: LEGACY_URL_ALIAS_TYPE, + perPage: ALIAS_SEARCH_PER_PAGE, + filter, + }); + const aliasesMap = new Map>(); + let error: Error | undefined; + try { + for await (const { saved_objects: savedObjects } of finder.find()) { + for (const alias of savedObjects) { + const { sourceId, targetType, targetNamespace } = alias.attributes; + const key = getKey({ type: targetType, id: sourceId }); + const val = aliasesMap.get(key) ?? new Set(); + val.add(targetNamespace); + aliasesMap.set(key, val); + } + } + } catch (e) { + error = e; + } + + try { + await finder.close(); + } catch (e) { + if (!error) { + error = e; + } + } + + if (error) { + throw new Error(`Failed to retrieve legacy URL aliases: ${error.message}`); + } + return aliasesMap; +} + +function createAliasKueryFilter(objects: SavedObjectReferenceWithContext[]) { + const { buildNode } = esKuery.nodeTypes.function; + const kueryNodes = objects.reduce((acc, { type, id }) => { + const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.targetType`, type); + const match2 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.sourceId`, id); + acc.push(buildNode('and', [match1, match2])); + return acc; + }, []); + return buildNode('and', [ + buildNode('not', buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.attributes.disabled`, true)), // ignore aliases that have been disabled + buildNode('or', kueryNodes), + ]); +} + +/** Takes an object with a `type` and `id` field and returns a key string */ +function getKey({ type, id }: { type: string; id: string }) { + return `${type}:${id}`; +} + +/** Parses a 'type:id' key string and returns an object with a `type` field and an `id` field */ +function parseKey(key: string) { + const type = key.slice(0, key.indexOf(':')); + const id = key.slice(type.length + 1); + return { type, id }; +} diff --git a/src/core/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts index 334cda91129f3..51c431b1c6b3b 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.test.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.test.ts @@ -6,125 +6,63 @@ * Side Public License, v 1. */ -import { includedFields } from './included_fields'; +import { getRootFields, includedFields } from './included_fields'; -const BASE_FIELD_COUNT = 10; +describe('getRootFields', () => { + it('returns copy of root fields', () => { + const fields = getRootFields(); + expect(fields).toMatchInlineSnapshot(` + Array [ + "namespace", + "namespaces", + "type", + "references", + "migrationVersion", + "coreMigrationVersion", + "updated_at", + "originId", + ] + `); + }); +}); describe('includedFields', () => { + const rootFields = getRootFields(); + it('returns undefined if fields are not provided', () => { expect(includedFields()).toBe(undefined); }); - it('accepts type string', () => { + it('accepts type and field as string', () => { const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('type'); + expect(fields).toEqual(['config.foo', ...rootFields, 'foo']); }); - it('accepts type as string array', () => { + it('accepts type as array and field as string', () => { const fields = includedFields(['config', 'secret'], 'foo'); - expect(fields).toMatchInlineSnapshot(` -Array [ - "config.foo", - "secret.foo", - "namespace", - "namespaces", - "type", - "references", - "migrationVersion", - "coreMigrationVersion", - "updated_at", - "originId", - "foo", -] -`); - }); - - it('accepts field as string', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('config.foo'); + expect(fields).toEqual(['config.foo', 'secret.foo', ...rootFields, 'foo']); }); - it('accepts fields as an array', () => { + it('accepts type as string and field as array', () => { const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); - expect(fields).toContain('config.foo'); - expect(fields).toContain('config.bar'); + expect(fields).toEqual(['config.foo', 'config.bar', ...rootFields, 'foo', 'bar']); }); - it('accepts type as string array and fields as string array', () => { + it('accepts type as array and field as array', () => { const fields = includedFields(['config', 'secret'], ['foo', 'bar']); - expect(fields).toMatchInlineSnapshot(` -Array [ - "config.foo", - "config.bar", - "secret.foo", - "secret.bar", - "namespace", - "namespaces", - "type", - "references", - "migrationVersion", - "coreMigrationVersion", - "updated_at", - "originId", - "foo", - "bar", -] -`); - }); - - it('includes namespace', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('namespace'); - }); - - it('includes namespaces', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('namespaces'); - }); - - it('includes references', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('references'); - }); - - it('includes migrationVersion', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('migrationVersion'); - }); - - it('includes updated_at', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('updated_at'); - }); - - it('includes originId', () => { - const fields = includedFields('config', 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('originId'); + expect(fields).toEqual([ + 'config.foo', + 'config.bar', + 'secret.foo', + 'secret.bar', + ...rootFields, + 'foo', + 'bar', + ]); }); it('uses wildcard when type is not provided', () => { const fields = includedFields(undefined, 'foo'); - expect(fields).toHaveLength(BASE_FIELD_COUNT); - expect(fields).toContain('*.foo'); - }); - - describe('v5 compatibility', () => { - it('includes legacy field path', () => { - const fields = includedFields('config', ['foo', 'bar']); - - expect(fields).toHaveLength(BASE_FIELD_COUNT + 2); - expect(fields).toContain('foo'); - expect(fields).toContain('bar'); - }); + expect(fields).toEqual(['*.foo', ...rootFields, 'foo']); }); }); diff --git a/src/core/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts index cef83f103ec53..9613d8f6a4a41 100644 --- a/src/core/server/saved_objects/service/lib/included_fields.ts +++ b/src/core/server/saved_objects/service/lib/included_fields.ts @@ -9,6 +9,22 @@ function toArray(value: string | string[]): string[] { return typeof value === 'string' ? [value] : value; } + +const ROOT_FIELDS = [ + 'namespace', + 'namespaces', + 'type', + 'references', + 'migrationVersion', + 'coreMigrationVersion', + 'updated_at', + 'originId', +]; + +export function getRootFields() { + return [...ROOT_FIELDS]; +} + /** * Provides an array of paths for ES source filtering */ @@ -28,13 +44,6 @@ export function includedFields( .reduce((acc: string[], t) => { return [...acc, ...sourceFields.map((f) => `${t}.${f}`)]; }, []) - .concat('namespace') - .concat('namespaces') - .concat('type') - .concat('references') - .concat('migrationVersion') - .concat('coreMigrationVersion') - .concat('updated_at') - .concat('originId') + .concat(ROOT_FIELDS) .concat(fields); // v5 compatibility } diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index 09bce81b14c39..661d04b8a0b2a 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -27,3 +27,17 @@ export type { export { SavedObjectsErrorHelpers } from './errors'; export { SavedObjectsUtils } from './utils'; + +export type { + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from './collect_multi_namespace_references'; + +export type { + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, + SavedObjectsUpdateObjectsSpacesResponse, + SavedObjectsUpdateObjectsSpacesResponseObject, +} from './update_objects_spaces'; diff --git a/src/core/server/saved_objects/service/lib/internal_utils.test.ts b/src/core/server/saved_objects/service/lib/internal_utils.test.ts new file mode 100644 index 0000000000000..d1fd067990f07 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/internal_utils.test.ts @@ -0,0 +1,243 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import type { SavedObjectsRawDoc } from '../../serialization'; +import { encodeHitVersion } from '../../version'; +import { + getBulkOperationError, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from './internal_utils'; +import { ALL_NAMESPACES_STRING } from './utils'; + +describe('#getBulkOperationError', () => { + const type = 'obj-type'; + const id = 'obj-id'; + + it('returns index not found error', () => { + const rawResponse = { + status: 404, + error: { type: 'index_not_found_exception', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Internal Server Error', + message: 'An internal server error occurred', // TODO: this error payload is not very helpful to consumers, can we change it? + statusCode: 500, + }); + }); + + it('returns generic not found error', () => { + const rawResponse = { + status: 404, + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Not Found', + message: `Saved object [${type}/${id}] not found`, + statusCode: 404, + }); + }); + + it('returns conflict error', () => { + const rawResponse = { + status: 409, + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Conflict', + message: `Saved object [${type}/${id}] conflict`, + statusCode: 409, + }); + }); + + it('returns an unexpected result error', () => { + const rawResponse = { + status: 123, // any status + error: { type: 'anything', reason: 'some-reason', index: 'some-index' }, + }; + + const result = getBulkOperationError(type, id, rawResponse); + expect(result).toEqual({ + error: 'Internal Server Error', + message: `Unexpected bulk response [${rawResponse.status}] ${rawResponse.error.type}: ${rawResponse.error.reason}`, + statusCode: 500, + }); + }); +}); + +describe('#getSavedObjectFromSource', () => { + const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type'; + const NON_NAMESPACE_AGNOSTIC_TYPE = 'other-type'; + + const registry = typeRegistryMock.create(); + registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE); + + const id = 'obj-id'; + const _seq_no = 1; + const _primary_term = 1; + const attributes = { foo: 'bar' }; + const references = [{ type: 'ref-type', id: 'ref-id', name: 'ref-name' }]; + const migrationVersion = { foo: 'migrationVersion' }; + const coreMigrationVersion = 'coreMigrationVersion'; + const originId = 'originId'; + // eslint-disable-next-line @typescript-eslint/naming-convention + const updated_at = 'updatedAt'; + + function createRawDoc( + type: string, + namespaceAttrs?: { namespace?: string; namespaces?: string[] } + ) { + return { + // other fields exist on the raw document, but they are not relevant to these test cases + _seq_no, + _primary_term, + _source: { + type, + [type]: attributes, + references, + migrationVersion, + coreMigrationVersion, + originId, + updated_at, + ...namespaceAttrs, + }, + }; + } + + it('returns object with expected attributes', () => { + const type = 'any-type'; + const doc = createRawDoc(type); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual({ + attributes, + coreMigrationVersion, + id, + migrationVersion, + namespaces: expect.anything(), // see specific test cases below + originId, + references, + type, + updated_at, + version: encodeHitVersion(doc), + }); + }); + + it('returns object with empty namespaces array when type is namespace-agnostic', () => { + const type = NAMESPACE_AGNOSTIC_TYPE; + const doc = createRawDoc(type); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual(expect.objectContaining({ namespaces: [] })); + }); + + it('returns object with namespaces when type is not namespace-agnostic and namespaces array is defined', () => { + const type = NON_NAMESPACE_AGNOSTIC_TYPE; + const namespaces = ['foo-ns', 'bar-ns']; + const doc = createRawDoc(type, { namespaces }); + + const result = getSavedObjectFromSource(registry, type, id, doc); + expect(result).toEqual(expect.objectContaining({ namespaces })); + }); + + it('derives namespaces from namespace attribute when type is not namespace-agnostic and namespaces array is not defined', () => { + // Deriving namespaces from the namespace attribute is an implementation detail of SavedObjectsUtils.namespaceIdToString(). + // However, these test cases assertions are written out anyway for clarity. + const type = NON_NAMESPACE_AGNOSTIC_TYPE; + const doc1 = createRawDoc(type, { namespace: undefined }); + const doc2 = createRawDoc(type, { namespace: 'foo-ns' }); + + const result1 = getSavedObjectFromSource(registry, type, id, doc1); + const result2 = getSavedObjectFromSource(registry, type, id, doc2); + expect(result1).toEqual(expect.objectContaining({ namespaces: ['default'] })); + expect(result2).toEqual(expect.objectContaining({ namespaces: ['foo-ns'] })); + }); +}); + +describe('#rawDocExistsInNamespace', () => { + const SINGLE_NAMESPACE_TYPE = 'single-type'; + const MULTI_NAMESPACE_TYPE = 'multi-type'; + const NAMESPACE_AGNOSTIC_TYPE = 'agnostic-type'; + + const registry = typeRegistryMock.create(); + registry.isSingleNamespace.mockImplementation((type) => type === SINGLE_NAMESPACE_TYPE); + registry.isMultiNamespace.mockImplementation((type) => type === MULTI_NAMESPACE_TYPE); + registry.isNamespaceAgnostic.mockImplementation((type) => type === NAMESPACE_AGNOSTIC_TYPE); + + function createRawDoc( + type: string, + namespaceAttrs: { namespace?: string; namespaces?: string[] } + ) { + return { + // other fields exist on the raw document, but they are not relevant to these test cases + _source: { + type, + ...namespaceAttrs, + }, + } as SavedObjectsRawDoc; + } + + describe('single-namespace type', () => { + it('returns true regardless of namespace or namespaces fields', () => { + // Technically, a single-namespace type does not exist in a space unless it has a namespace prefix in its raw ID and a matching + // 'namespace' field. However, historically we have not enforced the latter, we have just relied on searching for and deserializing + // documents with the correct namespace prefix. We may revisit this in the future. + const doc1 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespace: 'some-space' }); // the namespace field is ignored + const doc2 = createRawDoc(SINGLE_NAMESPACE_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored + expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true); + }); + }); + + describe('multi-namespace type', () => { + const docInDefaultSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['default'] }); + const docInSomeSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: ['some-space'] }); + const docInAllSpaces = createRawDoc(MULTI_NAMESPACE_TYPE, { + namespaces: [ALL_NAMESPACES_STRING], + }); + const docInNoSpace = createRawDoc(MULTI_NAMESPACE_TYPE, { namespaces: [] }); + + it('returns true when the document namespaces matches', () => { + expect(rawDocExistsInNamespace(registry, docInDefaultSpace, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, docInAllSpaces, 'other-space')).toBe(true); + }); + + it('returns false when the document namespace does not match', () => { + expect(rawDocExistsInNamespace(registry, docInDefaultSpace, 'other-space')).toBe(false); + expect(rawDocExistsInNamespace(registry, docInSomeSpace, 'other-space')).toBe(false); + expect(rawDocExistsInNamespace(registry, docInNoSpace, 'other-space')).toBe(false); + }); + }); + + describe('namespace-agnostic type', () => { + it('returns true regardless of namespace or namespaces fields', () => { + const doc1 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespace: 'some-space' }); // the namespace field is ignored + const doc2 = createRawDoc(NAMESPACE_AGNOSTIC_TYPE, { namespaces: ['some-space'] }); // the namespaces field is ignored + expect(rawDocExistsInNamespace(registry, doc1, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc1, 'other-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, undefined)).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'some-space')).toBe(true); + expect(rawDocExistsInNamespace(registry, doc2, 'other-space')).toBe(true); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/internal_utils.ts b/src/core/server/saved_objects/service/lib/internal_utils.ts new file mode 100644 index 0000000000000..feaaea15649c7 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/internal_utils.ts @@ -0,0 +1,143 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Payload } from '@hapi/boom'; +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsRawDoc, SavedObjectsRawDocSource } from '../../serialization'; +import type { SavedObject } from '../../types'; +import { decodeRequestVersion, encodeHitVersion } from '../../version'; +import { SavedObjectsErrorHelpers } from './errors'; +import { ALL_NAMESPACES_STRING, SavedObjectsUtils } from './utils'; + +/** + * Checks the raw response of a bulk operation and returns an error if necessary. + * + * @param type + * @param id + * @param rawResponse + * + * @internal + */ +export function getBulkOperationError( + type: string, + id: string, + rawResponse: { + status: number; + error?: { type: string; reason: string; index: string }; + // Other fields are present on a bulk operation result but they are irrelevant for this function + } +): Payload | undefined { + const { status, error } = rawResponse; + if (error) { + switch (status) { + case 404: + return error.type === 'index_not_found_exception' + ? SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index).output.payload + : SavedObjectsErrorHelpers.createGenericNotFoundError(type, id).output.payload; + case 409: + return SavedObjectsErrorHelpers.createConflictError(type, id).output.payload; + default: + return { + error: 'Internal Server Error', + message: `Unexpected bulk response [${status}] ${error.type}: ${error.reason}`, + statusCode: 500, + }; + } + } +} + +/** + * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. + * + * @param version Optional version specified by the consumer. + * @param document Optional existing document that was obtained in a preflight operation. + * + * @internal + */ +export function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { + if (version) { + return decodeRequestVersion(version); + } else if (document) { + return { + if_seq_no: document._seq_no, + if_primary_term: document._primary_term, + }; + } + return {}; +} + +/** + * Gets a saved object from a raw ES document. + * + * @param registry + * @param type + * @param id + * @param doc + * + * @internal + */ +export function getSavedObjectFromSource( + registry: ISavedObjectTypeRegistry, + type: string, + id: string, + doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } +): SavedObject { + const { originId, updated_at: updatedAt } = doc._source; + + let namespaces: string[] = []; + if (!registry.isNamespaceAgnostic(type)) { + namespaces = doc._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(doc._source.namespace), + ]; + } + + return { + id, + type, + namespaces, + ...(originId && { originId }), + ...(updatedAt && { updated_at: updatedAt }), + version: encodeHitVersion(doc), + attributes: doc._source[type], + references: doc._source.references || [], + migrationVersion: doc._source.migrationVersion, + coreMigrationVersion: doc._source.coreMigrationVersion, + }; +} + +/** + * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as + * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the + * document's `namespaces` value includes the string representation of the given namespace. + * + * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID + * format mentioned above do not apply. + * + * @param registry + * @param raw + * @param namespace + */ +export function rawDocExistsInNamespace( + registry: ISavedObjectTypeRegistry, + raw: SavedObjectsRawDoc, + namespace: string | undefined +) { + const rawDocType = raw._source.type; + + // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees + // of the document ID format and don't need to check this + if (!registry.isMultiNamespace(rawDocType)) { + return true; + } + + const namespaces = raw._source.namespaces; + const existsInNamespace = + namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) || + namespaces?.includes(ALL_NAMESPACES_STRING); + return existsInNamespace ?? false; +} diff --git a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts index 9a8dcceafebb2..f0ed943c585e5 100644 --- a/src/core/server/saved_objects/service/lib/point_in_time_finder.ts +++ b/src/core/server/saved_objects/service/lib/point_in_time_finder.ts @@ -39,14 +39,14 @@ export interface PointInTimeFinderDependencies } /** @public */ -export interface ISavedObjectsPointInTimeFinder { +export interface ISavedObjectsPointInTimeFinder { /** * An async generator which wraps calls to `savedObjectsClient.find` and * iterates over multiple pages of results using `_pit` and `search_after`. * This will open a new Point-In-Time (PIT), and continue paging until a set * of results is received that's smaller than the designated `perPage` size. */ - find: () => AsyncGenerator; + find: () => AsyncGenerator>; /** * Closes the Point-In-Time associated with this finder instance. * @@ -63,7 +63,8 @@ export interface ISavedObjectsPointInTimeFinder { /** * @internal */ -export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { +export class PointInTimeFinder + implements ISavedObjectsPointInTimeFinder { readonly #log: Logger; readonly #client: PointInTimeFinderClient; readonly #findOptions: SavedObjectsFindOptions; @@ -162,7 +163,7 @@ export class PointInTimeFinder implements ISavedObjectsPointInTimeFinder { searchAfter?: estypes.Id[]; }) { try { - return await this.#client.find({ + return await this.#client.find({ // Sort fields are required to use searchAfter, so we set some defaults here sortField: 'updated_at', sortOrder: 'desc', diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index a2092e0571808..0e1426a58f8ae 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -24,11 +24,11 @@ const create = () => { openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), deleteByNamespace: jest.fn(), incrementCounter: jest.fn(), removeReferencesTo: jest.fn(), + collectMultiNamespaceReferences: jest.fn(), + updateObjectsSpaces: jest.fn(), }; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 33754d0ad9661..22c40a547f419 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { pointInTimeFinderMock } from './repository.test.mock'; +import { + pointInTimeFinderMock, + mockCollectMultiNamespaceReferences, + mockGetBulkOperationError, + mockUpdateObjectsSpaces, +} from './repository.test.mock'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; @@ -67,9 +72,9 @@ describe('SavedObjectsRepository', () => { * This type has namespaceType: 'multiple-isolated'. * * That means that the object is serialized with a globally unique ID across namespaces. It also means that the object is NOT shareable - * across namespaces. This distinction only matters when using the `addToNamespaces` and `deleteFromNamespaces` APIs, or when using the - * `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what namespaces an object - * exists in. + * across namespaces. This distinction only matters when using the `collectMultiNamespaceReferences` or `updateObjectsSpaces` APIs, or + * when using the `initialNamespaces` argument with the `create` and `bulkCreate` APIs. Those allow you to define or change what + * namespaces an object exists in. * * In a nutshell, this type is more restrictive than `MULTI_NAMESPACE_TYPE`, so we use `MULTI_NAMESPACE_ISOLATED_TYPE` for any test cases * where `MULTI_NAMESPACE_TYPE` would also satisfy the test case. @@ -295,164 +300,6 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'search_0', type: 'search', id: '123' }], }); - describe('#addToNamespaces', () => { - const id = 'some-id'; - const type = MULTI_NAMESPACE_TYPE; - const currentNs1 = 'default'; - const currentNs2 = 'foo-namespace'; - const newNs1 = 'bar-namespace'; - const newNs2 = 'baz-namespace'; - - const mockGetResponse = (type, id) => { - // mock a document that exists in two namespaces - const mockResponse = getMockGetResponse({ type, id }); - mockResponse._source.namespaces = [currentNs1, currentNs2]; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - }; - - const addToNamespacesSuccess = async (type, id, namespaces, options) => { - mockGetResponse(type, id); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }) - ); - const result = await savedObjectsRepository.addToNamespaces(type, id, namespaces, options); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - return result; - }; - - describe('client calls', () => { - it(`should use ES get action then update action`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - }); - - it(`defaults to the version of the existing document`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining(versionProperties), - expect.anything() - ); - }); - - it(`accepts version`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2], { - version: encodeHitVersion({ _seq_no: 100, _primary_term: 200 }), - }); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ if_seq_no: 100, if_primary_term: 200 }), - expect.anything() - ); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ refresh: 'wait_for' }), - expect.anything() - ); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, namespaces, options) => { - await expect( - savedObjectsRepository.addToNamespaces(type, id, namespaces, options) - ).rejects.toThrowError(createGenericNotFoundError(type, id)); - }; - const expectBadRequestError = async (type, id, namespaces, message) => { - await expect( - savedObjectsRepository.addToNamespaces(type, id, namespaces) - ).rejects.toThrowError(createBadRequestError(message)); - }; - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id, [newNs1, newNs2]); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id, [newNs1, newNs2]); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is not shareable`, async () => { - const test = async (type) => { - const message = `${type} doesn't support multiple namespaces`; - await expectBadRequestError(type, id, [newNs1, newNs2], message); - expect(client.update).not.toHaveBeenCalled(); - }; - await test('index-pattern'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); - }); - - it(`throws when namespaces is an empty array`, async () => { - const test = async (namespaces) => { - const message = 'namespaces must be a non-empty array of strings'; - await expectBadRequestError(type, id, namespaces, message); - expect(client.update).not.toHaveBeenCalled(); - }; - await test([]); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id); - await expectNotFoundError(type, id, [newNs1, newNs2], { - namespace: 'some-other-namespace', - }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id); - client.update.mockResolvedValue( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [newNs1, newNs2]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns all existing and new namespaces on success`, async () => { - const result = await addToNamespacesSuccess(type, id, [newNs1, newNs2]); - expect(result).toEqual({ namespaces: [currentNs1, currentNs2, newNs1, newNs2] }); - }); - - it(`succeeds when adding existing namespaces`, async () => { - const result = await addToNamespacesSuccess(type, id, [currentNs1]); - expect(result).toEqual({ namespaces: [currentNs1, currentNs2] }); - }); - }); - }); - describe('#bulkCreate', () => { const obj1 = { type: 'config', @@ -757,6 +604,10 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + const obj3 = { type: 'dashboard', id: 'three', @@ -764,11 +615,13 @@ describe('SavedObjectsRepository', () => { references: [{ name: 'ref_0', type: 'test', id: '2' }], }; - const bulkCreateError = async (obj, esError, expectedError) => { + const bulkCreateError = async (obj, isBulkError, expectedErrorResult) => { let response; - if (esError) { + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); response = getMockBulkCreateResponse([obj1, obj, obj2]); - response.items[1].create = { error: esError }; } else { response = getMockBulkCreateResponse([obj1, obj2]); } @@ -779,14 +632,14 @@ describe('SavedObjectsRepository', () => { const objects = [obj1, obj, obj2]; const result = await savedObjectsRepository.bulkCreate(objects); expect(client.bulk).toHaveBeenCalled(); - const objCall = esError ? expectObjArgs(obj) : []; + const objCall = isBulkError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), expect.anything() ); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], }); }; @@ -878,25 +731,9 @@ describe('SavedObjectsRepository', () => { }); }); - it(`returns error when there is a version conflict (bulk)`, async () => { - const esError = { type: 'version_conflict_engine_exception' }; - await bulkCreateError(obj3, esError, expectErrorConflict(obj3)); - }); - - it(`returns error when document is missing`, async () => { - const esError = { type: 'document_missing_exception' }; - await bulkCreateError(obj3, esError, expectErrorNotFound(obj3)); - }); - - it(`returns error reason for other errors`, async () => { - const esError = { reason: 'some_other_error' }; - await bulkCreateError(obj3, esError, expectErrorResult(obj3, { message: esError.reason })); - }); - - it(`returns error string for other errors if no reason is defined`, async () => { - const esError = { foo: 'some_other_error' }; - const expectedError = expectErrorResult(obj3, { message: JSON.stringify(esError) }); - await bulkCreateError(obj3, esError, expectedError); + it(`returns bulk error`, async () => { + const expectedErrorResult = { type: obj3.type, id: obj3.id, error: 'Oh no, a bulk error!' }; + await bulkCreateError(obj3, true, expectedErrorResult); }); }); @@ -1530,16 +1367,22 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { + afterEach(() => { + mockGetBulkOperationError.mockReset(); + }); + const obj = { type: 'dashboard', id: 'three', }; - const bulkUpdateError = async (obj, esError, expectedError) => { + const bulkUpdateError = async (obj, isBulkError, expectedErrorResult) => { const objects = [obj1, obj, obj2]; const mockResponse = getMockBulkUpdateResponse(objects); - if (esError) { - mockResponse.items[1].update = { error: esError }; + if (isBulkError) { + // mock the bulk error for only the second object + mockGetBulkOperationError.mockReturnValueOnce(undefined); + mockGetBulkOperationError.mockReturnValueOnce(expectedErrorResult.error); } client.bulk.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) @@ -1547,14 +1390,14 @@ describe('SavedObjectsRepository', () => { const result = await savedObjectsRepository.bulkUpdate(objects); expect(client.bulk).toHaveBeenCalled(); - const objCall = esError ? expectObjArgs(obj) : []; + const objCall = isBulkError ? expectObjArgs(obj) : []; const body = [...expectObjArgs(obj1), ...objCall, ...expectObjArgs(obj2)]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), expect.anything() ); expect(result).toEqual({ - saved_objects: [expectSuccess(obj1), expectedError, expectSuccess(obj2)], + saved_objects: [expectSuccess(obj1), expectedErrorResult, expectSuccess(obj2)], }); }; @@ -1592,19 +1435,19 @@ describe('SavedObjectsRepository', () => { it(`returns error when type is invalid`, async () => { const _obj = { ...obj, type: 'unknownType' }; - await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); }); it(`returns error when type is hidden`, async () => { const _obj = { ...obj, type: HIDDEN_TYPE }; - await bulkUpdateError(_obj, undefined, expectErrorNotFound(_obj)); + await bulkUpdateError(_obj, false, expectErrorNotFound(_obj)); }); it(`returns error when object namespace is '*'`, async () => { const _obj = { ...obj, namespace: '*' }; await bulkUpdateError( _obj, - undefined, + false, expectErrorResult(obj, createBadRequestError('"namespace" cannot be "*"')) ); }); @@ -1627,25 +1470,9 @@ describe('SavedObjectsRepository', () => { await bulkUpdateMultiError([obj1, _obj, obj2], { namespace }, mgetResponse); }); - it(`returns error when there is a version conflict (bulk)`, async () => { - const esError = { type: 'version_conflict_engine_exception' }; - await bulkUpdateError(obj, esError, expectErrorConflict(obj)); - }); - - it(`returns error when document is missing (bulk)`, async () => { - const esError = { type: 'document_missing_exception' }; - await bulkUpdateError(obj, esError, expectErrorNotFound(obj)); - }); - - it(`returns error reason for other errors (bulk)`, async () => { - const esError = { reason: 'some_other_error' }; - await bulkUpdateError(obj, esError, expectErrorResult(obj, { message: esError.reason })); - }); - - it(`returns error string for other errors if no reason is defined (bulk)`, async () => { - const esError = { foo: 'some_other_error' }; - const expectedError = expectErrorResult(obj, { message: JSON.stringify(esError) }); - await bulkUpdateError(obj, esError, expectedError); + it(`returns bulk error`, async () => { + const expectedErrorResult = { type: obj.type, id: obj.id, error: 'Oh no, a bulk error!' }; + await bulkUpdateError(obj, true, expectedErrorResult); }); }); @@ -3898,352 +3725,6 @@ describe('SavedObjectsRepository', () => { }); }); - describe('#deleteFromNamespaces', () => { - const id = 'some-id'; - const type = MULTI_NAMESPACE_TYPE; - const namespace1 = 'default'; - const namespace2 = 'foo-namespace'; - const namespace3 = 'bar-namespace'; - - const mockGetResponse = (type, id, namespaces) => { - // mock a document that exists in two namespaces - const mockResponse = getMockGetResponse({ type, id }); - mockResponse._source.namespaces = namespaces; - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockResponse) - ); - }; - - const deleteFromNamespacesSuccess = async ( - type, - id, - namespaces, - currentNamespaces, - options - ) => { - mockGetResponse(type, id, currentNamespaces); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'deleted', - }) - ); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - }) - ); - - return await savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options); - }; - - describe('client calls', () => { - describe('delete action', () => { - const deleteFromNamespacesSuccessDelete = async (expectFn, options, _type = type) => { - const test = async (namespaces) => { - await deleteFromNamespacesSuccess(_type, id, namespaces, namespaces, options); - expectFn(); - client.delete.mockClear(); - client.get.mockClear(); - }; - await test([namespace1]); - await test([namespace1, namespace2]); - }; - - it(`should use ES get action then delete action if the object has no namespaces remaining`, async () => { - const expectFn = () => { - expect(client.delete).toHaveBeenCalledTimes(1); - expect(client.get).toHaveBeenCalledTimes(1); - }; - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`formats the ES requests`, async () => { - const expectFn = () => { - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - ...versionProperties, - }), - expect.anything() - ); - }; - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - await deleteFromNamespacesSuccessDelete(() => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ) - ); - }); - - it(`should use default index`, async () => { - const expectFn = () => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test' }), - expect.anything() - ); - await deleteFromNamespacesSuccessDelete(expectFn); - }); - - it(`should use custom index`, async () => { - const expectFn = () => - expect(client.delete).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom' }), - expect.anything() - ); - await deleteFromNamespacesSuccessDelete(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); - }); - }); - - describe('update action', () => { - const deleteFromNamespacesSuccessUpdate = async (expectFn, options, _type = type) => { - const test = async (remaining) => { - const currentNamespaces = [namespace1].concat(remaining); - await deleteFromNamespacesSuccess(_type, id, [namespace1], currentNamespaces, options); - expectFn(); - client.get.mockClear(); - client.update.mockClear(); - }; - await test([namespace2]); - await test([namespace2, namespace3]); - }; - - it(`should use ES get action then update action if the object has one or more namespaces remaining`, async () => { - const expectFn = () => { - expect(client.update).toHaveBeenCalledTimes(1); - expect(client.get).toHaveBeenCalledTimes(1); - }; - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`formats the ES requests`, async () => { - let ctr = 0; - const expectFn = () => { - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - }), - expect.anything() - ); - const namespaces = ctr++ === 0 ? [namespace2] : [namespace2, namespace3]; - const versionProperties = { - if_seq_no: mockVersionProps._seq_no, - if_primary_term: mockVersionProps._primary_term, - }; - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: `${type}:${id}`, - ...versionProperties, - body: { doc: { ...mockTimestampFields, namespaces } }, - }), - expect.anything() - ); - }; - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`defaults to a refresh setting of wait_for`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ - refresh: 'wait_for', - }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`should use default index`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ index: '.kibana-test' }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn); - }); - - it(`should use custom index`, async () => { - const expectFn = () => - expect(client.update).toHaveBeenCalledWith( - expect.objectContaining({ index: 'custom' }), - expect.anything() - ); - await deleteFromNamespacesSuccessUpdate(expectFn, {}, MULTI_NAMESPACE_CUSTOM_INDEX_TYPE); - }); - }); - }); - - describe('errors', () => { - const expectNotFoundError = async (type, id, namespaces, options) => { - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, namespaces, options) - ).rejects.toThrowError(createGenericNotFoundError(type, id)); - }; - const expectBadRequestError = async (type, id, namespaces, message) => { - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, namespaces) - ).rejects.toThrowError(createBadRequestError(message)); - }; - - it(`throws when type is invalid`, async () => { - await expectNotFoundError('unknownType', id, [namespace1, namespace2]); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is hidden`, async () => { - await expectNotFoundError(HIDDEN_TYPE, id, [namespace1, namespace2]); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }); - - it(`throws when type is not shareable`, async () => { - const test = async (type) => { - const message = `${type} doesn't support multiple namespaces`; - await expectBadRequestError(type, id, [namespace1, namespace2], message); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }; - await test('index-pattern'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); - }); - - it(`throws when namespaces is an empty array`, async () => { - const test = async (namespaces) => { - const message = 'namespaces must be a non-empty array of strings'; - await expectBadRequestError(type, id, namespaces, message); - expect(client.delete).not.toHaveBeenCalled(); - expect(client.update).not.toHaveBeenCalled(); - }; - await test([]); - }); - - it(`throws when ES is unable to find the document during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) - ); - await expectNotFoundError(type, id, [namespace1, namespace2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during get`, async () => { - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [namespace1, namespace2]); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when the document exists, but not in this namespace`, async () => { - mockGetResponse(type, id, [namespace1]); - await expectNotFoundError(type, id, [namespace1], { namespace: 'some-other-namespace' }); - expect(client.get).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during delete`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the index during delete`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - error: { type: 'index_not_found_exception' }, - }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES returns an unexpected response`, async () => { - mockGetResponse(type, id, [namespace1]); - client.delete.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - result: 'something unexpected', - }) - ); - await expect( - savedObjectsRepository.deleteFromNamespaces(type, id, [namespace1]) - ).rejects.toThrowError('Unexpected Elasticsearch DELETE response'); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.delete).toHaveBeenCalledTimes(1); - }); - - it(`throws when ES is unable to find the document during update`, async () => { - mockGetResponse(type, id, [namespace1, namespace2]); - client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) - ); - await expectNotFoundError(type, id, [namespace1]); - expect(client.get).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledTimes(1); - }); - }); - - describe('returns', () => { - it(`returns an empty namespaces array on success (delete)`, async () => { - const test = async (namespaces) => { - const result = await deleteFromNamespacesSuccess(type, id, namespaces, namespaces); - expect(result).toEqual({ namespaces: [] }); - client.delete.mockClear(); - }; - await test([namespace1]); - await test([namespace1, namespace2]); - }); - - it(`returns remaining namespaces on success (update)`, async () => { - const test = async (remaining) => { - const currentNamespaces = [namespace1].concat(remaining); - const result = await deleteFromNamespacesSuccess( - type, - id, - [namespace1], - currentNamespaces - ); - expect(result).toEqual({ namespaces: remaining }); - client.delete.mockClear(); - }; - await test([namespace2]); - await test([namespace2, namespace3]); - }); - - it(`succeeds when the document doesn't exist in all of the targeted namespaces`, async () => { - const namespaces = [namespace2]; - const currentNamespaces = [namespace1]; - const result = await deleteFromNamespacesSuccess(type, id, namespaces, currentNamespaces); - expect(result).toEqual({ namespaces: currentNamespaces }); - }); - }); - }); - describe('#update', () => { const id = 'logstash-*'; const type = 'index-pattern'; @@ -4722,4 +4203,65 @@ describe('SavedObjectsRepository', () => { ); }); }); + + describe('#collectMultiNamespaceReferences', () => { + afterEach(() => { + mockCollectMultiNamespaceReferences.mockReset(); + }); + + it('passes arguments to the collectMultiNamespaceReferences module and returns the result', async () => { + const objects = Symbol(); + const expectedResult = Symbol(); + mockCollectMultiNamespaceReferences.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.collectMultiNamespaceReferences(objects) + ).resolves.toEqual(expectedResult); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockCollectMultiNamespaceReferences).toHaveBeenCalledWith( + expect.objectContaining({ objects }) + ); + }); + + it('returns an error from the collectMultiNamespaceReferences module', async () => { + const expectedResult = new Error('Oh no!'); + mockCollectMultiNamespaceReferences.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.collectMultiNamespaceReferences([])).rejects.toEqual( + expectedResult + ); + }); + }); + + describe('#updateObjectsSpaces', () => { + afterEach(() => { + mockUpdateObjectsSpaces.mockReset(); + }); + + it('passes arguments to the updateObjectsSpaces module and returns the result', async () => { + const objects = Symbol(); + const spacesToAdd = Symbol(); + const spacesToRemove = Symbol(); + const options = Symbol(); + const expectedResult = Symbol(); + mockUpdateObjectsSpaces.mockResolvedValue(expectedResult); + + await expect( + savedObjectsRepository.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).resolves.toEqual(expectedResult); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(mockUpdateObjectsSpaces).toHaveBeenCalledWith( + expect.objectContaining({ objects, spacesToAdd, spacesToRemove, options }) + ); + }); + + it('returns an error from the updateObjectsSpaces module', async () => { + const expectedResult = new Error('Oh no!'); + mockUpdateObjectsSpaces.mockRejectedValue(expectedResult); + + await expect(savedObjectsRepository.updateObjectsSpaces([], [], [])).rejects.toEqual( + expectedResult + ); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.test.mock.ts b/src/core/server/saved_objects/service/lib/repository.test.mock.ts index 3eba77b465819..f044fe9279fbf 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.mock.ts @@ -6,6 +6,36 @@ * Side Public License, v 1. */ +import type { collectMultiNamespaceReferences } from './collect_multi_namespace_references'; +import type * as InternalUtils from './internal_utils'; +import type { updateObjectsSpaces } from './update_objects_spaces'; + +export const mockCollectMultiNamespaceReferences = jest.fn() as jest.MockedFunction< + typeof collectMultiNamespaceReferences +>; + +jest.mock('./collect_multi_namespace_references', () => ({ + collectMultiNamespaceReferences: mockCollectMultiNamespaceReferences, +})); + +export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getBulkOperationError'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + getBulkOperationError: mockGetBulkOperationError, + }; +}); + +export const mockUpdateObjectsSpaces = jest.fn() as jest.MockedFunction; + +jest.mock('./update_objects_spaces', () => ({ + updateObjectsSpaces: mockUpdateObjectsSpaces, +})); + export const pointInTimeFinderMock = jest.fn(); jest.doMock('./point_in_time_finder', () => ({ PointInTimeFinder: pointInTimeFinderMock, diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 2ef3be71407b0..c626a2b2acfb5 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -48,10 +48,6 @@ import { SavedObjectsBulkUpdateObject, SavedObjectsBulkUpdateOptions, SavedObjectsDeleteOptions, - SavedObjectsAddToNamespacesOptions, - SavedObjectsAddToNamespacesResponse, - SavedObjectsDeleteFromNamespacesOptions, - SavedObjectsDeleteFromNamespacesResponse, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, SavedObjectsResolveResponse, @@ -64,15 +60,31 @@ import { MutatingOperationRefreshSetting, } from '../../types'; import { LegacyUrlAlias, LEGACY_URL_ALIAS_TYPE } from '../../object_types'; -import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; import { validateAndConvertAggregations } from './aggregations'; +import { + getBulkOperationError, + getExpectedVersionProperties, + getSavedObjectFromSource, + rawDocExistsInNamespace, +} from './internal_utils'; import { ALL_NAMESPACES_STRING, FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, SavedObjectsUtils, } from './utils'; +import { + collectMultiNamespaceReferences, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, +} from './collect_multi_namespace_references'; +import { + updateObjectsSpaces, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, +} from './update_objects_spaces'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -95,7 +107,7 @@ export interface SavedObjectsRepositoryOptions { index: string; mappings: IndexMapping; client: ElasticsearchClient; - typeRegistry: SavedObjectTypeRegistry; + typeRegistry: ISavedObjectTypeRegistry; serializer: SavedObjectsSerializer; migrator: IKibanaMigrator; allowedTypes: string[]; @@ -134,7 +146,7 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: boolean; } -const DEFAULT_REFRESH_SETTING = 'wait_for'; +export const DEFAULT_REFRESH_SETTING = 'wait_for'; /** * See {@link SavedObjectsRepository} @@ -160,7 +172,7 @@ export class SavedObjectsRepository { private _migrator: IKibanaMigrator; private _index: string; private _mappings: IndexMapping; - private _registry: SavedObjectTypeRegistry; + private _registry: ISavedObjectTypeRegistry; private _allowedTypes: string[]; private readonly client: RepositoryEsClient; private _serializer: SavedObjectsSerializer; @@ -176,7 +188,7 @@ export class SavedObjectsRepository { */ public static createRepository( migrator: IKibanaMigrator, - typeRegistry: SavedObjectTypeRegistry, + typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, @@ -511,16 +523,11 @@ export class SavedObjectsRepository { } const { requestedId, rawMigratedDoc, esRequestIndex } = expectedResult.value; - const { error, ...rawResponse } = Object.values( - bulkResponse?.body.items[esRequestIndex] ?? {} - )[0] as any; + const rawResponse = Object.values(bulkResponse?.body.items[esRequestIndex] ?? {})[0] as any; + const error = getBulkOperationError(rawMigratedDoc._source.type, requestedId, rawResponse); if (error) { - return { - id: requestedId, - type: rawMigratedDoc._source.type, - error: getBulkOperationError(error, rawMigratedDoc._source.type, requestedId), - }; + return { type: rawMigratedDoc._source.type, id: requestedId, error }; } // When method == 'index' the bulkResponse doesn't include the indexed @@ -989,7 +996,7 @@ export class SavedObjectsRepository { } // @ts-expect-error MultiGetHit._source is optional - return this.getSavedObjectFromSource(type, id, doc); + return getSavedObjectFromSource(this._registry, type, id, doc); }), }; } @@ -1033,7 +1040,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return this.getSavedObjectFromSource(type, id, body); + return getSavedObjectFromSource(this._registry, type, id, body); } /** @@ -1138,20 +1145,25 @@ export class SavedObjectsRepository { if (foundExactMatch && foundAliasMatch) { return { // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'conflict', aliasTargetId: legacyUrlAlias.targetId, }; } else if (foundExactMatch) { return { // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, id, exactMatchDoc), + saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'exactMatch', }; } else if (foundAliasMatch) { return { - // @ts-expect-error MultiGetHit._source is optional - saved_object: this.getSavedObjectFromSource(type, legacyUrlAlias.targetId, aliasMatchDoc), + saved_object: getSavedObjectFromSource( + this._registry, + type, + legacyUrlAlias.targetId, + // @ts-expect-error MultiGetHit._source is optional + aliasMatchDoc + ), outcome: 'aliasMatch', aliasTargetId: legacyUrlAlias.targetId, }; @@ -1263,169 +1275,52 @@ export class SavedObjectsRepository { } /** - * Adds one or more namespaces to a given multi-namespace saved object. This method and - * [`deleteFromNamespaces`]{@link SavedObjectsRepository.deleteFromNamespaces} are the only ways to change which Spaces a multi-namespace - * saved object is shared to. + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + * + * @param objects The objects to get the references for. */ - async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ): Promise { - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `${type} doesn't support multiple namespaces` - ); - } - - if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'namespaces must be a non-empty array of strings' - ); - } - - const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used - - const rawId = this._serializer.generateRawId(undefined, type, id); - const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); - // there should never be a case where a multi-namespace object does not have any existing namespaces - // however, it is a possibility if someone manually modifies the document in Elasticsearch - const time = this._getCurrentTime(); - - const doc = { - updated_at: time, - namespaces: existingNamespaces ? unique(existingNamespaces.concat(namespaces)) : namespaces, - }; - - const { statusCode } = await this.client.update( - { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult), - refresh, - body: { - doc, - }, - }, - { ignore: [404] } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - return { namespaces: doc.namespaces }; + async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ) { + return collectMultiNamespaceReferences({ + registry: this._registry, + allowedTypes: this._allowedTypes, + client: this.client, + serializer: this._serializer, + getIndexForType: this.getIndexForType.bind(this), + createPointInTimeFinder: this.createPointInTimeFinder.bind(this), + objects, + options, + }); } /** - * Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted - * entirely. This method and [`addToNamespaces`]{@link SavedObjectsRepository.addToNamespaces} are the only ways to change which Spaces a - * multi-namespace saved object is shared to. + * Updates one or more objects to add and/or remove them from specified spaces. + * + * @param objects + * @param spacesToAdd + * @param spacesToRemove + * @param options */ - async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ): Promise { - if (!this._allowedTypes.includes(type)) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `${type} doesn't support multiple namespaces` - ); - } - - if (!namespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'namespaces must be a non-empty array of strings' - ); - } - - const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; - // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used - - const rawId = this._serializer.generateRawId(undefined, type, id); - const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); - // if there are somehow no existing namespaces, allow the operation to proceed and delete this saved object - const remainingNamespaces = existingNamespaces?.filter((x) => !namespaces.includes(x)); - - if (remainingNamespaces?.length) { - // if there is 1 or more namespace remaining, update the saved object - const time = this._getCurrentTime(); - - const doc = { - updated_at: time, - namespaces: remainingNamespaces, - }; - - const { statusCode } = await this.client.update( - { - id: rawId, - index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), - refresh, - - body: { - doc, - }, - }, - { - ignore: [404], - } - ); - - if (statusCode === 404) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return { namespaces: doc.namespaces }; - } else { - // if there are no namespaces remaining, delete the saved object - const { body, statusCode } = await this.client.delete( - { - id: this._serializer.generateRawId(undefined, type, id), - refresh, - ...getExpectedVersionProperties(undefined, preflightResult), - index: this.getIndexForType(type), - }, - { - ignore: [404], - } - ); - - const deleted = body.result === 'deleted'; - if (deleted) { - return { namespaces: [] }; - } - - const deleteDocNotFound = body.result === 'not_found'; - // @ts-expect-error - const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; - if (deleteDocNotFound || deleteIndexNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - - throw new Error( - `Unexpected Elasticsearch DELETE response: ${JSON.stringify({ - type, - id, - response: { body, statusCode }, - })}` - ); - } + async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return updateObjectsSpaces({ + registry: this._registry, + allowedTypes: this._allowedTypes, + client: this.client, + serializer: this._serializer, + getIndexForType: this.getIndexForType.bind(this), + objects, + spacesToAdd, + spacesToRemove, + options, + }); } /** @@ -1617,21 +1512,19 @@ export class SavedObjectsRepository { const { type, id, namespaces, documentToSave, esRequestIndex } = expectedResult.value; const response = bulkUpdateResponse?.body.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { type, id, error }; + } + // When a bulk update operation is completed, any fields specified in `_sourceIncludes` will be found in the "get" value of the // returned object. We need to retrieve the `originId` if it exists so we can return it to the consumer. - const { error, _seq_no: seqNo, _primary_term: primaryTerm, get } = Object.values( - response - )[0] as any; + const { _seq_no: seqNo, _primary_term: primaryTerm, get } = rawResponse; // eslint-disable-next-line @typescript-eslint/naming-convention const { [type]: attributes, references, updated_at } = documentToSave; - if (error) { - return { - id, - type, - error: getBulkOperationError(error, type, id), - }; - } const { originId } = get._source; return { @@ -2055,10 +1948,10 @@ export class SavedObjectsRepository { * } * ``` */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies - ): ISavedObjectsPointInTimeFinder { + ): ISavedObjectsPointInTimeFinder { return new PointInTimeFinder(findOptions, { logger: this._logger, client: this, @@ -2108,28 +2001,8 @@ export class SavedObjectsRepository { return omit(savedObject, ['namespace']) as SavedObject; } - /** - * Check to ensure that a raw document exists in a namespace. If the document is not a multi-namespace type, then this returns `true` as - * we rely on the guarantees of the document ID format. If the document is a multi-namespace type, this checks to ensure that the - * document's `namespaces` value includes the string representation of the given namespace. - * - * WARNING: This should only be used for documents that were retrieved from Elasticsearch. Otherwise, the guarantees of the document ID - * format mentioned above do not apply. - */ - private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace?: string) { - const rawDocType = raw._source.type; - - // if the type is namespace isolated, or namespace agnostic, we can continue to rely on the guarantees - // of the document ID format and don't need to check this - if (!this._registry.isMultiNamespace(rawDocType)) { - return true; - } - - const namespaces = raw._source.namespaces; - const existsInNamespace = - namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) || - namespaces?.includes('*'); - return existsInNamespace ?? false; + private rawDocExistsInNamespace(raw: SavedObjectsRawDoc, namespace: string | undefined) { + return rawDocExistsInNamespace(this._registry, raw, namespace); } /** @@ -2204,34 +2077,6 @@ export class SavedObjectsRepository { return body; } - private getSavedObjectFromSource( - type: string, - id: string, - doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource } - ): SavedObject { - const { originId, updated_at: updatedAt } = doc._source; - - let namespaces: string[] = []; - if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [ - SavedObjectsUtils.namespaceIdToString(doc._source.namespace), - ]; - } - - return { - id, - type, - namespaces, - ...(originId && { originId }), - ...(updatedAt && { updated_at: updatedAt }), - version: encodeHitVersion(doc), - attributes: doc._source[type], - references: doc._source.references || [], - migrationVersion: doc._source.migrationVersion, - coreMigrationVersion: doc._source.coreMigrationVersion, - }; - } - private async resolveExactMatch( type: string, id: string, @@ -2242,43 +2087,6 @@ export class SavedObjectsRepository { } } -function getBulkOperationError( - error: { type: string; reason?: string; index?: string }, - type: string, - id: string -) { - switch (error.type) { - case 'version_conflict_engine_exception': - return errorContent(SavedObjectsErrorHelpers.createConflictError(type, id)); - case 'document_missing_exception': - return errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); - case 'index_not_found_exception': - return errorContent(SavedObjectsErrorHelpers.createIndexAliasNotFoundError(error.index!)); - default: - return { - message: error.reason || JSON.stringify(error), - }; - } -} - -/** - * Returns an object with the expected version properties. This facilitates Elasticsearch's Optimistic Concurrency Control. - * - * @param version Optional version specified by the consumer. - * @param document Optional existing document that was obtained in a preflight operation. - */ -function getExpectedVersionProperties(version?: string, document?: SavedObjectsRawDoc) { - if (version) { - return decodeRequestVersion(version); - } else if (document) { - return { - if_seq_no: document._seq_no, - if_primary_term: document._primary_term, - }; - } - return {}; -} - /** * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts new file mode 100644 index 0000000000000..d7aa762e01aab --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.mock.ts @@ -0,0 +1,29 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type * as InternalUtils from './internal_utils'; + +export const mockGetBulkOperationError = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getBulkOperationError'] +>; +export const mockGetExpectedVersionProperties = jest.fn() as jest.MockedFunction< + typeof InternalUtils['getExpectedVersionProperties'] +>; +export const mockRawDocExistsInNamespace = jest.fn() as jest.MockedFunction< + typeof InternalUtils['rawDocExistsInNamespace'] +>; + +jest.mock('./internal_utils', () => { + const actual = jest.requireActual('./internal_utils'); + return { + ...actual, + getBulkOperationError: mockGetBulkOperationError, + getExpectedVersionProperties: mockGetExpectedVersionProperties, + rawDocExistsInNamespace: mockRawDocExistsInNamespace, + }; +}); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts new file mode 100644 index 0000000000000..489432a4ab169 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts @@ -0,0 +1,453 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + mockGetBulkOperationError, + mockGetExpectedVersionProperties, + mockRawDocExistsInNamespace, +} from './update_objects_spaces.test.mock'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/target/jest'; +import type { ElasticsearchClient } from 'src/core/server/elasticsearch'; +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { typeRegistryMock } from '../../saved_objects_type_registry.mock'; +import { SavedObjectsSerializer } from '../../serialization'; +import type { + SavedObjectsUpdateObjectsSpacesObject, + UpdateObjectsSpacesParams, +} from './update_objects_spaces'; +import { updateObjectsSpaces } from './update_objects_spaces'; + +type SetupParams = Partial< + Pick +>; + +const EXISTING_SPACE = 'existing-space'; +const VERSION_PROPS = { _seq_no: 1, _primary_term: 1 }; +const EXPECTED_VERSION_PROPS = { if_seq_no: 1, if_primary_term: 1 }; +const BULK_ERROR = { + error: 'Oh no, a bulk error!', + type: 'error_type', + message: 'error_message', + statusCode: 400, +}; + +const SHAREABLE_OBJ_TYPE = 'type-a'; +const NON_SHAREABLE_OBJ_TYPE = 'type-b'; +const SHAREABLE_HIDDEN_OBJ_TYPE = 'type-c'; + +const mockCurrentTime = new Date('2021-05-01T10:20:30Z'); + +beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(mockCurrentTime); +}); + +beforeEach(() => { + mockGetExpectedVersionProperties.mockReturnValue(EXPECTED_VERSION_PROPS); + mockRawDocExistsInNamespace.mockReset(); + mockRawDocExistsInNamespace.mockReturnValue(true); // return true by default +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +describe('#updateObjectsSpaces', () => { + let client: DeeplyMockedKeys; + + /** Sets up the type registry, saved objects client, etc. and return the full parameters object to be passed to `updateObjectsSpaces` */ + function setup({ objects = [], spacesToAdd = [], spacesToRemove = [], options }: SetupParams) { + const registry = typeRegistryMock.create(); + registry.isShareable.mockImplementation( + (type) => [SHAREABLE_OBJ_TYPE, SHAREABLE_HIDDEN_OBJ_TYPE].includes(type) // NON_SHAREABLE_OBJ_TYPE is excluded + ); + client = elasticsearchClientMock.createElasticsearchClient(); + const serializer = new SavedObjectsSerializer(registry); + return { + registry, + allowedTypes: [SHAREABLE_OBJ_TYPE, NON_SHAREABLE_OBJ_TYPE], // SHAREABLE_HIDDEN_OBJ_TYPE is excluded + client, + serializer, + getIndexForType: (type: string) => `index-for-${type}`, + objects, + spacesToAdd, + spacesToRemove, + options, + }; + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockMgetResults(...results: Array<{ found: boolean }>) { + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + docs: results.map((x) => + x.found + ? { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + _source: { namespaces: [EXISTING_SPACE] }, + ...VERSION_PROPS, + found: true, + } + : { + _id: 'doesnt-matter', + _index: 'doesnt-matter', + found: false, + } + ), + }) + ); + } + + /** Asserts that mget is called for the given objects */ + function expectMgetArgs(...objects: SavedObjectsUpdateObjectsSpacesObject[]) { + const docs = objects.map(({ type, id }) => expect.objectContaining({ _id: `${type}:${id}` })); + expect(client.mget).toHaveBeenCalledWith({ body: { docs } }, expect.anything()); + } + + /** Mocks the saved objects client so it returns the expected results */ + function mockBulkResults(...results: Array<{ error: boolean }>) { + results.forEach(({ error }) => { + if (error) { + mockGetBulkOperationError.mockReturnValueOnce(BULK_ERROR); + } else { + mockGetBulkOperationError.mockReturnValueOnce(undefined); + } + }); + client.bulk.mockReturnValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + items: results.map(() => ({})), // as long as the result does not contain an error field, it is treated as a success + errors: false, + took: 0, + }) + ); + } + + /** Asserts that mget is called for the given objects */ + function expectBulkArgs( + ...objectActions: Array<{ + object: { type: string; id: string; namespaces?: string[] }; + action: 'update' | 'delete'; + }> + ) { + const body = objectActions.flatMap( + ({ object: { type, id, namespaces = expect.any(Array) }, action }) => { + const operation = { + [action]: { + _id: `${type}:${id}`, + _index: `index-for-${type}`, + ...EXPECTED_VERSION_PROPS, + }, + }; + return action === 'update' + ? [operation, { doc: { namespaces, updated_at: mockCurrentTime.toISOString() } }] // 'update' uses an operation and document metadata + : [operation]; // 'delete' only uses an operation + } + ); + expect(client.bulk).toHaveBeenCalledWith(expect.objectContaining({ body })); + } + + beforeEach(() => { + mockGetBulkOperationError.mockReset(); // reset calls and return undefined by default + }); + + describe('errors', () => { + it('throws when spacesToAdd and spacesToRemove are empty', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const params = setup({ objects }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow( + 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings: Bad Request' + ); + }); + + it('throws when spacesToAdd and spacesToRemove intersect', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space', 'bar-space']; + const spacesToRemove = ['bar-space', 'baz-space']; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow( + 'spacesToAdd and spacesToRemove cannot contain any of the same strings: Bad Request' + ); + }); + + it('throws when mget cluster call fails', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + client.mget.mockReturnValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('mget error')) + ); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow('mget error'); + }); + + it('throws when bulk cluster call fails', async () => { + const objects = [{ type: SHAREABLE_OBJ_TYPE, id: 'id-1' }]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }); + client.bulk.mockReturnValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('bulk error')) + ); + + await expect(() => updateObjectsSpaces(params)).rejects.toThrow('bulk error'); + }); + + it('returns mix of type errors, mget/bulk cluster errors, and successes', async () => { + const obj1 = { type: SHAREABLE_HIDDEN_OBJ_TYPE, id: 'id-1' }; // invalid type (Not Found) + const obj2 = { type: NON_SHAREABLE_OBJ_TYPE, id: 'id-2' }; // non-shareable type (Bad Request) + // obj3 below is mocking an example where a SOC wrapper attempted to retrieve it in a pre-flight request but it was not found. + // Since it has 'spaces: []', that indicates it should be skipped for cluster calls and just returned as a Not Found error. + // Realistically this would not be intermingled with other requested objects that do not have 'spaces' arrays, but it's fine for this + // specific test case. + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [] }; // does not exist (Not Found) + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4' }; // mget error (found but doesn't exist in the current space) + const obj5 = { type: SHAREABLE_OBJ_TYPE, id: 'id-5' }; // mget error (Not Found) + const obj6 = { type: SHAREABLE_OBJ_TYPE, id: 'id-6' }; // bulk error (mocked as BULK_ERROR) + const obj7 = { type: SHAREABLE_OBJ_TYPE, id: 'id-7' }; // success + + const objects = [obj1, obj2, obj3, obj4, obj5, obj6, obj7]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }, { found: false }, { found: true }, { found: true }); // results for obj4, obj5, obj6, and obj7 + mockRawDocExistsInNamespace.mockReturnValueOnce(false); // for obj4 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj6 + mockRawDocExistsInNamespace.mockReturnValueOnce(true); // for obj7 + mockBulkResults({ error: true }, { error: false }); // results for obj6 and obj7 + + const result = await updateObjectsSpaces(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(obj4, obj5, obj6, obj7); + expect(mockRawDocExistsInNamespace).toHaveBeenCalledTimes(3); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ action: 'update', object: obj6 }, { action: 'update', object: obj7 }); + expect(result.objects).toEqual([ + { ...obj1, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj2, spaces: [], error: expect.objectContaining({ error: 'Bad Request' }) }, + { ...obj3, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj4, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj5, spaces: [], error: expect.objectContaining({ error: 'Not Found' }) }, + { ...obj6, spaces: [], error: BULK_ERROR }, + { ...obj7, spaces: [EXISTING_SPACE, 'foo-space'] }, + ]); + }); + }); + + // Note: these test cases do not include requested objects that will result in errors (those are covered above) + describe('cluster and module calls', () => { + it('mget call skips objects that have "spaces" defined', async () => { + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2' }; // will be passed to mget + + const objects = [obj1, obj2]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockMgetResults({ found: true }); // result for obj2 + mockBulkResults({ error: false }, { error: false }); // results for obj1 and obj2 + + await updateObjectsSpaces(params); + expect(client.mget).toHaveBeenCalledTimes(1); + expectMgetArgs(obj2); + }); + + it('does not call mget if all objects have "spaces" defined', async () => { + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [EXISTING_SPACE] }; // will not be retrieved + + const objects = [obj1]; + const spacesToAdd = ['foo-space']; + const params = setup({ objects, spacesToAdd }); + mockBulkResults({ error: false }); // result for obj1 + + await updateObjectsSpaces(params); + expect(client.mget).not.toHaveBeenCalled(); + }); + + describe('bulk call skips objects that will not be changed', () => { + it('when adding spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs({ + action: 'update', + object: { ...obj2, namespaces: [space2, space1] }, + }); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left) + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space2] } }, + { action: 'delete', object: obj3 } + ); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2 + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2 + + const objects = [obj1, obj2, obj3, obj4]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 + + await updateObjectsSpaces(params); + expect(client.bulk).toHaveBeenCalledTimes(1); + expectBulkArgs( + { action: 'update', object: { ...obj2, namespaces: [space3, space1] } }, + { action: 'update', object: { ...obj3, namespaces: [space1] } }, + { action: 'update', object: { ...obj4, namespaces: [space3, space1] } } + ); + }); + }); + + describe('does not call bulk if all objects do not need to be changed', () => { + it('when adding spaces', async () => { + const space = 'space-to-add'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space] }; // will not be changed + + const objects = [obj1]; + const spacesToAdd = [space]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + + const objects = [obj1]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + + const objects = [obj1]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget or bulk + + await updateObjectsSpaces(params); + expect(client.bulk).not.toHaveBeenCalled(); + }); + }); + }); + + describe('returns expected results', () => { + it('when adding spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space2] }; // will be updated + + const objects = [obj1, obj2]; + const spacesToAdd = [space1]; + const params = setup({ objects, spacesToAdd }); + // this test case does not call mget + mockBulkResults({ error: false }); // result for obj2 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space1] }, + { ...obj2, spaces: [space2, space1] }, + ]); + }); + + it('when removing spaces', async () => { + const space1 = 'space-to-remove'; + const space2 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space2] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space1, space2] }; // will be updated to remove space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1] }; // will be deleted (since it would have no spaces left) + + const objects = [obj1, obj2, obj3]; + const spacesToRemove = [space1]; + const params = setup({ objects, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }); // results for obj2 and obj3 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space2] }, + { ...obj2, spaces: [space2] }, + { ...obj3, spaces: [] }, + ]); + }); + + it('when adding and removing spaces', async () => { + const space1 = 'space-to-add'; + const space2 = 'space-to-remove'; + const space3 = 'other-space'; + const obj1 = { type: SHAREABLE_OBJ_TYPE, id: 'id-1', spaces: [space1] }; // will not be changed + const obj2 = { type: SHAREABLE_OBJ_TYPE, id: 'id-2', spaces: [space3] }; // will be updated to add space1 + const obj3 = { type: SHAREABLE_OBJ_TYPE, id: 'id-3', spaces: [space1, space2] }; // will be updated to remove space2 + const obj4 = { type: SHAREABLE_OBJ_TYPE, id: 'id-4', spaces: [space2, space3] }; // will be updated to add space1 and remove space2 + + const objects = [obj1, obj2, obj3, obj4]; + const spacesToAdd = [space1]; + const spacesToRemove = [space2]; + const params = setup({ objects, spacesToAdd, spacesToRemove }); + // this test case does not call mget + mockBulkResults({ error: false }, { error: false }, { error: false }); // results for obj2, obj3, and obj4 + + const result = await updateObjectsSpaces(params); + expect(result.objects).toEqual([ + { ...obj1, spaces: [space1] }, + { ...obj2, spaces: [space3, space1] }, + { ...obj3, spaces: [space1] }, + { ...obj4, spaces: [space3, space1] }, + ]); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/update_objects_spaces.ts b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts new file mode 100644 index 0000000000000..079549265385c --- /dev/null +++ b/src/core/server/saved_objects/service/lib/update_objects_spaces.ts @@ -0,0 +1,315 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { BulkOperationContainer, MultiGetOperation } from '@elastic/elasticsearch/api/types'; +import intersection from 'lodash/intersection'; + +import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import type { SavedObjectsRawDocSource, SavedObjectsSerializer } from '../../serialization'; +import type { + MutatingOperationRefreshSetting, + SavedObjectError, + SavedObjectsBaseOptions, +} from '../../types'; +import type { DecoratedError } from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; +import { + getBulkOperationError, + getExpectedVersionProperties, + rawDocExistsInNamespace, +} from './internal_utils'; +import { DEFAULT_REFRESH_SETTING } from './repository'; +import type { RepositoryEsClient } from './repository_es_client'; + +/** + * An object that should have its spaces updated. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesObject { + /** The type of the object to update */ + id: string; + /** The ID of the object to update */ + type: string; + /** + * The space(s) that the object to update currently exists in. This is only intended to be used by SOC wrappers. + * + * @internal + */ + spaces?: string[]; + /** + * The version of the object to update; this is used for optimistic concurrency control. This is only intended to be used by SOC wrappers. + * + * @internal + */ + version?: string; +} + +/** + * Options for the update operation. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { + /** The Elasticsearch Refresh setting for this operation */ + refresh?: MutatingOperationRefreshSetting; +} + +/** + * The response when objects' spaces are updated. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesResponse { + objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +} + +/** + * Details about a specific object's update result. + * + * @public + */ +export interface SavedObjectsUpdateObjectsSpacesResponseObject { + /** The type of the referenced object */ + type: string; + /** The ID of the referenced object */ + id: string; + /** The space(s) that the referenced object exists in */ + spaces: string[]; + /** Included if there was an error updating this object's spaces */ + error?: SavedObjectError; +} + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Left = { tag: 'Left'; error: SavedObjectsUpdateObjectsSpacesResponseObject }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type Right = { tag: 'Right'; value: Record }; +type Either = Left | Right; +const isLeft = (either: Either): either is Left => either.tag === 'Left'; +const isRight = (either: Either): either is Right => either.tag === 'Right'; + +/** + * Parameters for the updateObjectsSpaces function. + * + * @internal + */ +export interface UpdateObjectsSpacesParams { + registry: ISavedObjectTypeRegistry; + allowedTypes: string[]; + client: RepositoryEsClient; + serializer: SavedObjectsSerializer; + getIndexForType: (type: string) => string; + objects: SavedObjectsUpdateObjectsSpacesObject[]; + spacesToAdd: string[]; + spacesToRemove: string[]; + options?: SavedObjectsUpdateObjectsSpacesOptions; +} + +/** + * Gets all references and transitive references of the given objects. Ignores any object and/or reference that is not a multi-namespace + * type. + */ +export async function updateObjectsSpaces({ + registry, + allowedTypes, + client, + serializer, + getIndexForType, + objects, + spacesToAdd, + spacesToRemove, + options = {}, +}: UpdateObjectsSpacesParams): Promise { + if (!spacesToAdd.length && !spacesToRemove.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'spacesToAdd and/or spacesToRemove must be a non-empty array of strings' + ); + } + if (intersection(spacesToAdd, spacesToRemove).length > 0) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'spacesToAdd and spacesToRemove cannot contain any of the same strings' + ); + } + + const { namespace } = options; + + let bulkGetRequestIndexCounter = 0; + const expectedBulkGetResults: Either[] = objects.map((object) => { + const { type, id, spaces, version } = object; + + if (!allowedTypes.includes(type)) { + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + if (!registry.isShareable(type)) { + const error = errorContent( + SavedObjectsErrorHelpers.createBadRequestError( + `${type} doesn't support multiple namespaces` + ) + ); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + + return { + tag: 'Right' as 'Right', + value: { + type, + id, + spaces, + version, + ...(!spaces && { esRequestIndex: bulkGetRequestIndexCounter++ }), + }, + }; + }); + + const bulkGetDocs = expectedBulkGetResults.reduce((acc, x) => { + if (isRight(x) && x.value.esRequestIndex !== undefined) { + acc.push({ + _id: serializer.generateRawId(undefined, x.value.type, x.value.id), + _index: getIndexForType(x.value.type), + _source: ['type', 'namespaces'], + }); + } + return acc; + }, []); + const bulkGetResponse = bulkGetDocs.length + ? await client.mget( + { body: { docs: bulkGetDocs } }, + { ignore: [404] } + ) + : undefined; + + const time = new Date().toISOString(); + let bulkOperationRequestIndexCounter = 0; + const bulkOperationParams: BulkOperationContainer[] = []; + const expectedBulkOperationResults: Either[] = expectedBulkGetResults.map( + (expectedBulkGetResult) => { + if (isLeft(expectedBulkGetResult)) { + return expectedBulkGetResult; + } + + const { id, type, spaces, version, esRequestIndex } = expectedBulkGetResult.value; + + let currentSpaces: string[] = spaces; + let versionProperties; + if (esRequestIndex !== undefined) { + const doc = bulkGetResponse?.body.docs[esRequestIndex]; + // @ts-expect-error MultiGetHit._source is optional + if (!doc?.found || !rawDocExistsInNamespace(registry, doc, namespace)) { + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } + currentSpaces = doc._source?.namespaces ?? []; + // @ts-expect-error MultiGetHit._source is optional + versionProperties = getExpectedVersionProperties(version, doc); + } else if (spaces?.length === 0) { + // A SOC wrapper attempted to retrieve this object in a pre-flight request and it was not found. + const error = errorContent(SavedObjectsErrorHelpers.createGenericNotFoundError(type, id)); + return { + tag: 'Left' as 'Left', + error: { id, type, spaces: [], error }, + }; + } else { + versionProperties = getExpectedVersionProperties(version); + } + + const { newSpaces, isUpdateRequired } = getNewSpacesArray( + currentSpaces, + spacesToAdd, + spacesToRemove + ); + const expectedResult = { + type, + id, + newSpaces, + ...(isUpdateRequired && { esRequestIndex: bulkOperationRequestIndexCounter++ }), + }; + + if (isUpdateRequired) { + const documentMetadata = { + _id: serializer.generateRawId(undefined, type, id), + _index: getIndexForType(type), + ...versionProperties, + }; + if (newSpaces.length) { + const documentToSave = { updated_at: time, namespaces: newSpaces }; + // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional + bulkOperationParams.push({ update: documentMetadata }, { doc: documentToSave }); + } else { + // @ts-expect-error BulkOperation.retry_on_conflict, BulkOperation.routing. BulkOperation.version, and BulkOperation.version_type are optional + bulkOperationParams.push({ delete: documentMetadata }); + } + } + + return { tag: 'Right' as 'Right', value: expectedResult }; + } + ); + + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const bulkOperationResponse = bulkOperationParams.length + ? await client.bulk({ refresh, body: bulkOperationParams, require_alias: true }) + : undefined; + + return { + objects: expectedBulkOperationResults.map( + (expectedResult) => { + if (isLeft(expectedResult)) { + return expectedResult.error; + } + + const { type, id, newSpaces, esRequestIndex } = expectedResult.value; + if (esRequestIndex !== undefined) { + const response = bulkOperationResponse?.body.items[esRequestIndex] ?? {}; + const rawResponse = Object.values(response)[0] as any; + const error = getBulkOperationError(type, id, rawResponse); + if (error) { + return { id, type, spaces: [], error }; + } + } + + return { id, type, spaces: newSpaces }; + } + ), + }; +} + +/** Extracts the contents of a decorated error to return the attributes for bulk operations. */ +function errorContent(error: DecoratedError) { + return error.output.payload; +} + +/** Gets the remaining spaces for an object after adding new ones and removing old ones. */ +function getNewSpacesArray( + existingSpaces: string[], + spacesToAdd: string[], + spacesToRemove: string[] +) { + const addSet = new Set(spacesToAdd); + const removeSet = new Set(spacesToRemove); + const newSpaces = existingSpaces + .filter((x) => { + addSet.delete(x); + return !removeSet.delete(x); + }) + .concat(Array.from(addSet)); + + const isAnySpaceAdded = addSet.size > 0; + const isAnySpaceRemoved = removeSet.size < spacesToRemove.length; + const isUpdateRequired = isAnySpaceAdded || isAnySpaceRemoved; + + return { newSpaces, isUpdateRequired }; +} diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 544e92e32f1a1..e02387d41addf 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -26,9 +26,9 @@ const create = () => { openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), - addToNamespaces: jest.fn(), - deleteFromNamespaces: jest.fn(), removeReferencesTo: jest.fn(), + collectMultiNamespaceReferences: jest.fn(), + updateObjectsSpaces: jest.fn(), } as unknown) as jest.Mocked; mock.createPointInTimeFinder = savedObjectsPointInTimeFinderMock.create({ diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 29381c7e418b5..1a369475f2c6d 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -237,52 +237,39 @@ test(`#bulkUpdate`, async () => { expect(result).toBe(returnValue); }); -test(`#addToNamespaces`, async () => { +test(`#collectMultiNamespaceReferences`, async () => { const returnValue = Symbol(); const mockRepository = { - addToNamespaces: jest.fn().mockResolvedValue(returnValue), + collectMultiNamespaceReferences: jest.fn().mockResolvedValue(returnValue), }; const client = new SavedObjectsClient(mockRepository); - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Symbol(); - const result = await client.addToNamespaces(type, id, namespaces, options); - - expect(mockRepository.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); - expect(result).toBe(returnValue); -}); - -test(`#deleteFromNamespaces`, async () => { - const returnValue = Symbol(); - const mockRepository = { - deleteFromNamespaces: jest.fn().mockResolvedValue(returnValue), - }; - const client = new SavedObjectsClient(mockRepository); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); + const objects = Symbol(); const options = Symbol(); - const result = await client.deleteFromNamespaces(type, id, namespaces, options); + const result = await client.collectMultiNamespaceReferences(objects, options); - expect(mockRepository.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, options); + expect(mockRepository.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); expect(result).toBe(returnValue); }); -test(`#removeReferencesTo`, async () => { +test(`#updateObjectsSpaces`, async () => { const returnValue = Symbol(); const mockRepository = { - removeReferencesTo: jest.fn().mockResolvedValue(returnValue), + updateObjectsSpaces: jest.fn().mockResolvedValue(returnValue), }; const client = new SavedObjectsClient(mockRepository); - const type = Symbol(); - const id = Symbol(); + const objects = Symbol(); + const spacesToAdd = Symbol(); + const spacesToRemove = Symbol(); const options = Symbol(); - const result = await client.removeReferencesTo(type, id, options); - - expect(mockRepository.removeReferencesTo).toHaveBeenCalledWith(type, id, options); + const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockRepository.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + options + ); expect(result).toBe(returnValue); }); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index bf5cae0736cad..af682cfb81296 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -11,6 +11,11 @@ import type { ISavedObjectsPointInTimeFinder, SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreatePointInTimeFinderDependencies, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, } from './lib'; import { SavedObject, @@ -218,44 +223,6 @@ export interface SavedObjectsUpdateOptions extends SavedOb upsert?: Attributes; } -/** - * - * @public - */ -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ - version?: string; - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - -/** - * - * @public - */ -export interface SavedObjectsAddToNamespacesResponse { - /** The namespaces the object exists in after this operation is complete. */ - namespaces: string[]; -} - -/** - * - * @public - */ -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { - /** The Elasticsearch Refresh setting for this operation */ - refresh?: MutatingOperationRefreshSetting; -} - -/** - * - * @public - */ -export interface SavedObjectsDeleteFromNamespacesResponse { - /** The namespaces the object exists in after this operation is complete. An empty array indicates the object was deleted. */ - namespaces: string[]; -} - /** * * @public @@ -536,40 +503,6 @@ export class SavedObjectsClient { return await this._repository.update(type, id, attributes, options); } - /** - * Adds namespaces to a SavedObject - * - * @param type - * @param id - * @param namespaces - * @param options - */ - async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ): Promise { - return await this._repository.addToNamespaces(type, id, namespaces, options); - } - - /** - * Removes namespaces from a SavedObject - * - * @param type - * @param id - * @param namespaces - * @param options - */ - async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ): Promise { - return await this._repository.deleteFromNamespaces(type, id, namespaces, options); - } - /** * Bulk Updates multiple SavedObject at once * @@ -665,14 +598,49 @@ export class SavedObjectsClient { * } * ``` */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies - ): ISavedObjectsPointInTimeFinder { + ): ISavedObjectsPointInTimeFinder { return this._repository.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that SO client wrappers have their settings applied. ...dependencies, }); } + + /** + * Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. + * + * @param objects + * @param options + */ + async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ): Promise { + return await this._repository.collectMultiNamespaceReferences(objects, options); + } + + /** + * Updates one or more objects to add and/or remove them from specified spaces. + * + * @param objects + * @param spacesToAdd + * @param spacesToRemove + * @param options + */ + async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return await this._repository.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + } } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index f4c70d718bc87..972e220baae3e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1255,9 +1255,9 @@ export type ISavedObjectsExporter = PublicMethodsOf; export type ISavedObjectsImporter = PublicMethodsOf; // @public (undocumented) -export interface ISavedObjectsPointInTimeFinder { +export interface ISavedObjectsPointInTimeFinder { close: () => Promise; - find: () => AsyncGenerator; + find: () => AsyncGenerator>; } // @public @@ -2144,6 +2144,7 @@ export type SavedObjectAttributeSingle = string | number | boolean | null | unde // @public (undocumented) export interface SavedObjectExportBaseOptions { excludeExportDetails?: boolean; + includeNamespaces?: boolean; includeReferencesDeep?: boolean; namespace?: string; request: KibanaRequest; @@ -2175,15 +2176,18 @@ export interface SavedObjectReference { type: string; } -// @public (undocumented) -export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; - version?: string; -} - -// @public (undocumented) -export interface SavedObjectsAddToNamespacesResponse { - namespaces: string[]; +// @public +export interface SavedObjectReferenceWithContext { + id: string; + inboundReferences: Array<{ + type: string; + id: string; + name: string; + }>; + isMissing?: boolean; + spaces: string[]; + spacesWithMatchingAliases?: string[]; + type: string; } // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts @@ -2277,16 +2281,15 @@ export interface SavedObjectsCheckConflictsResponse { export class SavedObjectsClient { // @internal constructor(repository: ISavedObjectsRepository); - addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; - deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) static errors: typeof SavedObjectsErrorHelpers; // (undocumented) @@ -2297,6 +2300,7 @@ export class SavedObjectsClient { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2341,6 +2345,25 @@ export interface SavedObjectsClosePointInTimeResponse { succeeded: boolean; } +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesObject { + // (undocumented) + id: string; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesOptions extends SavedObjectsBaseOptions { + purpose?: 'collectMultiNamespaceReferences' | 'updateObjectsSpaces'; +} + +// @public +export interface SavedObjectsCollectMultiNamespaceReferencesResponse { + // (undocumented) + objects: SavedObjectReferenceWithContext[]; +} + // @public export interface SavedObjectsComplexFieldMapping { // (undocumented) @@ -2401,16 +2424,6 @@ export interface SavedObjectsDeleteByNamespaceOptions extends SavedObjectsBaseOp refresh?: boolean; } -// @public (undocumented) -export interface SavedObjectsDeleteFromNamespacesOptions extends SavedObjectsBaseOptions { - refresh?: MutatingOperationRefreshSetting; -} - -// @public (undocumented) -export interface SavedObjectsDeleteFromNamespacesResponse { - namespaces: string[]; -} - // @public (undocumented) export interface SavedObjectsDeleteOptions extends SavedObjectsBaseOptions { force?: boolean; @@ -2884,21 +2897,20 @@ export interface SavedObjectsRemoveReferencesToResponse extends SavedObjectsBase // @public (undocumented) export class SavedObjectsRepository { - addToNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsAddToNamespacesOptions): Promise; bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; + collectMultiNamespaceReferences(objects: SavedObjectsCollectMultiNamespaceReferencesObject[], options?: SavedObjectsCollectMultiNamespaceReferencesOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; + createPointInTimeFinder(findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies): ISavedObjectsPointInTimeFinder; // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: IKibanaMigrator, typeRegistry: ISavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, logger: Logger, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; - deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; @@ -2907,6 +2919,7 @@ export class SavedObjectsRepository { removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; + updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; } // @public @@ -2938,7 +2951,7 @@ export class SavedObjectsSerializer { generateRawId(namespace: string | undefined, type: string, id: string): string; generateRawLegacyUrlAliasId(namespace: string, type: string, id: string): string; isRawSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): boolean; - rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; + rawToSavedObject(doc: SavedObjectsRawDoc, options?: SavedObjectsRawDocParseOptions): SavedObjectSanitizedDoc; savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; } @@ -3004,6 +3017,35 @@ export interface SavedObjectsTypeMappingDefinition { properties: SavedObjectsMappingProperties; } +// @public +export interface SavedObjectsUpdateObjectsSpacesObject { + id: string; + // @internal + spaces?: string[]; + type: string; + // @internal + version?: string; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesOptions extends SavedObjectsBaseOptions { + refresh?: MutatingOperationRefreshSetting; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesResponse { + // (undocumented) + objects: SavedObjectsUpdateObjectsSpacesResponseObject[]; +} + +// @public +export interface SavedObjectsUpdateObjectsSpacesResponseObject { + error?: SavedObjectError; + id: string; + spaces: string[]; + type: string; +} + // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; diff --git a/src/core/server/types.ts b/src/core/server/types.ts index be07a3cfb1fd3..77b5378f9477f 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -37,6 +37,10 @@ export type { SavedObjectsClientContract, SavedObjectsNamespaceType, } from './saved_objects/types'; +export type { + SavedObjectReferenceWithContext, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from './saved_objects/service'; export type { DomainDeprecationDetails, DeprecationsGetResponse } from './deprecations/types'; export * from './ui_settings/types'; export type { EnvironmentMode, PackageInfo } from '@kbn/config'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index a71ce360a2190..dbb49825b2409 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -993,7 +993,7 @@ export class IndexPatternsServiceProvider implements Plugin_3, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1263,7 +1263,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; }; search: ISearchStart>; }; diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index e460d9a43ef6b..ddee9c0528ba1 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -169,8 +169,8 @@ export interface ShareToSpaceFlyoutProps { behaviorContext?: 'within-space' | 'outside-space'; /** * Optional handler that is called when the user has saved changes and there are spaces to be added to and/or removed from the object. If - * this is not defined, a default handler will be used that calls `/api/spaces/_share_saved_object_add` and/or - * `/api/spaces/_share_saved_object_remove` and displays toast(s) indicating what occurred. + * this is not defined, a default handler will be used that calls `/api/spaces/_update_objects_spaces` and displays a toast indicating + * what occurred. */ changeSpacesHandler?: (spacesToAdd: string[], spacesToRemove: string[]) => Promise; /** diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index dcd34c604dc31..d009a66e9df55 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -574,6 +574,7 @@ export default ({ getService }: FtrProviderContext) => { id: 'legacy-url-alias:spacex:foo:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newFooId, targetNamespace: 'spacex', targetType: 'foo', @@ -606,6 +607,7 @@ export default ({ getService }: FtrProviderContext) => { id: 'legacy-url-alias:spacex:bar:1', type: 'legacy-url-alias', 'legacy-url-alias': { + sourceId: '1', targetId: newBarId, targetNamespace: 'spacex', targetType: 'bar', diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index d18e7e427eeca..10a645295e2de 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -1819,6 +1819,82 @@ describe('#closePointInTime', () => { expect(mockBaseClient.closePointInTime).toHaveBeenCalledTimes(1); }); + + describe('#collectMultiNamespaceReferences', () => { + it('redirects request to underlying base client', async () => { + const objects = [{ type: 'foo', id: 'bar' }]; + const options = { namespace: 'some-ns' }; + await wrapper.collectMultiNamespaceReferences(objects, options); + + expect(mockBaseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockBaseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, options); + }); + + it('returns response from underlying client', async () => { + const returnValue = { objects: [] }; + mockBaseClient.collectMultiNamespaceReferences.mockResolvedValue(returnValue); + + const objects = [{ type: 'foo', id: 'bar' }]; + const result = await wrapper.collectMultiNamespaceReferences(objects); + + expect(result).toBe(returnValue); + }); + + it('fails if base client fails', async () => { + const failureReason = new Error('Something bad happened...'); + mockBaseClient.collectMultiNamespaceReferences.mockRejectedValue(failureReason); + + const objects = [{ type: 'foo', id: 'bar' }]; + await expect(wrapper.collectMultiNamespaceReferences(objects)).rejects.toThrowError( + failureReason + ); + + expect(mockBaseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + }); + }); + + describe('#updateObjectsSpaces', () => { + const objects = [{ type: 'foo', id: 'bar' }]; + const spacesToAdd = ['space-x']; + const spacesToRemove = ['space-y']; + const options = {}; + it('redirects request to underlying base client', async () => { + await wrapper.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockBaseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(mockBaseClient.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + options + ); + }); + + it('returns response from underlying client', async () => { + const returnValue = { objects: [] }; + mockBaseClient.updateObjectsSpaces.mockResolvedValue(returnValue); + + const result = await wrapper.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + + expect(result).toBe(returnValue); + }); + + it('fails if base client fails', async () => { + const failureReason = new Error('Something bad happened...'); + mockBaseClient.updateObjectsSpaces.mockRejectedValue(failureReason); + + await expect( + wrapper.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).rejects.toThrowError(failureReason); + + expect(mockBaseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + }); + }); }); describe('#createPointInTimeFinder', () => { diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index 9b699d6ce007c..a339f213bdce4 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -8,7 +8,6 @@ import type { ISavedObjectTypeRegistry, SavedObject, - SavedObjectsAddToNamespacesOptions, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -18,15 +17,19 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, SavedObjectsCreateOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsFindResponse, SavedObjectsOpenPointInTimeOptions, SavedObjectsRemoveReferencesToOptions, SavedObjectsRemoveReferencesToResponse, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'src/core/server'; @@ -228,24 +231,6 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon ); } - public async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options?: SavedObjectsAddToNamespacesOptions - ) { - return await this.options.baseClient.addToNamespaces(type, id, namespaces, options); - } - - public async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options?: SavedObjectsDeleteFromNamespacesOptions - ) { - return await this.options.baseClient.deleteFromNamespaces(type, id, namespaces, options); - } - public async removeReferencesTo( type: string, id: string, @@ -265,17 +250,38 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon return await this.options.baseClient.closePointInTime(id, options); } - public createPointInTimeFinder( + public createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies ) { - return this.options.baseClient.createPointInTimeFinder(findOptions, { + return this.options.baseClient.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that subsequent SO client wrappers have their settings applied. ...dependencies, }); } + public async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options?: SavedObjectsCollectMultiNamespaceReferencesOptions + ): Promise { + return await this.options.baseClient.collectMultiNamespaceReferences(objects, options); + } + + public async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options?: SavedObjectsUpdateObjectsSpacesOptions + ) { + return await this.options.baseClient.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + options + ); + } + /** * Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't * registered, response is returned as is. diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx index 912dfe99aa3ed..85d1301fee957 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx @@ -37,13 +37,17 @@ export const JobSpacesList: FC = ({ spacesApi, spaceIds, jobId, jobType, const [showFlyout, setShowFlyout] = useState(false); - async function changeSpacesHandler(spacesToAdd: string[], spacesToRemove: string[]) { - if (spacesToAdd.length) { - const resp = await ml.savedObjects.assignJobToSpace(jobType, [jobId], spacesToAdd); - handleApplySpaces(resp); - } - if (spacesToRemove.length && !spacesToAdd.includes(ALL_SPACES_ID)) { - const resp = await ml.savedObjects.removeJobFromSpace(jobType, [jobId], spacesToRemove); + async function changeSpacesHandler(spacesToAdd: string[], spacesToMaybeRemove: string[]) { + // If the user is adding the job to all current and future spaces, don't remove it from any specified spaces + const spacesToRemove = spacesToAdd.includes(ALL_SPACES_ID) ? [] : spacesToMaybeRemove; + + if (spacesToAdd.length || spacesToRemove.length) { + const resp = await ml.savedObjects.updateJobsSpaces( + jobType, + [jobId], + spacesToAdd, + spacesToRemove + ); handleApplySpaces(resp); } onClose(); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts index 38cbeb486df09..dd2e35f3f7759 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/saved_objects.ts @@ -26,18 +26,15 @@ export const savedObjectsApiProvider = (httpService: HttpService) => ({ method: 'GET', }); }, - assignJobToSpace(jobType: JobType, jobIds: string[], spaces: string[]) { - const body = JSON.stringify({ jobType, jobIds, spaces }); + updateJobsSpaces( + jobType: JobType, + jobIds: string[], + spacesToAdd: string[], + spacesToRemove: string[] + ) { + const body = JSON.stringify({ jobType, jobIds, spacesToAdd, spacesToRemove }); return httpService.http({ - path: `${basePath()}/saved_objects/assign_job_to_space`, - method: 'POST', - body, - }); - }, - removeJobFromSpace(jobType: JobType, jobIds: string[], spaces: string[]) { - const body = JSON.stringify({ jobType, jobIds, spaces }); - return httpService.http({ - path: `${basePath()}/saved_objects/remove_job_from_space`, + path: `${basePath()}/saved_objects/update_jobs_spaces`, method: 'POST', body, }); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 81db7ca15b258..803bd0ae4cb3a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -776,10 +776,11 @@ export class DataRecognizer { this._request ); if (canCreateGlobalJobs === true) { - await this._jobSavedObjectService.assignJobsToSpaces( + await this._jobSavedObjectService.updateJobsSpaces( 'anomaly-detector', jobs.map((j) => j.id), - ['*'] + ['*'], // spacesToAdd + [] // spacesToRemove ); } } diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 7b54e48099d60..16cd3ea8df629 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -147,8 +147,7 @@ "SavedObjectsStatus", "SyncJobSavedObjects", "InitializeJobSavedObjects", - "AssignJobsToSpaces", - "RemoveJobsFromSpaces", + "UpdateJobsSpaces", "RemoveJobsFromCurrentSpace", "JobsSpaces", "CanDeleteJob", diff --git a/x-pack/plugins/ml/server/routes/saved_objects.ts b/x-pack/plugins/ml/server/routes/saved_objects.ts index c93730517cc11..e9fb748a4c7f8 100644 --- a/x-pack/plugins/ml/server/routes/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/saved_objects.ts @@ -126,15 +126,15 @@ export function savedObjectsRoutes( /** * @apiGroup JobSavedObjects * - * @api {post} /api/ml/saved_objects/assign_job_to_space Assign jobs to spaces - * @apiName AssignJobsToSpaces - * @apiDescription Add list of spaces to a list of jobs + * @api {post} /api/ml/saved_objects/update_jobs_spaces Update what spaces jobs are assigned to + * @apiName UpdateJobsSpaces + * @apiDescription Update a list of jobs to add and/or remove them from given spaces * * @apiSchema (body) jobsAndSpaces */ router.post( { - path: '/api/ml/saved_objects/assign_job_to_space', + path: '/api/ml/saved_objects/update_jobs_spaces', validate: { body: jobsAndSpaces, }, @@ -144,43 +144,14 @@ export function savedObjectsRoutes( }, routeGuard.fullLicenseAPIGuard(async ({ request, response, jobSavedObjectService }) => { try { - const { jobType, jobIds, spaces } = request.body; + const { jobType, jobIds, spacesToAdd, spacesToRemove } = request.body; - const body = await jobSavedObjectService.assignJobsToSpaces(jobType, jobIds, spaces); - - return response.ok({ - body, - }); - } catch (e) { - return response.customError(wrapError(e)); - } - }) - ); - - /** - * @apiGroup JobSavedObjects - * - * @api {post} /api/ml/saved_objects/remove_job_from_space Remove jobs from spaces - * @apiName RemoveJobsFromSpaces - * @apiDescription Remove a list of spaces from a list of jobs - * - * @apiSchema (body) jobsAndSpaces - */ - router.post( - { - path: '/api/ml/saved_objects/remove_job_from_space', - validate: { - body: jobsAndSpaces, - }, - options: { - tags: ['access:ml:canCreateJob', 'access:ml:canCreateDataFrameAnalytics'], - }, - }, - routeGuard.fullLicenseAPIGuard(async ({ request, response, jobSavedObjectService }) => { - try { - const { jobType, jobIds, spaces } = request.body; - - const body = await jobSavedObjectService.removeJobsFromSpaces(jobType, jobIds, spaces); + const body = await jobSavedObjectService.updateJobsSpaces( + jobType, + jobIds, + spacesToAdd, + spacesToRemove + ); return response.ok({ body, @@ -227,9 +198,12 @@ export function savedObjectsRoutes( }); } - const body = await jobSavedObjectService.removeJobsFromSpaces(jobType, jobIds, [ - currentSpaceId, - ]); + const body = await jobSavedObjectService.updateJobsSpaces( + jobType, + jobIds, + [], // spacesToAdd + [currentSpaceId] // spacesToRemove + ); return response.ok({ body, diff --git a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts index 85f56c1ffb412..64d0b291772f9 100644 --- a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts @@ -17,7 +17,8 @@ export const jobTypeSchema = schema.object({ jobType: jobTypeLiterals }); export const jobsAndSpaces = schema.object({ jobType: jobTypeLiterals, jobIds: schema.arrayOf(schema.string()), - spaces: schema.arrayOf(schema.string()), + spacesToAdd: schema.arrayOf(schema.string()), + spacesToRemove: schema.arrayOf(schema.string()), }); export const jobsAndCurrentSpace = schema.object({ diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index 7a39f2ed5ebfe..da7d11776ee53 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -301,51 +301,60 @@ export function jobSavedObjectServiceFactory( return filterJobObjectIdsForSpace('anomaly-detector', ids, 'datafeed_id', allowWildcards); } - async function assignJobsToSpaces(jobType: JobType, jobIds: string[], spaces: string[]) { + async function updateJobsSpaces( + jobType: JobType, + jobIds: string[], + spacesToAdd: string[], + spacesToRemove: string[] + ) { const results: Record = {}; const jobs = await _getJobObjects(jobType); - for (const id of jobIds) { - const job = jobs.find((j) => j.attributes.job_id === id); + const jobObjectIdMap = new Map(); + const objectsToUpdate: Array<{ type: string; id: string }> = []; + for (const jobId of jobIds) { + const job = jobs.find((j) => j.attributes.job_id === jobId); if (job === undefined) { - results[id] = { + results[jobId] = { success: false, - error: createError(id, 'job_id'), + error: createError(jobId, 'job_id'), }; } else { - try { - await savedObjectsClient.addToNamespaces(ML_SAVED_OBJECT_TYPE, job.id, spaces); - results[id] = { - success: true, - }; - } catch (error) { - results[id] = { - success: false, - error: getSavedObjectClientError(error), - }; - } + jobObjectIdMap.set(job.id, jobId); + objectsToUpdate.push({ type: ML_SAVED_OBJECT_TYPE, id: job.id }); } } - return results; - } - async function removeJobsFromSpaces(jobType: JobType, jobIds: string[], spaces: string[]) { - const results: Record = {}; - const jobs = await _getJobObjects(jobType); - for (const job of jobs) { - if (jobIds.includes(job.attributes.job_id)) { - try { - await savedObjectsClient.deleteFromNamespaces(ML_SAVED_OBJECT_TYPE, job.id, spaces); - results[job.attributes.job_id] = { - success: true, - }; - } catch (error) { - results[job.attributes.job_id] = { + try { + const updateResult = await savedObjectsClient.updateObjectsSpaces( + objectsToUpdate, + spacesToAdd, + spacesToRemove + ); + updateResult.objects.forEach(({ id: objectId, error }) => { + const jobId = jobObjectIdMap.get(objectId)!; + if (error) { + results[jobId] = { success: false, error: getSavedObjectClientError(error), }; + } else { + results[jobId] = { + success: true, + }; } - } + }); + } catch (error) { + // If the entire operation failed, return success: false for each job + const clientError = getSavedObjectClientError(error); + objectsToUpdate.forEach(({ id: objectId }) => { + const jobId = jobObjectIdMap.get(objectId)!; + results[jobId] = { + success: false, + error: clientError, + }; + }); } + return results; } @@ -372,8 +381,7 @@ export function jobSavedObjectServiceFactory( filterJobIdsForSpace, filterDatafeedsForSpace, filterDatafeedIdsForSpace, - assignJobsToSpaces, - removeJobsFromSpaces, + updateJobsSpaces, bulkCreateJobs, getAllJobObjectsForAllSpaces, canCreateGlobalJobs, diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 70d8149682370..611e7bd456da3 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -142,6 +142,8 @@ export enum SavedObjectAction { REMOVE_REFERENCES = 'saved_object_remove_references', OPEN_POINT_IN_TIME = 'saved_object_open_point_in_time', CLOSE_POINT_IN_TIME = 'saved_object_close_point_in_time', + COLLECT_MULTINAMESPACE_REFERENCES = 'saved_object_collect_multinamespace_references', // this is separate from 'saved_object_get' because the user is only accessing an object's metadata + UPDATE_OBJECTS_SPACES = 'saved_object_update_objects_spaces', // this is separate from 'saved_object_update' because the user is only updating an object's metadata } type VerbsTuple = [string, string, string]; @@ -170,6 +172,16 @@ const savedObjectAuditVerbs: Record = { 'removing references to', 'removed references to', ], + saved_object_collect_multinamespace_references: [ + 'collect references and spaces of', + 'collecting references and spaces of', + 'collected references and spaces of', + ], + saved_object_update_objects_spaces: [ + 'update spaces of', + 'updating spaces of', + 'updated spaces of', + ], }; const savedObjectAuditTypes: Record = { @@ -184,6 +196,8 @@ const savedObjectAuditTypes: Record = { saved_object_open_point_in_time: 'creation', saved_object_close_point_in_time: 'deletion', saved_object_remove_references: 'change', + saved_object_collect_multinamespace_references: 'access', + saved_object_update_objects_spaces: 'change', }; export interface SavedObjectEventParams { diff --git a/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts b/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts new file mode 100644 index 0000000000000..531b547a1f275 --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/ensure_authorized.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'src/core/server'; + +import type { CheckSavedObjectsPrivileges } from '../authorization'; +import { Actions } from '../authorization'; +import type { CheckPrivilegesResponse } from '../authorization/types'; +import type { EnsureAuthorizedResult } from './ensure_authorized'; +import { ensureAuthorized, getEnsureAuthorizedActionResult } from './ensure_authorized'; + +describe('ensureAuthorized', () => { + function setupDependencies() { + const actions = new Actions('some-version'); + jest + .spyOn(actions.savedObject, 'get') + .mockImplementation((type: string, action: string) => `mock-saved_object:${type}/${action}`); + const errors = ({ + decorateForbiddenError: jest.fn().mockImplementation((err) => err), + decorateGeneralError: jest.fn().mockImplementation((err) => err), + } as unknown) as jest.Mocked; + const checkSavedObjectsPrivilegesAsCurrentUser: jest.MockedFunction = jest.fn(); + return { actions, errors, checkSavedObjectsPrivilegesAsCurrentUser }; + } + + // These arguments are used for all unit tests below + const types = ['a', 'b', 'c']; + const actions = ['foo', 'bar']; + const namespaces = ['x', 'y']; + + const mockAuthorizedResolvedPrivileges = { + hasAllRequested: true, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, + ], + }, + } as CheckPrivilegesResponse; + + test('calls checkSavedObjectsPrivilegesAsCurrentUser with expected privilege actions and namespaces', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue( + mockAuthorizedResolvedPrivileges + ); + await ensureAuthorized(deps, types, actions, namespaces); + expect(deps.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( + [ + 'mock-saved_object:a/foo', + 'mock-saved_object:a/bar', + 'mock-saved_object:b/foo', + 'mock-saved_object:b/bar', + 'mock-saved_object:c/foo', + 'mock-saved_object:c/bar', + ], + namespaces + ); + }); + + test('throws an error when privilege check fails', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error('Oh no!')); + expect(ensureAuthorized(deps, [], [], [])).rejects.toThrowError('Oh no!'); + }); + + describe('fully authorized', () => { + const expectedResult = { + status: 'fully_authorized', + typeActionMap: new Map([ + [ + 'a', + { + foo: { isGloballyAuthorized: true, authorizedSpaces: [] }, + bar: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }, + ], + ['b', { foo: { authorizedSpaces: ['x', 'y'] }, bar: { authorizedSpaces: ['x', 'y'] } }], + ['c', { foo: { authorizedSpaces: ['x', 'y'] }, bar: { authorizedSpaces: ['x', 'y'] } }], + ]), + }; + + test('with default options', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue( + mockAuthorizedResolvedPrivileges + ); + const result = await ensureAuthorized(deps, types, actions, namespaces); + expect(result).toEqual(expectedResult); + }); + + test('with requireFullAuthorization=false', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue( + mockAuthorizedResolvedPrivileges + ); + const options = { requireFullAuthorization: false }; + const result = await ensureAuthorized(deps, types, actions, namespaces, options); + expect(result).toEqual(expectedResult); + }); + }); + + describe('partially authorized', () => { + const resolvedPrivileges = { + hasAllRequested: false, + privileges: { + kibana: [ + // For type 'a', the user is authorized to use 'foo' action but not 'bar' action (all spaces) + // For type 'b', the user is authorized to use 'foo' action but not 'bar' action (both spaces) + // For type 'c', the user is authorized to use both actions in space 'x' but not space 'y' + { privilege: 'mock-saved_object:a/foo', authorized: true }, + { privilege: 'mock-saved_object:a/bar', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: true }, + { privilege: 'mock-saved_object:c/foo', authorized: false }, // inverse fail-secure check + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: true }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, + { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) + ], + }, + } as CheckPrivilegesResponse; + + test('with default options', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + expect( + ensureAuthorized(deps, types, actions, namespaces) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unable to (bar a),(bar b),(bar c),(foo c)"`); + }); + + test('with requireFullAuthorization=false', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + const options = { requireFullAuthorization: false }; + const result = await ensureAuthorized(deps, types, actions, namespaces, options); + expect(result).toEqual({ + status: 'partially_authorized', + typeActionMap: new Map([ + ['a', { foo: { isGloballyAuthorized: true, authorizedSpaces: [] } }], + ['b', { foo: { authorizedSpaces: ['x', 'y'] } }], + ['c', { foo: { authorizedSpaces: ['x'] }, bar: { authorizedSpaces: ['x'] } }], + ]), + }); + }); + }); + + describe('unauthorized', () => { + const resolvedPrivileges = { + hasAllRequested: false, + privileges: { + kibana: [ + { privilege: 'mock-saved_object:a/foo', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: false }, + { privilege: 'mock-saved_object:a/bar', authorized: true }, // fail-secure check + { resource: 'x', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'x', privilege: 'mock-saved_object:c/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:a/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:a/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:b/bar', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/foo', authorized: false }, + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: false }, + { privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + { resource: 'y', privilege: 'mock-saved_object:c/bar', authorized: true }, // fail-secure check + // The fail-secure checks are a contrived scenario, as we *shouldn't* get both an unauthorized and authorized result for a given resource... + // However, in case we do, we should fail-secure (authorized + unauthorized = unauthorized) + ], + }, + } as CheckPrivilegesResponse; + + test('with default options', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + expect( + ensureAuthorized(deps, types, actions, namespaces) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to (bar a),(bar b),(bar c),(foo a),(foo b),(foo c)"` + ); + }); + + test('with requireFullAuthorization=false', async () => { + const deps = setupDependencies(); + deps.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue(resolvedPrivileges); + const options = { requireFullAuthorization: false }; + const result = await ensureAuthorized(deps, types, actions, namespaces, options); + expect(result).toEqual({ status: 'unauthorized', typeActionMap: new Map() }); + }); + }); +}); + +describe('getEnsureAuthorizedActionResult', () => { + const typeActionMap: EnsureAuthorizedResult<'action'>['typeActionMap'] = new Map([ + ['type', { action: { authorizedSpaces: ['space-id'] } }], + ]); + + test('returns the appropriate result if it is in the typeActionMap', () => { + const result = getEnsureAuthorizedActionResult('type', 'action', typeActionMap); + expect(result).toEqual({ authorizedSpaces: ['space-id'] }); + }); + + test('returns an unauthorized result if it is not in the typeActionMap', () => { + const result = getEnsureAuthorizedActionResult('other-type', 'action', typeActionMap); + expect(result).toEqual({ authorizedSpaces: [] }); + }); +}); diff --git a/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts b/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts new file mode 100644 index 0000000000000..0ce7b5f78f13b --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/ensure_authorized.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'src/core/server'; + +import type { Actions, CheckSavedObjectsPrivileges } from '../authorization'; +import type { CheckPrivilegesResponse } from '../authorization/types'; + +export interface EnsureAuthorizedDependencies { + actions: Actions; + errors: SavedObjectsClientContract['errors']; + checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; +} + +export interface EnsureAuthorizedOptions { + /** Whether or not to throw an error if the user is not fully authorized. Default is true. */ + requireFullAuthorization?: boolean; +} + +export interface EnsureAuthorizedResult { + status: 'fully_authorized' | 'partially_authorized' | 'unauthorized'; + typeActionMap: Map>; +} + +export interface EnsureAuthorizedActionResult { + authorizedSpaces: string[]; + isGloballyAuthorized?: boolean; +} + +/** + * Checks to ensure a user is authorized to access object types in given spaces. + * + * @param {EnsureAuthorizedDependencies} deps the dependencies needed to make the privilege checks. + * @param {string[]} types the type(s) to check privileges for. + * @param {T[]} actions the action(s) to check privileges for. + * @param {string[]} spaceIds the id(s) of spaces to check privileges for. + * @param {EnsureAuthorizedOptions} options the options to use. + */ +export async function ensureAuthorized( + deps: EnsureAuthorizedDependencies, + types: string[], + actions: T[], + spaceIds: string[], + options: EnsureAuthorizedOptions = {} +): Promise> { + const { requireFullAuthorization = true } = options; + const privilegeActionsMap = new Map( + types.flatMap((type) => + actions.map((action) => [deps.actions.savedObject.get(type, action), { type, action }]) + ) + ); + const privilegeActions = Array.from(privilegeActionsMap.keys()); + const { hasAllRequested, privileges } = await checkPrivileges(deps, privilegeActions, spaceIds); + + const missingPrivileges = getMissingPrivileges(privileges); + const typeActionMap = privileges.kibana.reduce< + Map> + >((acc, { resource, privilege }) => { + const missingPrivilegesAtResource = + (resource && missingPrivileges.get(resource)?.has(privilege)) || + (!resource && missingPrivileges.get(undefined)?.has(privilege)); + + if (missingPrivilegesAtResource) { + return acc; + } + const { type, action } = privilegeActionsMap.get(privilege)!; // always defined + const actionAuthorizations = acc.get(type) ?? ({} as Record); + const authorization: EnsureAuthorizedActionResult = actionAuthorizations[action] ?? { + authorizedSpaces: [], + }; + + if (resource === undefined) { + return acc.set(type, { + ...actionAuthorizations, + [action]: { ...authorization, isGloballyAuthorized: true }, + }); + } + + return acc.set(type, { + ...actionAuthorizations, + [action]: { + ...authorization, + authorizedSpaces: authorization.authorizedSpaces.concat(resource), + }, + }); + }, new Map()); + + if (hasAllRequested) { + return { typeActionMap, status: 'fully_authorized' }; + } + + if (!requireFullAuthorization) { + const isPartiallyAuthorized = typeActionMap.size > 0; + if (isPartiallyAuthorized) { + return { typeActionMap, status: 'partially_authorized' }; + } else { + return { typeActionMap, status: 'unauthorized' }; + } + } + + // Neither fully nor partially authorized. Bail with error. + const uniqueUnauthorizedPrivileges = [...missingPrivileges.entries()].reduce( + (acc, [, privilegeSet]) => new Set([...acc, ...privilegeSet]), + new Set() + ); + const targetTypesAndActions = [...uniqueUnauthorizedPrivileges] + .map((privilege) => { + const { type, action } = privilegeActionsMap.get(privilege)!; + return `(${action} ${type})`; + }) + .sort() + .join(','); + const msg = `Unable to ${targetTypesAndActions}`; + throw deps.errors.decorateForbiddenError(new Error(msg)); +} + +/** + * Helper function that, given an `EnsureAuthorizedResult`, checks to see what spaces the user is authorized to perform a given action for + * the given object type. + * + * @param {string} objectType the object type to check. + * @param {T} action the action to check. + * @param {EnsureAuthorizedResult['typeActionMap']} typeActionMap the typeActionMap from an EnsureAuthorizedResult. + */ +export function getEnsureAuthorizedActionResult( + objectType: string, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'] +): EnsureAuthorizedActionResult { + const record = typeActionMap.get(objectType) ?? ({} as Record); + return record[action] ?? { authorizedSpaces: [] }; +} + +async function checkPrivileges( + deps: EnsureAuthorizedDependencies, + actions: string | string[], + namespaceOrNamespaces?: string | Array +) { + try { + return await deps.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces); + } catch (error) { + throw deps.errors.decorateGeneralError(error, error.body && error.body.reason); + } +} + +function getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { + return privileges.kibana.reduce>>( + (acc, { resource, privilege, authorized }) => { + if (!authorized) { + if (resource) { + acc.set(resource, (acc.get(resource) || new Set()).add(privilege)); + } + // Fail-secure: if a user is not authorized for a specific resource, they are not authorized for the global resource too (global resource is undefined) + // The inverse is not true; if a user is not authorized for the global resource, they may still be authorized for a specific resource + acc.set(undefined, (acc.get(undefined) || new Set()).add(privilege)); + } + return acc; + }, + new Map() + ); +} diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts new file mode 100644 index 0000000000000..9e772f5394cc2 --- /dev/null +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ensureAuthorized } from './ensure_authorized'; + +export const mockEnsureAuthorized = jest.fn() as jest.MockedFunction; + +jest.mock('./ensure_authorized', () => { + return { + ...jest.requireActual('./ensure_authorized'), + ensureAuthorized: mockEnsureAuthorized, + }; +}); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 2658f4edec5ac..e5a2340aba3f0 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -5,7 +5,15 @@ * 2.0. */ -import type { EcsEventOutcome, SavedObjectsClientContract } from 'src/core/server'; +import { mockEnsureAuthorized } from './secure_saved_objects_client_wrapper.test.mocks'; + +import type { + EcsEventOutcome, + SavedObject, + SavedObjectReferenceWithContext, + SavedObjectsClientContract, + SavedObjectsUpdateObjectsSpacesResponseObject, +} from 'src/core/server'; import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { AuditEvent } from '../audit'; @@ -20,6 +28,7 @@ jest.mock('src/core/server/saved_objects/service/lib/utils', () => { ); return { SavedObjectsUtils: { + ...SavedObjectsUtils, createEmptyFindResponse: SavedObjectsUtils.createEmptyFindResponse, generateId: () => 'mock-saved-object-id', }, @@ -179,8 +188,6 @@ const expectObjectNamespaceFiltering = async ( clientOpts.baseClient.get.mockReturnValue(returnValue as any); // 'resolve' is excluded because it has a specific test case written for it clientOpts.baseClient.update.mockReturnValue(returnValue as any); - clientOpts.baseClient.addToNamespaces.mockReturnValue(returnValue as any); - clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(returnValue as any); const result = await fn.bind(client)(...Object.values(args)); // we will never redact the "All Spaces" ID @@ -210,7 +217,7 @@ const expectAuditEvent = ( }), kibana: savedObject ? expect.objectContaining({ - saved_object: savedObject, + saved_object: { type: savedObject.type, id: savedObject.id }, }) : expect.anything(), }) @@ -313,146 +320,12 @@ beforeEach(() => { clientOpts = createSecureSavedObjectsClientWrapperOptions(); client = new SecureSavedObjectsClientWrapper(clientOpts); - // succeed privilege checks by default + // succeed legacyEnsureAuthorized privilege checks by default clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesSuccess ); -}); - -describe('#addToNamespaces', () => { - const type = 'foo'; - const id = `${type}-id`; - const newNs1 = 'foo-namespace'; - const newNs2 = 'bar-namespace'; - const namespaces = [newNs1, newNs2]; - const currentNs = 'default'; - const privilege = `mock-saved_object:${type}/share_to_space`; - - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - await expectGeneralError(client.addToNamespaces, { type, id, namespaces }); - }); - - test(`throws decorated ForbiddenError when unauthorized to create in new space`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure - ); - - await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( - clientOpts.forbiddenError - ); - - expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect( - clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure - ).toHaveBeenCalledWith( - USERNAME, - 'addToNamespacesCreate', - [type], - namespaces.sort(), - [{ privilege, spaceId: newNs1 }], - { id, type, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( - getMockCheckPrivilegesSuccess // create - ); - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure // update - ); - - await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrowError( - clientOpts.forbiddenError - ); - expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect( - clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure - ).toHaveBeenLastCalledWith( - USERNAME, - 'addToNamespacesUpdate', - [type], - [currentNs], - [{ privilege, spaceId: currentNs }], - { id, type, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - }); - - test(`returns result of baseClient.addToNamespaces when authorized`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); - - const result = await client.addToNamespaces(type, id, namespaces); - expect(result).toBe(apiCallReturnValue); - - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( - 1, - USERNAME, - 'addToNamespacesCreate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesCreate' - [type], - namespaces.sort(), - { type, id, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( - 2, - USERNAME, - 'addToNamespacesUpdate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesUpdate' - [type], - [currentNs], - { type, id, namespaces, options: {} } - ); - }); - - test(`checks privileges for user, actions, and namespaces`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( - getMockCheckPrivilegesSuccess // create - ); - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure // update - ); - - await expect(client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case - - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(2); - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( - 1, - [privilege], - namespaces - ); - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenNthCalledWith( - 2, - [privilege], - undefined // default namespace - ); - }); - - test(`filters namespaces that the user doesn't have access to`, async () => { - // this operation is unique because it requires two privilege checks before it executes - await expectObjectNamespaceFiltering(client.addToNamespaces, { type, id, namespaces }, 2); - }); - - test(`adds audit event when successful`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); - await client.addToNamespaces(type, id, namespaces); - - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_add_to_spaces', 'unknown', { type, id }); - }); - - test(`adds audit event when not successful`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); - await expect(() => client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_add_to_spaces', 'failure', { type, id }); - }); + mockEnsureAuthorized.mockReset(); }); describe('#bulkCreate', () => { @@ -1163,92 +1036,6 @@ describe('#resolve', () => { }); }); -describe('#deleteFromNamespaces', () => { - const type = 'foo'; - const id = `${type}-id`; - const namespace1 = 'default'; - const namespace2 = 'another-namespace'; - const namespaces = [namespace1, namespace2]; - const privilege = `mock-saved_object:${type}/share_to_space`; - - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - await expectGeneralError(client.deleteFromNamespaces, { type, id, namespaces }); - }); - - test(`throws decorated ForbiddenError when unauthorized`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure - ); - - await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrowError( - clientOpts.forbiddenError - ); - - expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( - USERNAME, - 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' - [type], - namespaces.sort(), - [{ privilege, spaceId: namespace1 }], - { type, id, namespaces, options: {} } - ); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); - }); - - test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); - - const result = await client.deleteFromNamespaces(type, id, namespaces); - expect(result).toBe(apiCallReturnValue); - - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( - USERNAME, - 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' - [type], - namespaces.sort(), - { type, id, namespaces, options: {} } - ); - }); - - test(`checks privileges for user, actions, and namespace`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( - getMockCheckPrivilegesFailure - ); - - await expect(client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); // test is simpler with error case - - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); - expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( - [privilege], - namespaces - ); - }); - - test(`filters namespaces that the user doesn't have access to`, async () => { - await expectObjectNamespaceFiltering(client.deleteFromNamespaces, { type, id, namespaces }); - }); - - test(`adds audit event when successful`, async () => { - const apiCallReturnValue = Symbol(); - clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); - await client.deleteFromNamespaces(type, id, namespaces); - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_delete_from_spaces', 'unknown', { type, id }); - }); - - test(`adds audit event when not successful`, async () => { - clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); - await expect(() => client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); - expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); - expectAuditEvent('saved_object_delete_from_spaces', 'failure', { type, id }); - }); -}); - describe('#update', () => { const type = 'foo'; const id = `${type}-id`; @@ -1351,6 +1138,583 @@ describe('#removeReferencesTo', () => { }); }); +/** + * Naming conventions used in this group of tests: + * * 'reqObj' is an object that the consumer requests (SavedObjectsCollectMultiNamespaceReferencesObject) + * * 'obj' is the object result that was fetched from Elasticsearch (SavedObjectReferenceWithContext) + */ +describe('#collectMultiNamespaceReferences', () => { + const AUDIT_ACTION = 'saved_object_collect_multinamespace_references'; + const spaceX = 'space-x'; + const spaceY = 'space-y'; + const spaceZ = 'space-z'; + + /** Returns a valid inboundReferences field for mock baseClient results. */ + function getInboundRefsFrom( + ...objects: Array<{ type: string; id: string }> + ): Pick { + return { + inboundReferences: objects.map(({ type, id }) => { + return { type, id, name: `ref-${type}:${id}` }; + }), + }; + } + + beforeEach(() => { + // by default, the result is a success, each object exists in the current space and another space + clientOpts.baseClient.collectMultiNamespaceReferences.mockImplementation((objects) => + Promise.resolve({ + objects: objects.map(({ type, id }) => ({ + type, + id, + spaces: [spaceX, spaceY, spaceZ], + inboundReferences: [], + })), + }) + ); + }); + + describe('errors', () => { + const reqObj1 = { type: 'a', id: '1' }; + const reqObj2 = { type: 'b', id: '2' }; + const reqObj3 = { type: 'c', id: '3' }; + + test(`throws an error if the base client operation fails`, async () => { + clientOpts.baseClient.collectMultiNamespaceReferences.mockRejectedValue(new Error('Oh no!')); + await expect(() => + client.collectMultiNamespaceReferences([reqObj1], { namespace: spaceX }) + ).rejects.toThrowError('Oh no!'); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.log).not.toHaveBeenCalled(); + }); + + describe(`throws decorated ForbiddenError and adds audit events when unauthorized`, () => { + test(`with purpose 'collectMultiNamespaceReferences'`, async () => { + // Use the default mocked results for the base client call. + // This fails because the user is not authorized to bulk_get type 'c' in the current space. + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] } }) + .set('b', { bulk_get: { authorizedSpaces: [spaceX, spaceY] } }) + .set('c', { bulk_get: { authorizedSpaces: [spaceY] } }), + }); + const options = { namespace: spaceX }; // spaceX is the current space + await expect(() => + client.collectMultiNamespaceReferences([reqObj1, reqObj2, reqObj3], options) + ).rejects.toThrowError(clientOpts.forbiddenError); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj1); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj2); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj3); + }); + + test(`with purpose 'updateObjectsSpaces'`, async () => { + // Use the default mocked results for the base client call. + // This fails because the user is not authorized to share_to_space type 'c' in the current space. + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('b', { + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + share_to_space: { authorizedSpaces: [spaceX, spaceY] }, + }) + .set('c', { + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + share_to_space: { authorizedSpaces: [spaceY] }, + }), + }); + const options = { namespace: spaceX, purpose: 'updateObjectsSpaces' as const }; // spaceX is the current space + await expect(() => + client.collectMultiNamespaceReferences([reqObj1, reqObj2, reqObj3], options) + ).rejects.toThrowError(clientOpts.forbiddenError); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj1); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj2); + expectAuditEvent(AUDIT_ACTION, 'failure', reqObj3); + }); + }); + + test(`throws an error if the base client result includes a requested object without a valid inbound reference`, async () => { + // We *shouldn't* ever get an inbound reference that is not also present in the base client response objects array. + const spaces = [spaceX]; + + const obj1 = { ...reqObj1, spaces, inboundReferences: [] }; + const obj2 = { + type: 'a', + id: '2', + spaces, + ...getInboundRefsFrom({ type: 'some-type', id: 'some-id' }), + }; + clientOpts.baseClient.collectMultiNamespaceReferences.mockResolvedValueOnce({ + objects: [obj1, obj2], + }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map().set('a', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + // When the loop gets to obj2, it will determine that the user is authorized for the object but *not* for the graph. However, it will + // also determine that there is *no* valid inbound reference tying this object back to what was requested. In this case, throw an + // error. + + const options = { namespace: spaceX }; // spaceX is the current space + await expect(() => + client.collectMultiNamespaceReferences([reqObj1], options) + ).rejects.toThrowError('Unexpected inbound reference to "some-type:some-id"'); + }); + }); + + describe(`checks privileges`, () => { + // Other test cases below contain more complex assertions for privilege checks, but these focus on the current space (default vs non-default) + const reqObj1 = { type: 'a', id: '1' }; + const obj1 = { ...reqObj1, spaces: ['*'], inboundReferences: [] }; + + beforeEach(() => { + clientOpts.baseClient.collectMultiNamespaceReferences.mockResolvedValueOnce({ + objects: [obj1], + }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'fully_authorized', + typeActionMap: new Map().set('a', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, // success case for the simplest test + }), + }); + }); + + test(`in the default space`, async () => { + await client.collectMultiNamespaceReferences([reqObj1]); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a'], // unique types of the fetched objects + ['bulk_get'], // actions + ['default'], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + }); + + test(`in a non-default space`, async () => { + await client.collectMultiNamespaceReferences([reqObj1], { namespace: spaceX }); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a'], // unique types of the fetched objects + ['bulk_get'], // actions + [spaceX], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + }); + }); + + describe(`checks privileges, filters/redacts objects correctly, and records audit events`, () => { + const reqObj1 = { type: 'a', id: '1' }; + const reqObj2 = { type: 'b', id: '2' }; + const spaces = [spaceX, spaceY, spaceZ]; + + // Actual object graph: + // ─► obj1 (a:1) ─┬─► obj3 (c:3) ───► obj5 (c:5) ─► obj8 (c:8) ─┐ + // │ ▲ │ + // │ │ │ + // └─► obj4 (d:4) ─┬─► obj6 (c:6) ◄──────────────┘ + // ─► obj2 (b:2) └─► obj7 (c:7) + // + // Object graph that the consumer sees after authorization: + // ─► obj1 (a:1) ─┬─► obj3 (c:3) ───► obj5 (c:5) ─► obj8 (c:8) ─► obj6 (c:6) ─┐ + // │ ▲ │ + // │ └───────────────────────────────────┘ + // └─► obj4 (d:4) + // ─► obj2 (b:2) + const obj1 = { ...reqObj1, spaces, inboundReferences: [] }; + const obj2 = { ...reqObj2, spaces: [], inboundReferences: [] }; // non-multi-namespace types and hidden types will be returned with an empty spaces array + const obj3 = { type: 'c', id: '3', spaces, ...getInboundRefsFrom(obj1) }; + const obj4 = { type: 'd', id: '4', spaces, ...getInboundRefsFrom(obj1) }; + const obj5 = { + type: 'c', + id: '5', + spaces: ['*'], + ...getInboundRefsFrom(obj3, { type: 'c', id: '6' }), + }; + const obj6 = { + type: 'c', + id: '6', + spaces, + ...getInboundRefsFrom(obj4, { type: 'c', id: '8' }), + }; + const obj7 = { type: 'c', id: '7', spaces, ...getInboundRefsFrom(obj4) }; + const obj8 = { type: 'c', id: '8', spaces, ...getInboundRefsFrom(obj5) }; + + beforeEach(() => { + clientOpts.baseClient.collectMultiNamespaceReferences.mockResolvedValueOnce({ + objects: [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8], + }); + }); + + test(`with purpose 'collectMultiNamespaceReferences'`, async () => { + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] } }) + .set('b', { bulk_get: { authorizedSpaces: [spaceX] } }) + .set('c', { bulk_get: { authorizedSpaces: [spaceX] } }), + // the user is not authorized to read type 'd' + }); + + const options = { namespace: spaceX }; // spaceX is the current space + const result = await client.collectMultiNamespaceReferences([reqObj1, reqObj2], options); + expect(result).toEqual({ + objects: [ + obj1, // obj1's spaces array is not redacted because the user is globally authorized to access it + obj2, // obj2 has an empty spaces array (see above) + { ...obj3, spaces: [spaceX, '?', '?'] }, + { ...obj4, spaces: [], isMissing: true }, // obj4 is marked as Missing because the user was not authorized to access it + obj5, // obj5's spaces array is not redacted, because it exists in All Spaces + // obj7 is not included at all because the user was not authorized to access its inbound reference (obj4) + { ...obj8, spaces: [spaceX, '?', '?'] }, + { ...obj6, spaces: [spaceX, '?', '?'], ...getInboundRefsFrom(obj8) }, // obj6 is at the back of the list and its inboundReferences array is redacted because the user is not authorized to access one of its inbound references, obj4 + ], + }); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith( + [reqObj1, reqObj2], + options + ); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a', 'b', 'c', 'd'], // unique types of the fetched objects + ['bulk_get'], // actions + [spaceX, spaceY, spaceZ], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(5); + expectAuditEvent(AUDIT_ACTION, 'success', obj1); + expectAuditEvent(AUDIT_ACTION, 'success', obj3); + expectAuditEvent(AUDIT_ACTION, 'success', obj5); + expectAuditEvent(AUDIT_ACTION, 'success', obj8); + expectAuditEvent(AUDIT_ACTION, 'success', obj6); + // obj2, obj4, and obj7 are intentionally excluded from the audit record because we did not return any information about them to the user + }); + + test(`with purpose 'updateObjectsSpaces'`, async () => { + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('a', { + share_to_space: { authorizedSpaces: [spaceX] }, + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + // Even though the user can only share type 'a' in spaceX, we won't redact spaceY or spaceZ because the user has global read privileges + }) + .set('b', { + share_to_space: { authorizedSpaces: [spaceX] }, + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + }) + .set('c', { + share_to_space: { authorizedSpaces: [spaceX] }, + bulk_get: { authorizedSpaces: [spaceX, spaceY] }, + // Even though the user can only share type 'c' in spaceX, we won't redact spaceY because the user has read privileges there + }), + // the user is not authorized to read or share type 'd' + }); + + const options = { namespace: spaceX, purpose: 'updateObjectsSpaces' as const }; // spaceX is the current space + const result = await client.collectMultiNamespaceReferences([reqObj1, reqObj2], options); + expect(result).toEqual({ + objects: [ + obj1, // obj1's spaces array is not redacted because the user is globally authorized to access it + obj2, // obj2 has an empty spaces array (see above) + { ...obj3, spaces: [spaceX, spaceY, '?'] }, + { ...obj4, spaces: [], isMissing: true }, // obj4 is marked as Missing because the user was not authorized to access it + obj5, // obj5's spaces array is not redacted, because it exists in All Spaces + // obj7 is not included at all because the user was not authorized to access its inbound reference (obj4) + { ...obj8, spaces: [spaceX, spaceY, '?'] }, + { ...obj6, spaces: [spaceX, spaceY, '?'], ...getInboundRefsFrom(obj8) }, // obj6 is at the back of the list and its inboundReferences array is redacted because the user is not authorized to access one of its inbound references, obj4 + ], + }); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith( + [reqObj1, reqObj2], + options + ); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['a', 'b', 'c', 'd'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceX, spaceY, spaceZ], // unique spaces that the fetched objects exist in, along with the current space + { requireFullAuthorization: false } + ); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(5); + expectAuditEvent(AUDIT_ACTION, 'success', obj1); + expectAuditEvent(AUDIT_ACTION, 'success', obj3); + expectAuditEvent(AUDIT_ACTION, 'success', obj5); + expectAuditEvent(AUDIT_ACTION, 'success', obj8); + expectAuditEvent(AUDIT_ACTION, 'success', obj6); + // obj2, obj4, and obj7 are intentionally excluded from the audit record because we did not return any information about them to the user + }); + }); +}); + +describe('#updateObjectsSpaces', () => { + const AUDIT_ACTION = 'saved_object_update_objects_spaces'; + const spaceA = 'space-a'; + const spaceB = 'space-b'; + const spaceC = 'space-c'; + const spaceD = 'space-d'; + const obj1 = { type: 'x', id: '1' }; + const obj2 = { type: 'y', id: '2' }; + const obj3 = { type: 'z', id: '3' }; + const obj4 = { type: 'z', id: '4' }; + const obj5 = { type: 'z', id: '5' }; + + describe('errors', () => { + test(`throws an error if the base client bulkGet operation fails`, async () => { + clientOpts.baseClient.bulkGet.mockRejectedValue(new Error('Oh no!')); + await expect(() => + client.updateObjectsSpaces([obj1], [spaceA], [spaceB], { namespace: spaceC }) + ).rejects.toThrowError('Oh no!'); + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.log).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError and adds audit events when unauthorized`, async () => { + clientOpts.baseClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { ...obj1, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj2, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj3, namespaces: [spaceB, spaceC, spaceD] }, + ] as SavedObject[], + }); + // This fails because the user is not authorized to share_to_space type 'z' in the current space. + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('y', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + }) + .set('z', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB] }, + }), + }); + + const objects = [obj1, obj2, obj3]; + const spacesToAdd = [spaceA]; + const spacesToRemove = [spaceB]; + const options = { namespace: spaceC }; // spaceC is the current space + await expect(() => + client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).rejects.toThrowError(clientOpts.forbiddenError); + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'failure', obj1); + expectAuditEvent(AUDIT_ACTION, 'failure', obj2); + expectAuditEvent(AUDIT_ACTION, 'failure', obj3); + expect(clientOpts.baseClient.updateObjectsSpaces).not.toHaveBeenCalled(); + }); + + test(`throws an error if the base client updateObjectsSpaces operation fails`, async () => { + clientOpts.baseClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { ...obj1, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj2, namespaces: [spaceB, spaceC, spaceD] }, + { ...obj3, namespaces: [spaceB, spaceC, spaceD] }, + ] as SavedObject[], + }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('y', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }) + .set('z', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockRejectedValue(new Error('Oh no!')); + + const objects = [obj1, obj2, obj3]; + const spacesToAdd = [spaceA]; + const spacesToRemove = [spaceB]; + const options = { namespace: spaceC }; // spaceC is the current space + await expect(() => + client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options) + ).rejects.toThrowError('Oh no!'); + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(3); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj1); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj2); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj3); + expect(clientOpts.baseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + }); + }); + + test(`checks privileges, filters/redacts objects correctly, and records audit events`, async () => { + const bulkGetResults = [ + { ...obj1, namespaces: [spaceB, spaceC, spaceD], version: 'v1' }, + { ...obj2, namespaces: [spaceB, spaceC, spaceD], version: 'v2' }, + { ...obj3, namespaces: [spaceB, spaceC, spaceD], version: 'v3' }, + { ...obj4, namespaces: ['*'], version: 'v4' }, // obj4 exists in all spaces + { ...obj5, namespaces: [spaceB, spaceC, spaceD], version: 'v5' }, + ] as SavedObject[]; + clientOpts.baseClient.bulkGet.mockResolvedValue({ saved_objects: bulkGetResults }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'partially_authorized', + typeActionMap: new Map() + .set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }) + .set('y', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC, spaceD] }, + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }) + .set('z', { + bulk_get: { authorizedSpaces: [spaceA, spaceB, spaceC] }, // the user is not authorized to bulkGet type 'z' in spaceD, so it will be redacted from the results + share_to_space: { authorizedSpaces: [spaceA, spaceB, spaceC] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockResolvedValue({ + objects: [ + // Each object was added to spaceA and removed from spaceB + { ...obj1, spaces: [spaceA, spaceC, spaceD] }, + { ...obj2, spaces: [spaceA, spaceC, spaceD] }, + { ...obj3, spaces: [spaceA, spaceC, spaceD] }, + { ...obj4, spaces: ['*', spaceA] }, // even though this object exists in all spaces, we won't pass '*' to ensureAuthorized + { ...obj5, spaces: [], error: new Error('Oh no!') }, // we encountered an error when attempting to update obj5 + ] as SavedObjectsUpdateObjectsSpacesResponseObject[], + }); + + const objects = [obj1, obj2, obj3, obj4, obj5]; + const spacesToAdd = [spaceA]; + const spacesToRemove = [spaceB]; + const options = { namespace: spaceC }; // spaceC is the current space + const result = await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + expect(result).toEqual({ + objects: [ + { ...obj1, spaces: [spaceA, spaceC, spaceD] }, // obj1's spaces array is not redacted because the user is globally authorized to access it + { ...obj2, spaces: [spaceA, spaceC, spaceD] }, // obj2's spaces array is not redacted because the user is authorized to access it in each space + { ...obj3, spaces: [spaceA, spaceC, '?'] }, // obj3's spaces array is redacted because the user is not authorized to access it in spaceD + { ...obj4, spaces: ['*', spaceA] }, + { ...obj5, spaces: [], error: new Error('Oh no!') }, + ], + }); + + expect(clientOpts.baseClient.bulkGet).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['x', 'y', 'z'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceC, spaceA, spaceB, spaceD], // unique spaces of: the current space, spacesToAdd, spacesToRemove, and spaces that the fetched objects exist in (excludes '*') + { requireFullAuthorization: false } + ); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(5); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj1); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj2); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj3); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj4); + expectAuditEvent(AUDIT_ACTION, 'unknown', obj5); + expect(clientOpts.baseClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(clientOpts.baseClient.updateObjectsSpaces).toHaveBeenCalledWith( + bulkGetResults.map(({ namespaces: spaces, ...otherAttrs }) => ({ spaces, ...otherAttrs })), + spacesToAdd, + spacesToRemove, + options + ); + }); + + test(`checks privileges for the global resource when spacesToAdd includes '*'`, async () => { + const bulkGetResults = [{ ...obj1, namespaces: [spaceA], version: 'v1' }] as SavedObject[]; + clientOpts.baseClient.bulkGet.mockResolvedValue({ saved_objects: bulkGetResults }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'fully_authorized', + typeActionMap: new Map().set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockResolvedValue({ + objects: [ + // The object was removed from spaceA and added to '*' + { ...obj1, spaces: ['*'] }, + ] as SavedObjectsUpdateObjectsSpacesResponseObject[], + }); + + const objects = [obj1]; + const spacesToAdd = ['*']; + const spacesToRemove = [spaceA]; + const options = { namespace: spaceC }; // spaceC is the current space + await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['x'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceC, '*', spaceA], // unique spaces of: the current space, spacesToAdd, spacesToRemove, and spaces that the fetched objects exist in (excludes '*') + { requireFullAuthorization: false } + ); + }); + + test(`checks privileges for the global resource when spacesToRemove includes '*'`, async () => { + const bulkGetResults = [{ ...obj1, namespaces: ['*'], version: 'v1' }] as SavedObject[]; + clientOpts.baseClient.bulkGet.mockResolvedValue({ saved_objects: bulkGetResults }); + mockEnsureAuthorized.mockResolvedValue({ + status: 'fully_authorized', + typeActionMap: new Map().set('x', { + bulk_get: { isGloballyAuthorized: true, authorizedSpaces: [] }, + share_to_space: { isGloballyAuthorized: true, authorizedSpaces: [] }, + }), + }); + clientOpts.baseClient.updateObjectsSpaces.mockResolvedValue({ + objects: [ + // The object was removed from spaceA and added to '*' + { ...obj1, spaces: ['*'] }, + ] as SavedObjectsUpdateObjectsSpacesResponseObject[], + }); + + const objects = [obj1]; + const spacesToAdd = [spaceA]; + const spacesToRemove = ['*']; + const options = { namespace: spaceC }; // spaceC is the current space + await client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, options); + + expect(mockEnsureAuthorized).toHaveBeenCalledTimes(1); + expect(mockEnsureAuthorized).toHaveBeenCalledWith( + expect.any(Object), // dependencies + ['x'], // unique types of the fetched objects + ['bulk_get', 'share_to_space'], // actions + [spaceC, spaceA, '*'], // unique spaces of: the current space, spacesToAdd, spacesToRemove, and spaces that the fetched objects exist in (excludes '*') + { requireFullAuthorization: false } + ); + }); +}); + describe('other', () => { test(`assigns errors from constructor to .errors`, () => { expect(client.errors).toBe(clientOpts.errors); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 066a720f70721..ef3dcac4c064b 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -7,7 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { - SavedObjectsAddToNamespacesOptions, + SavedObjectReferenceWithContext, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -15,13 +15,17 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, SavedObjectsCreateOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsOpenPointInTimeOptions, SavedObjectsRemoveReferencesToOptions, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, SavedObjectsUpdateOptions, } from 'src/core/server'; @@ -32,6 +36,12 @@ import { SavedObjectAction, savedObjectEvent } from '../audit'; import type { Actions, CheckSavedObjectsPrivileges } from '../authorization'; import type { CheckPrivilegesResponse } from '../authorization/types'; import type { SpacesService } from '../plugin'; +import type { + EnsureAuthorizedDependencies, + EnsureAuthorizedOptions, + EnsureAuthorizedResult, +} from './ensure_authorized'; +import { ensureAuthorized, getEnsureAuthorizedActionResult } from './ensure_authorized'; interface SecureSavedObjectsClientWrapperOptions { actions: Actions; @@ -51,21 +61,20 @@ interface SavedObjectsNamespaces { saved_objects: SavedObjectNamespaces[]; } -interface EnsureAuthorizedOptions { +interface LegacyEnsureAuthorizedOptions { args?: Record; auditAction?: string; requireFullAuthorization?: boolean; } -interface EnsureAuthorizedResult { +interface LegacyEnsureAuthorizedResult { status: 'fully_authorized' | 'partially_authorized' | 'unauthorized'; - typeMap: Map; + typeMap: Map; } -interface EnsureAuthorizedTypeResult { +interface LegacyEnsureAuthorizedTypeResult { authorizedSpaces: string[]; isGloballyAuthorized?: boolean; } - export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { private readonly actions: Actions; private readonly legacyAuditLogger: PublicMethodsOf; @@ -102,7 +111,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const namespaces = [optionsWithId.namespace, ...(optionsWithId.initialNamespaces || [])]; try { const args = { type, attributes, options: optionsWithId }; - await this.ensureAuthorized(type, 'create', namespaces, { args }); + await this.legacyEnsureAuthorized(type, 'create', namespaces, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -131,7 +140,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { const args = { objects, options }; const types = this.getUniqueObjectTypes(objects); - await this.ensureAuthorized(types, 'bulk_create', options.namespace, { + await this.legacyEnsureAuthorized(types, 'bulk_create', options.namespace, { args, auditAction: 'checkConflicts', }); @@ -154,7 +163,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); try { const args = { objects: objectsWithId, options }; - await this.ensureAuthorized( + await this.legacyEnsureAuthorized( this.getUniqueObjectTypes(objectsWithId), 'bulk_create', namespaces, @@ -191,7 +200,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'delete', options.namespace, { args }); + await this.legacyEnsureAuthorized(type, 'delete', options.namespace, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -230,7 +239,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } const args = { options }; - const { status, typeMap } = await this.ensureAuthorized( + const { status, typeMap } = await this.legacyEnsureAuthorized( options.type, 'find', options.namespaces, @@ -278,7 +287,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { objects, options }; - await this.ensureAuthorized( + await this.legacyEnsureAuthorized( this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, @@ -318,7 +327,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'get', options.namespace, { args }); + await this.legacyEnsureAuthorized(type, 'get', options.namespace, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -349,7 +358,10 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'get', options.namespace, { args, auditAction: 'resolve' }); + await this.legacyEnsureAuthorized(type, 'get', options.namespace, { + args, + auditAction: 'resolve', + }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -386,7 +398,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, id, attributes, options }; - await this.ensureAuthorized(type, 'update', options.namespace, { args }); + await this.legacyEnsureAuthorized(type, 'update', options.namespace, { args }); } catch (error) { this.auditLogger.log( savedObjectEvent({ @@ -409,90 +421,6 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.redactSavedObjectNamespaces(savedObject, [options.namespace]); } - public async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} - ) { - const { namespace } = options; - try { - const args = { type, id, namespaces, options }; - // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'addToNamespacesCreate', - }); - - // To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the - // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in - // the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation - // will result in a 404 error. - await this.ensureAuthorized(type, 'share_to_space', namespace, { - args, - auditAction: 'addToNamespacesUpdate', - }); - } catch (error) { - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.ADD_TO_SPACES, - savedObject: { type, id }, - addToSpaces: namespaces, - error, - }) - ); - throw error; - } - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.ADD_TO_SPACES, - outcome: 'unknown', - savedObject: { type, id }, - addToSpaces: namespaces, - }) - ); - - const response = await this.baseClient.addToNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response, [namespace, ...namespaces]); - } - - public async deleteFromNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} - ) { - try { - const args = { type, id, namespaces, options }; - // To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'deleteFromNamespaces', - }); - } catch (error) { - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.DELETE_FROM_SPACES, - savedObject: { type, id }, - deleteFromSpaces: namespaces, - error, - }) - ); - throw error; - } - this.auditLogger.log( - savedObjectEvent({ - action: SavedObjectAction.DELETE_FROM_SPACES, - outcome: 'unknown', - savedObject: { type, id }, - deleteFromSpaces: namespaces, - }) - ); - - const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(response, namespaces); - } - public async bulkUpdate( objects: Array> = [], options: SavedObjectsBaseOptions = {} @@ -505,9 +433,14 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const namespaces = [options?.namespace, ...objectNamespaces]; try { const args = { objects, options }; - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { - args, - }); + await this.legacyEnsureAuthorized( + this.getUniqueObjectTypes(objects), + 'bulk_update', + namespaces, + { + args, + } + ); } catch (error) { objects.forEach(({ type, id }) => this.auditLogger.log( @@ -541,7 +474,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, id, options }; - await this.ensureAuthorized(type, 'delete', options.namespace, { + await this.legacyEnsureAuthorized(type, 'delete', options.namespace, { args, auditAction: 'removeReferences', }); @@ -573,7 +506,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { try { const args = { type, options }; - await this.ensureAuthorized(type, 'open_point_in_time', options?.namespace, { + await this.legacyEnsureAuthorized(type, 'open_point_in_time', options?.namespace, { args, // Partial authorization is acceptable in this case because this method is only designed // to be used with `find`, which already allows for partial authorization. @@ -618,20 +551,254 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return await this.baseClient.closePointInTime(id, options); } - public createPointInTimeFinder( + public createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies ) { // We don't need to perform an authorization check here or add an audit log, because // `createPointInTimeFinder` is simply a helper that calls `find`, `openPointInTimeForType`, // and `closePointInTime` internally, so authz checks and audit logs will already be applied. - return this.baseClient.createPointInTimeFinder(findOptions, { + return this.baseClient.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that subsequent SO client wrappers have their settings applied. ...dependencies, }); } + public async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): Promise { + const currentSpaceId = SavedObjectsUtils.namespaceIdToString(options.namespace); // We need this whether the Spaces plugin is enabled or not. + + // We don't know the space(s) that each object exists in, so we'll collect the objects and references first, then check authorization. + const response = await this.baseClient.collectMultiNamespaceReferences(objects, options); + const uniqueTypes = this.getUniqueObjectTypes(response.objects); + const uniqueSpaces = this.getUniqueSpaces( + currentSpaceId, + ...response.objects.flatMap(({ spaces, spacesWithMatchingAliases = [] }) => + spaces.concat(spacesWithMatchingAliases) + ) + ); + + const { typeActionMap } = await this.ensureAuthorized( + uniqueTypes, + options.purpose === 'updateObjectsSpaces' ? ['bulk_get', 'share_to_space'] : ['bulk_get'], + uniqueSpaces, + { requireFullAuthorization: false } + ); + + // The user must be authorized to access every requested object in the current space. + // Note: non-multi-namespace object types will have an empty spaces array. + const authAction = options.purpose === 'updateObjectsSpaces' ? 'share_to_space' : 'bulk_get'; + try { + this.ensureAuthorizedInAllSpaces(objects, authAction, typeActionMap, [currentSpaceId]); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.COLLECT_MULTINAMESPACE_REFERENCES, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } + + // The user is authorized to access all of the requested objects in the space(s) that they exist in. + // Now: 1. omit any result objects that the user has no access to, 2. for the rest, redact any space(s) that the user is not authorized + // for, and 3. create audit records for any objects that will be returned to the user. + const requestedObjectsSet = objects.reduce( + (acc, { type, id }) => acc.add(`${type}:${id}`), + new Set() + ); + const retrievedObjectsSet = response.objects.reduce( + (acc, { type, id }) => acc.add(`${type}:${id}`), + new Set() + ); + const traversedObjects = new Set(); + const filteredObjectsMap = new Map(); + const getIsAuthorizedForInboundReference = (inbound: { type: string; id: string }) => { + const found = filteredObjectsMap.get(`${inbound.type}:${inbound.id}`); + return found && !found.isMissing; // If true, this object can be linked back to one of the requested objects + }; + let objectsToProcess = [...response.objects]; + while (objectsToProcess.length > 0) { + const obj = objectsToProcess.shift()!; + const { type, id, spaces, inboundReferences } = obj; + const objKey = `${type}:${id}`; + traversedObjects.add(objKey); + // Is the user authorized to access this object in all required space(s)? + const isAuthorizedForObject = isAuthorizedForObjectInAllSpaces( + type, + authAction, + typeActionMap, + [currentSpaceId] + ); + // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access + const redactedInboundReferences = inboundReferences.filter((inbound) => { + if (inbound.type === type && inbound.id === id) { + // circular reference, don't redact it + return true; + } + return getIsAuthorizedForInboundReference(inbound); + }); + // If the user is not authorized to access at least one inbound reference of this object, then we should omit this object. + const isAuthorizedForGraph = + requestedObjectsSet.has(objKey) || // If true, this is one of the requested objects, and we checked authorization above + redactedInboundReferences.some(getIsAuthorizedForInboundReference); + + if (isAuthorizedForObject && isAuthorizedForGraph) { + if (spaces.length) { + // Don't generate audit records for "empty results" with zero spaces (requested object was a non-multi-namespace type or hidden type) + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.COLLECT_MULTINAMESPACE_REFERENCES, + savedObject: { type, id }, + }) + ); + } + filteredObjectsMap.set(objKey, obj); + } else if (!isAuthorizedForObject && isAuthorizedForGraph) { + filteredObjectsMap.set(objKey, { ...obj, spaces: [], isMissing: true }); + } else if (isAuthorizedForObject && !isAuthorizedForGraph) { + const hasUntraversedInboundReferences = inboundReferences.some( + (ref) => + !traversedObjects.has(`${ref.type}:${ref.id}`) && + retrievedObjectsSet.has(`${ref.type}:${ref.id}`) + ); + + if (hasUntraversedInboundReferences) { + // this object has inbound reference(s) that we haven't traversed yet; bump it to the back of the list + objectsToProcess = [...objectsToProcess, obj]; + } else { + // There should never be a missing inbound reference. + // If there is, then something has gone terribly wrong. + const missingInboundReference = inboundReferences.find( + (ref) => + !traversedObjects.has(`${ref.type}:${ref.id}`) && + !retrievedObjectsSet.has(`${ref.type}:${ref.id}`) + ); + + if (missingInboundReference) { + throw new Error( + `Unexpected inbound reference to "${missingInboundReference.type}:${missingInboundReference.id}"` + ); + } + } + } + } + + const filteredAndRedactedObjects = [...filteredObjectsMap.values()].map((obj) => { + const { type, id, spaces, spacesWithMatchingAliases, inboundReferences } = obj; + // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access + const redactedInboundReferences = inboundReferences.filter((inbound) => { + if (inbound.type === type && inbound.id === id) { + // circular reference, don't redact it + return true; + } + return getIsAuthorizedForInboundReference(inbound); + }); + const redactedSpaces = getRedactedSpaces(type, 'bulk_get', typeActionMap, spaces); + const redactedSpacesWithMatchingAliases = + spacesWithMatchingAliases && + getRedactedSpaces(type, 'bulk_get', typeActionMap, spacesWithMatchingAliases); + return { + ...obj, + spaces: redactedSpaces, + ...(redactedSpacesWithMatchingAliases && { + spacesWithMatchingAliases: redactedSpacesWithMatchingAliases, + }), + inboundReferences: redactedInboundReferences, + }; + }); + + return { + objects: filteredAndRedactedObjects, + }; + } + + public async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options: SavedObjectsUpdateObjectsSpacesOptions = {} + ) { + const { namespace } = options; + const currentSpaceId = SavedObjectsUtils.namespaceIdToString(namespace); // We need this whether the Spaces plugin is enabled or not. + + const allSpacesSet = new Set([currentSpaceId, ...spacesToAdd, ...spacesToRemove]); + const bulkGetResponse = await this.baseClient.bulkGet(objects, { namespace }); + const objectsToUpdate = objects.map(({ type, id }, i) => { + const { namespaces: spaces = [], version } = bulkGetResponse.saved_objects[i]; + // If 'namespaces' is undefined, the object was not found (or it is namespace-agnostic). + // Either way, we will pass in an empty 'spaces' array to the base client, which will cause it to skip this object. + for (const space of spaces) { + if (space !== ALL_SPACES_ID) { + // If this is a specific space, add it to the spaces we'll check privileges for (don't accidentally check for global privileges) + allSpacesSet.add(space); + } + } + return { type, id, spaces, version }; + }); + + const uniqueTypes = this.getUniqueObjectTypes(objects); + const { typeActionMap } = await this.ensureAuthorized( + uniqueTypes, + ['bulk_get', 'share_to_space'], + Array.from(allSpacesSet), + { requireFullAuthorization: false } + ); + + const addToSpaces = spacesToAdd.length ? spacesToAdd : undefined; + const deleteFromSpaces = spacesToRemove.length ? spacesToRemove : undefined; + try { + // The user must be authorized to share every requested object in each of: the current space, spacesToAdd, and spacesToRemove. + const spaces = this.getUniqueSpaces(currentSpaceId, ...spacesToAdd, ...spacesToRemove); + this.ensureAuthorizedInAllSpaces(objects, 'share_to_space', typeActionMap, spaces); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE_OBJECTS_SPACES, + savedObject: { type, id }, + addToSpaces, + deleteFromSpaces, + error, + }) + ) + ); + throw error; + } + for (const { type, id } of objectsToUpdate) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE_OBJECTS_SPACES, + outcome: 'unknown', + savedObject: { type, id }, + addToSpaces, + deleteFromSpaces, + }) + ); + } + + const response = await this.baseClient.updateObjectsSpaces( + objectsToUpdate, + spacesToAdd, + spacesToRemove, + { namespace } + ); + // Now that we have updated the objects' spaces, redact any spaces that the user is not authorized to see from the response. + const redactedObjects = response.objects.map((obj) => { + const { type, spaces } = obj; + const redactedSpaces = getRedactedSpaces(type, 'bulk_get', typeActionMap, spaces); + return { ...obj, spaces: redactedSpaces }; + }); + + return { objects: redactedObjects }; + } + private async checkPrivileges( actions: string | string[], namespaceOrNamespaces?: string | Array @@ -643,12 +810,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } } - private async ensureAuthorized( + private async legacyEnsureAuthorized( typeOrTypes: string | string[], action: string, namespaceOrNamespaces: undefined | string | Array, - options: EnsureAuthorizedOptions = {} - ): Promise { + options: LegacyEnsureAuthorizedOptions = {} + ): Promise { const { args, auditAction = action, requireFullAuthorization = true } = options; const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const actionsToTypesMap = new Map( @@ -663,7 +830,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ).sort() as string[]; const missingPrivileges = this.getMissingPrivileges(privileges); - const typeMap = privileges.kibana.reduce>( + const typeMap = privileges.kibana.reduce>( (acc, { resource, privilege, authorized }) => { if (!authorized) { return acc; @@ -724,6 +891,45 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } } + /** Unlike `legacyEnsureAuthorized`, this accepts multiple actions, and it does not utilize legacy audit logging */ + private async ensureAuthorized( + types: string[], + actions: T[], + namespaces: string[], + options?: EnsureAuthorizedOptions + ) { + const ensureAuthorizedDependencies: EnsureAuthorizedDependencies = { + actions: this.actions, + errors: this.errors, + checkSavedObjectsPrivilegesAsCurrentUser: this.checkSavedObjectsPrivilegesAsCurrentUser, + }; + return ensureAuthorized(ensureAuthorizedDependencies, types, actions, namespaces, options); + } + + /** + * If `ensureAuthorized` was called with `requireFullAuthorization: false`, this can be used with the result to ensure that a given + * array of objects are authorized in the required space(s). + */ + private ensureAuthorizedInAllSpaces( + objects: Array<{ type: string }>, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'], + spaces: string[] + ) { + const uniqueTypes = uniq(objects.map(({ type }) => type)); + const unauthorizedTypes = new Set(); + for (const type of uniqueTypes) { + if (!isAuthorizedForObjectInAllSpaces(type, action, typeActionMap, spaces)) { + unauthorizedTypes.add(type); + } + } + if (unauthorizedTypes.size > 0) { + const targetTypes = Array.from(unauthorizedTypes).sort().join(','); + const msg = `Unable to ${action} ${targetTypes}`; + throw this.errors.decorateForbiddenError(new Error(msg)); + } + } + private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { return privileges.kibana .filter(({ authorized }) => !authorized) @@ -734,6 +940,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra return uniq(objects.map((o) => o.type)); } + /** + * Given a list of spaces, returns a unique array of spaces. + * Excludes `'*'`, which is an identifier for All Spaces but is not an actual space. + */ + private getUniqueSpaces(...spaces: string[]) { + const set = new Set(spaces); + set.delete(ALL_SPACES_ID); + return Array.from(set); + } + private async getNamespacesPrivilegeMap( namespaces: string[], previouslyAuthorizedSpaceIds: string[] @@ -854,3 +1070,33 @@ function namespaceComparator(a: string, b: string) { } return A > B ? 1 : A < B ? -1 : 0; } + +function isAuthorizedForObjectInAllSpaces( + objectType: string, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'], + spacesToAuthorizeFor: string[] +) { + const actionResult = getEnsureAuthorizedActionResult(objectType, action, typeActionMap); + const { authorizedSpaces, isGloballyAuthorized } = actionResult; + const authorizedSpacesSet = new Set(authorizedSpaces); + return ( + isGloballyAuthorized || spacesToAuthorizeFor.every((space) => authorizedSpacesSet.has(space)) + ); +} + +function getRedactedSpaces( + objectType: string, + action: T, + typeActionMap: EnsureAuthorizedResult['typeActionMap'], + spacesToRedact: string[] +) { + const actionResult = getEnsureAuthorizedActionResult(objectType, action, typeActionMap); + const { authorizedSpaces, isGloballyAuthorized } = actionResult; + const authorizedSpacesSet = new Set(authorizedSpaces); + return spacesToRedact + .map((x) => + isGloballyAuthorized || x === ALL_SPACES_ID || authorizedSpacesSet.has(x) ? x : UNKNOWN_SPACE + ) + .sort(namespaceComparator); +} diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 38a452a82a6f9..9935d8055ec30 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -8,4 +8,4 @@ export { isReservedSpace } from './is_reserved_space'; export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; -export { GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult } from './types'; +export type { GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult } from './types'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx index b5b7c7c657b1b..4ec90b7e3826b 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx @@ -280,7 +280,7 @@ describe('ShareToSpaceFlyout', () => { it('handles errors thrown from shareSavedObjectsAdd API call', async () => { const { wrapper, mockSpacesManager, mockToastNotifications } = await setup(); - mockSpacesManager.shareSavedObjectAdd.mockRejectedValue( + mockSpacesManager.updateSavedObjectsSpaces.mockRejectedValue( Boom.serverUnavailable('Something bad happened') ); @@ -303,39 +303,7 @@ describe('ShareToSpaceFlyout', () => { wrapper.update(); }); - expect(mockSpacesManager.shareSavedObjectAdd).toHaveBeenCalled(); - expect(mockSpacesManager.shareSavedObjectRemove).not.toHaveBeenCalled(); - expect(mockToastNotifications.addError).toHaveBeenCalled(); - }); - - it('handles errors thrown from shareSavedObjectsRemove API call', async () => { - const { wrapper, mockSpacesManager, mockToastNotifications } = await setup(); - - mockSpacesManager.shareSavedObjectRemove.mockRejectedValue( - Boom.serverUnavailable('Something bad happened') - ); - - expect(wrapper.find(ShareToSpaceForm)).toHaveLength(1); - expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0); - expect(wrapper.find(NoSpacesAvailable)).toHaveLength(0); - - // Using props callback instead of simulating clicks, - // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects - const spaceSelector = wrapper.find(SelectableSpacesControl); - act(() => { - spaceSelector.props().onChange(['space-2', 'space-3']); - }); - - const startButton = findTestSubject(wrapper, 'sts-initiate-button'); - - await act(async () => { - startButton.simulate('click'); - await nextTick(); - wrapper.update(); - }); - - expect(mockSpacesManager.shareSavedObjectAdd).toHaveBeenCalled(); - expect(mockSpacesManager.shareSavedObjectRemove).toHaveBeenCalled(); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalled(); expect(mockToastNotifications.addError).toHaveBeenCalled(); }); @@ -369,9 +337,11 @@ describe('ShareToSpaceFlyout', () => { }); const { type, id } = savedObjectToShare; - const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; - expect(shareSavedObjectAdd).toHaveBeenCalledWith({ type, id }, ['space-2', 'space-3']); - expect(shareSavedObjectRemove).not.toHaveBeenCalled(); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalledWith( + [{ type, id }], + ['space-2', 'space-3'], + [] + ); expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); @@ -408,9 +378,11 @@ describe('ShareToSpaceFlyout', () => { }); const { type, id } = savedObjectToShare; - const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; - expect(shareSavedObjectAdd).not.toHaveBeenCalled(); - expect(shareSavedObjectRemove).toHaveBeenCalledWith({ type, id }, ['space-1']); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalledWith( + [{ type, id }], + [], + ['space-1'] + ); expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); @@ -447,11 +419,13 @@ describe('ShareToSpaceFlyout', () => { }); const { type, id } = savedObjectToShare; - const { shareSavedObjectAdd, shareSavedObjectRemove } = mockSpacesManager; - expect(shareSavedObjectAdd).toHaveBeenCalledWith({ type, id }, ['space-2', 'space-3']); - expect(shareSavedObjectRemove).toHaveBeenCalledWith({ type, id }, ['space-1']); + expect(mockSpacesManager.updateSavedObjectsSpaces).toHaveBeenCalledWith( + [{ type, id }], + ['space-2', 'space-3'], + ['space-1'] + ); - expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(2); + expect(mockToastNotifications.addSuccess).toHaveBeenCalledTimes(1); expect(mockToastNotifications.addError).not.toHaveBeenCalled(); expect(onClose).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx index fc5d42df8af5e..d8fc0f299d8e6 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx @@ -46,8 +46,17 @@ const LazyCopyToSpaceFlyout = lazy(() => ); const ALL_SPACES_TARGET = i18n.translate('xpack.spaces.shareToSpace.allSpacesTarget', { - defaultMessage: 'all', + defaultMessage: 'all spaces', }); +function getSpacesTargetString(spaces: string[]) { + if (spaces.includes(ALL_SPACES_ID)) { + return ALL_SPACES_TARGET; + } + return i18n.translate('xpack.spaces.shareToSpace.spacesTarget', { + defaultMessage: '{spacesCount, plural, one {# space} other {# spaces}}', + values: { spacesCount: spaces.length }, + }); +} const arraysAreEqual = (a: unknown[], b: unknown[]) => a.every((x) => b.includes(x)) && b.every((x) => a.includes(x)); @@ -59,44 +68,46 @@ function createDefaultChangeSpacesHandler( ) { return async (spacesToAdd: string[], spacesToRemove: string[]) => { const { type, id, title } = object; + const objects = [{ type, id }]; const toastTitle = i18n.translate('xpack.spaces.shareToSpace.shareSuccessTitle', { values: { objectNoun: object.noun }, defaultMessage: 'Updated {objectNoun}', + description: `Object noun can be plural or singular, examples: "Updated objects", "Updated job"`, }); + await spacesManager.updateSavedObjectsSpaces(objects, spacesToAdd, spacesToRemove); + const isSharedToAllSpaces = spacesToAdd.includes(ALL_SPACES_ID); - if (spacesToAdd.length > 0) { - await spacesManager.shareSavedObjectAdd({ type, id }, spacesToAdd); - const spaceTargets = isSharedToAllSpaces ? ALL_SPACES_TARGET : `${spacesToAdd.length}`; - const toastText = - !isSharedToAllSpaces && spacesToAdd.length === 1 - ? i18n.translate('xpack.spaces.shareToSpace.shareAddSuccessTextSingular', { - defaultMessage: `'{object}' was added to 1 space.`, - values: { object: title }, - }) - : i18n.translate('xpack.spaces.shareToSpace.shareAddSuccessTextPlural', { - defaultMessage: `'{object}' was added to {spaceTargets} spaces.`, - values: { object: title, spaceTargets }, - }); - toastNotifications.addSuccess({ title: toastTitle, text: toastText }); - } - if (spacesToRemove.length > 0) { - await spacesManager.shareSavedObjectRemove({ type, id }, spacesToRemove); - const isUnsharedFromAllSpaces = spacesToRemove.includes(ALL_SPACES_ID); - const spaceTargets = isUnsharedFromAllSpaces ? ALL_SPACES_TARGET : `${spacesToRemove.length}`; - const toastText = - !isUnsharedFromAllSpaces && spacesToRemove.length === 1 - ? i18n.translate('xpack.spaces.shareToSpace.shareRemoveSuccessTextSingular', { - defaultMessage: `'{object}' was removed from 1 space.`, - values: { object: title }, - }) - : i18n.translate('xpack.spaces.shareToSpace.shareRemoveSuccessTextPlural', { - defaultMessage: `'{object}' was removed from {spaceTargets} spaces.`, - values: { object: title, spaceTargets }, - }); - if (!isSharedToAllSpaces) { - toastNotifications.addSuccess({ title: toastTitle, text: toastText }); - } + let toastText: string; + if (spacesToAdd.length > 0 && spacesToRemove.length > 0 && !isSharedToAllSpaces) { + toastText = i18n.translate('xpack.spaces.shareToSpace.shareSuccessAddRemoveText', { + defaultMessage: `'{object}' was added to {spacesTargetAdd} and removed from {spacesTargetRemove}.`, // TODO: update to include # of references and/or # of tags + values: { + object: title, + spacesTargetAdd: getSpacesTargetString(spacesToAdd), + spacesTargetRemove: getSpacesTargetString(spacesToRemove), + }, + description: `Uses output of xpack.spaces.shareToSpace.spacesTarget or xpack.spaces.shareToSpace.allSpacesTarget as 'spacesTarget...' inputs. Example strings: "'Finance dashboard' was added to 1 space and removed from 2 spaces.", "'Finance dashboard' was added to 3 spaces and removed from all spaces."`, + }); + } else if (spacesToAdd.length > 0) { + toastText = i18n.translate('xpack.spaces.shareToSpace.shareSuccessAddText', { + defaultMessage: `'{object}' was added to {spacesTarget}.`, // TODO: update to include # of references and/or # of tags + values: { + object: title, + spacesTarget: getSpacesTargetString(spacesToAdd), + }, + description: `Uses output of xpack.spaces.shareToSpace.spacesTarget or xpack.spaces.shareToSpace.allSpacesTarget as 'spacesTarget' input. Example strings: "'Finance dashboard' was added to 1 space.", "'Finance dashboard' was added to all spaces."`, + }); + } else { + toastText = i18n.translate('xpack.spaces.shareToSpace.shareSuccessRemoveText', { + defaultMessage: `'{object}' was removed from {spacesTarget}.`, // TODO: update to include # of references and/or # of tags + values: { + object: title, + spacesTarget: getSpacesTargetString(spacesToRemove), + }, + description: `Uses output of xpack.spaces.shareToSpace.spacesTarget or xpack.spaces.shareToSpace.allSpacesTarget as 'spacesTarget' input. Example strings: "'Finance dashboard' was removed from 1 space.", "'Finance dashboard' was removed from all spaces."`, + }); } + toastNotifications.addSuccess({ title: toastTitle, text: toastText }); }; } @@ -148,9 +159,11 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { spaces: ShareToSpaceTarget[]; }>({ isLoading: true, spaces: [] }); useEffect(() => { - const getPermissions = spacesManager.getShareSavedObjectPermissions(savedObjectTarget.type); - Promise.all([shareToSpacesDataPromise, getPermissions]) - .then(([shareToSpacesData, permissions]) => { + const { type, id } = savedObjectTarget; + const getShareableReferences = spacesManager.getShareableReferences([{ type, id }]); // NOTE: not used yet, this is just included so you can see the request/response in Dev Tools + const getPermissions = spacesManager.getShareSavedObjectPermissions(type); + Promise.all([shareToSpacesDataPromise, getShareableReferences, getPermissions]) + .then(([shareToSpacesData, shareableReferences, permissions]) => { const activeSpaceId = !enableSpaceAgnosticBehavior && shareToSpacesData.activeSpaceId; const selectedSpaceIds = savedObjectTarget.namespaces.filter( (spaceId) => spaceId !== activeSpaceId diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index ccb475369104a..39c06a2bc874d 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -22,8 +22,8 @@ function createSpacesManagerMock() { updateSpace: jest.fn().mockResolvedValue(undefined), deleteSpace: jest.fn().mockResolvedValue(undefined), copySavedObjects: jest.fn().mockResolvedValue(undefined), - shareSavedObjectAdd: jest.fn().mockResolvedValue(undefined), - shareSavedObjectRemove: jest.fn().mockResolvedValue(undefined), + getShareableReferences: jest.fn().mockResolvedValue(undefined), + updateSavedObjectsSpaces: jest.fn().mockResolvedValue(undefined), resolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(undefined), getShareSavedObjectPermissions: jest.fn().mockResolvedValue(undefined), redirectToSpaceSelector: jest.fn().mockResolvedValue(undefined), diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index 1cae128299197..a7201def5ed40 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -9,7 +9,10 @@ import type { Observable } from 'rxjs'; import { BehaviorSubject } from 'rxjs'; import { skipWhile } from 'rxjs/operators'; -import type { HttpSetup } from 'src/core/public'; +import type { + HttpSetup, + SavedObjectsCollectMultiNamespaceReferencesResponse, +} from 'src/core/public'; import type { Space } from 'src/plugins/spaces_oss/common'; import type { GetAllSpacesOptions, GetSpaceResult } from '../../common'; @@ -136,15 +139,21 @@ export class SpacesManager { }); } - public async shareSavedObjectAdd(object: SavedObjectTarget, spaces: string[]): Promise { - return this.http.post(`/api/spaces/_share_saved_object_add`, { - body: JSON.stringify({ object, spaces }), + public async getShareableReferences( + objects: SavedObjectTarget[] + ): Promise { + return this.http.post(`/api/spaces/_get_shareable_references`, { + body: JSON.stringify({ objects }), }); } - public async shareSavedObjectRemove(object: SavedObjectTarget, spaces: string[]): Promise { - return this.http.post(`/api/spaces/_share_saved_object_remove`, { - body: JSON.stringify({ object, spaces }), + public async updateSavedObjectsSpaces( + objects: SavedObjectTarget[], + spacesToAdd: string[], + spacesToRemove: string[] + ): Promise { + return this.http.post(`/api/spaces/_update_objects_spaces`, { + body: JSON.stringify({ objects, spacesToAdd, spacesToRemove }), }); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index a8d8ed9b868c8..74ada21399f6e 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -9,7 +9,6 @@ import { Readable } from 'stream'; import type { SavedObjectsExportByObjectOptions, - SavedObjectsImportOptions, SavedObjectsImportResponse, SavedObjectsImportSuccess, } from 'src/core/server'; @@ -26,12 +25,9 @@ import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; interface SetupOpts { objects: Array<{ type: string; id: string; attributes: Record }>; exportByObjectsImpl?: (opts: SavedObjectsExportByObjectOptions) => Promise; - importSavedObjectsFromStreamImpl?: ( - opts: SavedObjectsImportOptions - ) => Promise; } -const expectStreamToContainObjects = async ( +const expectStreamToEqualObjects = async ( stream: Readable, expectedObjects: SetupOpts['objects'] ) => { @@ -50,10 +46,18 @@ const expectStreamToContainObjects = async ( }; describe('copySavedObjectsToSpaces', () => { + const FAILURE_SPACE = 'failure-space'; const mockExportResults = [ - { type: 'dashboard', id: 'my-dashboard', attributes: {} }, - { type: 'visualization', id: 'my-viz', attributes: {} }, - { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + // For this test case, these three objects can be shared to multiple spaces + { type: 'dashboard', id: 'my-dashboard', namespaces: ['source'], attributes: {} }, + { type: 'visualization', id: 'my-viz', namespaces: ['source', 'destination1'], attributes: {} }, + { + type: 'index-pattern', + id: 'my-index-pattern', + namespaces: ['source', 'destination1', 'destination2'], + attributes: {}, + }, + // This object is namespace-agnostic and cannot be copied to another space { type: 'globaltype', id: 'my-globaltype', attributes: {} }, ]; @@ -73,7 +77,7 @@ describe('copySavedObjectsToSpaces', () => { // don't need to include all types, just need a positive case (agnostic) and a negative case (non-agnostic) { name: 'dashboard', - namespaceType: 'single', + namespaceType: 'multiple', hidden: false, mappings: { properties: {} }, }, @@ -105,21 +109,45 @@ describe('copySavedObjectsToSpaces', () => { }); savedObjectsImporter.import.mockImplementation(async (opts) => { - const defaultImpl = async () => { - // namespace-agnostic types should be filtered out before import - const filteredObjects = setupOpts.objects.filter(({ type }) => type !== 'globaltype'); - await expectStreamToContainObjects(opts.readStream, filteredObjects); - const response: SavedObjectsImportResponse = { - success: true, - successCount: filteredObjects.length, - successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], - warnings: [], - }; - - return Promise.resolve(response); + if (opts.namespace === FAILURE_SPACE) { + throw new Error(`Some error occurred!`); + } + + // expectedObjects will never include globaltype, and each object will have its namespaces field omitted + let expectedObjects = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + { type: 'index-pattern', id: 'my-index-pattern', attributes: {} }, + ]; + + if (!opts.createNewCopies) { + // if we are *not* creating new copies of objects, then we check destination spaces so we don't try to copy an object to a space where it already exists + switch (opts.namespace) { + case 'destination1': + expectedObjects = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + // the visualization and index-pattern are not imported into destination1, they already exist there + ]; + break; + case 'destination2': + expectedObjects = [ + { type: 'dashboard', id: 'my-dashboard', attributes: {} }, + { type: 'visualization', id: 'my-viz', attributes: {} }, + // the index-pattern is not imported into destination2, it already exists there + ]; + break; + } + } + + await expectStreamToEqualObjects(opts.readStream, expectedObjects); + const response: SavedObjectsImportResponse = { + success: true, + successCount: expectedObjects.length, + successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], + warnings: [], }; - return setupOpts.importSavedObjectsFromStreamImpl?.(opts) ?? defaultImpl(); + return Promise.resolve(response); }); return { @@ -154,7 +182,7 @@ describe('copySavedObjectsToSpaces', () => { "destination1": Object { "errors": undefined, "success": true, - "successCount": 3, + "successCount": 1, "successResults": Array [ "Some success(es) occurred!", ], @@ -162,7 +190,7 @@ describe('copySavedObjectsToSpaces', () => { "destination2": Object { "errors": undefined, "success": true, - "successCount": 3, + "successCount": 2, "successResults": Array [ "Some success(es) occurred!", ], @@ -173,6 +201,7 @@ describe('copySavedObjectsToSpaces', () => { expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({ request: expect.any(Object), excludeExportDetails: true, + includeNamespaces: true, includeReferencesDeep: true, namespace, objects, @@ -193,23 +222,74 @@ describe('copySavedObjectsToSpaces', () => { }); }); + it('does not skip copying objects to spaces where they already exist if createNewCopies is enabled', async () => { + const { savedObjects, savedObjectsExporter, savedObjectsImporter } = setup({ + objects: mockExportResults.map(({ namespaces, ...remainingAttrs }) => ({ + ...remainingAttrs, // the objects are exported without the namespaces array + })), + }); + + const request = httpServerMock.createKibanaRequest(); + + const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(savedObjects, request); + + const namespace = 'sourceSpace'; + const objects = [{ type: 'dashboard', id: 'my-dashboard' }]; + const result = await copySavedObjectsToSpaces(namespace, ['destination1', 'destination2'], { + includeReferences: true, + overwrite: false, + objects, + createNewCopies: true, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "destination1": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + "destination2": Object { + "errors": undefined, + "success": true, + "successCount": 3, + "successResults": Array [ + "Some success(es) occurred!", + ], + }, + } + `); + + expect(savedObjectsExporter.exportByObjects).toHaveBeenCalledWith({ + request: expect.any(Object), + excludeExportDetails: true, + includeNamespaces: false, + includeReferencesDeep: true, + namespace, + objects, + }); + + const importOptions = { + createNewCopies: true, + overwrite: false, + readStream: expect.any(Readable), + }; + expect(savedObjectsImporter.import).toHaveBeenNthCalledWith(1, { + ...importOptions, + namespace: 'destination1', + }); + expect(savedObjectsImporter.import).toHaveBeenNthCalledWith(2, { + ...importOptions, + namespace: 'destination2', + }); + }); + it(`doesn't stop copy if some spaces fail`, async () => { const { savedObjects } = setup({ objects: mockExportResults, - importSavedObjectsFromStreamImpl: async (opts) => { - if (opts.namespace === 'failure-space') { - throw new Error(`Some error occurred!`); - } - // namespace-agnostic types should be filtered out before import - const filteredObjects = mockExportResults.filter(({ type }) => type !== 'globaltype'); - await expectStreamToContainObjects(opts.readStream, filteredObjects); - return Promise.resolve({ - success: true, - successCount: filteredObjects.length, - successResults: [('Some success(es) occurred!' as unknown) as SavedObjectsImportSuccess], - warnings: [], - }); - }, }); const request = httpServerMock.createKibanaRequest(); @@ -218,7 +298,7 @@ describe('copySavedObjectsToSpaces', () => { const result = await copySavedObjectsToSpaces( 'sourceSpace', - ['failure-space', 'non-existent-space', 'marketing'], + [FAILURE_SPACE, 'non-existent-space', 'marketing'], { includeReferences: true, overwrite: true, @@ -226,6 +306,7 @@ describe('copySavedObjectsToSpaces', () => { createNewCopies: false, } ); + // See savedObjectsImporter.import mock implementation above; FAILURE_SPACE is a special case that will throw an error expect(result).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 29dac92e5fc6d..ed09c4d39d137 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -9,6 +9,7 @@ import type { Readable } from 'stream'; import type { CoreStart, KibanaRequest, SavedObject } from 'src/core/server'; +import { ALL_SPACES_ID } from '../../../common/constants'; import { spaceIdToNamespace } from '../utils/namespace'; import { createEmptyFailureResponse } from './lib/create_empty_failure_response'; import { getIneligibleTypes } from './lib/get_ineligible_types'; @@ -27,14 +28,12 @@ export function copySavedObjectsToSpacesFactory( const savedObjectsExporter = createExporter(savedObjectsClient); const savedObjectsImporter = createImporter(savedObjectsClient); - const exportRequestedObjects = async ( - sourceSpaceId: string, - options: Pick - ) => { + const exportRequestedObjects = async (sourceSpaceId: string, options: CopyOptions) => { const objectStream = await savedObjectsExporter.exportByObjects({ request, namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, + includeNamespaces: !options.createNewCopies, // if we are not creating new copies, then include namespaces; this will ensure we can check for objects that already exist in the destination space below excludeExportDetails: true, objects: options.objects, }); @@ -76,13 +75,23 @@ export function copySavedObjectsToSpacesFactory( const exportedSavedObjects = await exportRequestedObjects(sourceSpaceId, options); const ineligibleTypes = getIneligibleTypes(getTypeRegistry()); const filteredObjects = exportedSavedObjects.filter( - ({ type }) => !ineligibleTypes.includes(type) + ({ type, namespaces }) => + // Don't attempt to copy ineligible types or objects that already exist in all spaces + !ineligibleTypes.includes(type) && !namespaces?.includes(ALL_SPACES_ID) ); for (const spaceId of destinationSpaceIds) { + const objectsToImport: SavedObject[] = []; + for (const { namespaces, ...object } of filteredObjects) { + if (!namespaces?.includes(spaceId)) { + // We check to ensure that each object doesn't already exist in the destination. If we don't do this, the consumer will see a + // conflict and have the option to skip or overwrite the object, both of which are effectively a no-op. + objectsToImport.push(object); + } + } response[spaceId] = await importObjectsToSpace( spaceId, - createReadableStreamFromArray(filteredObjects), + createReadableStreamFromArray(objectsToImport), options ); } diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 1ce030ef05d12..72a3921618ddc 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -91,6 +91,10 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const retries = entryRetries.map((retry) => ({ ...retry, replaceReferences: [] })); + // We do *not* include a check to ensure that each object doesn't already exist in the destination. Since we already do this in + // copySavedObjectsToSpaces, it is much less likely to occur while resolving copy errors, and as such we've omitted the same check + // here to reduce complexity and test cases. + response[spaceId] = await resolveConflictsForSpace( spaceId, createReadableStreamFromArray(filteredObjects), diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts new file mode 100644 index 0000000000000..1100f767c33b8 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts @@ -0,0 +1,144 @@ +/* + * 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 * as Rx from 'rxjs'; + +import type { RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { + coreMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, +} from 'src/core/server/mocks'; + +import { spacesConfig } from '../../../lib/__fixtures__'; +import { SpacesClientService } from '../../../spaces_client'; +import { SpacesService } from '../../../spaces_service'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; +import { + createMockSavedObjectsRepository, + createMockSavedObjectsService, + createSpaces, + mockRouteContext, + mockRouteContextWithInvalidLicense, +} from '../__fixtures__'; +import { initGetShareableReferencesApi } from './get_shareable_references'; + +describe('get shareable references', () => { + const spacesSavedObjects = createSpaces(); + const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); + + const setup = async () => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter(); + const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); + const log = loggingSystemMock.create().get('spaces'); + const coreStart = coreMock.createStart(); + const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); + coreStart.savedObjects = savedObjects; + + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, + }); + + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, + }); + initGetShareableReferencesApi({ + externalRouter: router, + getStartServices: async () => [coreStart, {}, {}], + log, + getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, + }); + + const [[getShareableReferences, getShareableReferencesRouteHandler]] = router.post.mock.calls; + + return { + coreStart, + savedObjectsClient, + getShareableReferences: { + routeValidation: getShareableReferences.validate as RouteValidatorConfig<{}, {}, {}>, + routeHandler: getShareableReferencesRouteHandler, + }, + savedObjectsRepositoryMock, + }; + }; + + describe('POST /api/spaces/_get_shareable_references', () => { + it(`returns http/403 when the license is invalid`, async () => { + const { getShareableReferences } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + }); + + const response = await getShareableReferences.routeHandler( + mockRouteContextWithInvalidLicense, + request, + kibanaResponseFactory + ); + + expect(response.status).toEqual(403); + expect(response.payload).toEqual({ + message: 'License is invalid for spaces', + }); + }); + + it('passes arguments to the saved objects client and returns the result', async () => { + const { getShareableReferences, savedObjectsClient } = await setup(); + const reqObj1 = { type: 'a', id: 'id-1' }; + const reqObjects = [reqObj1]; + const payload = { objects: reqObjects }; + const collectedObjects = [ + // the return value of collectMultiNamespaceReferences includes the 1 requested object, along with the 2 references + { ...reqObj1, spaces: ['space-1'], inboundReferences: [] }, + { + type: 'b', + id: 'id-4', + spaces: ['space-1', '?', '?'], + inboundReferences: [{ ...reqObj1, name: 'ref-a:1' }], + }, + { + type: 'c', + id: 'id-5', + spaces: ['space-1', 'space-2'], + inboundReferences: [{ ...reqObj1, name: 'ref-a:1' }], + }, + ]; + savedObjectsClient.collectMultiNamespaceReferences.mockResolvedValue({ + objects: collectedObjects, + }); + + const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); + const response = await getShareableReferences.routeHandler( + mockRouteContext, + request, + kibanaResponseFactory + ); + + expect(response.status).toEqual(200); + expect(response.payload).toEqual({ objects: collectedObjects }); + expect(savedObjectsClient.collectMultiNamespaceReferences).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.collectMultiNamespaceReferences).toHaveBeenCalledWith(reqObjects, { + purpose: 'updateObjectsSpaces', + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts new file mode 100644 index 0000000000000..a7afd38dcecb0 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { wrapError } from '../../../lib/errors'; +import { createLicensedRouteHandler } from '../../lib'; +import type { ExternalRouteDeps } from './'; + +export function initGetShareableReferencesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + externalRouter.post( + { + path: '/api/spaces/_get_shareable_references', + validate: { + body: schema.object({ + objects: schema.arrayOf(schema.object({ type: schema.string(), id: schema.string() })), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const { objects } = request.body; + + try { + const collectedObjects = await scopedClient.collectMultiNamespaceReferences(objects, { + purpose: 'updateObjectsSpaces', + }); + return response.ok({ body: collectedObjects }); + } catch (error) { + return response.customError(wrapError(error)); + } + }) + ); +} diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index 3e2a523d767ea..9cebd8d0f9352 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -14,9 +14,10 @@ import { initCopyToSpacesApi } from './copy_to_space'; import { initDeleteSpacesApi } from './delete'; import { initGetSpaceApi } from './get'; import { initGetAllSpacesApi } from './get_all'; +import { initGetShareableReferencesApi } from './get_shareable_references'; import { initPostSpacesApi } from './post'; import { initPutSpacesApi } from './put'; -import { initShareToSpacesApi } from './share_to_space'; +import { initUpdateObjectsSpacesApi } from './update_objects_spaces'; export interface ExternalRouteDeps { externalRouter: SpacesRouter; @@ -33,5 +34,6 @@ export function initExternalSpacesApi(deps: ExternalRouteDeps) { initPostSpacesApi(deps); initPutSpacesApi(deps); initCopyToSpacesApi(deps); - initShareToSpacesApi(deps); + initUpdateObjectsSpacesApi(deps); + initGetShareableReferencesApi(deps); } diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts deleted file mode 100644 index cae6fd152d8ff..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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 * as Rx from 'rxjs'; - -import type { ObjectType } from '@kbn/config-schema'; -import type { RouteValidatorConfig } from 'src/core/server'; -import { kibanaResponseFactory } from 'src/core/server'; -import { - coreMock, - httpServerMock, - httpServiceMock, - loggingSystemMock, -} from 'src/core/server/mocks'; - -import { spacesConfig } from '../../../lib/__fixtures__'; -import { SpacesClientService } from '../../../spaces_client'; -import { SpacesService } from '../../../spaces_service'; -import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; -import { - createMockSavedObjectsRepository, - createMockSavedObjectsService, - createSpaces, - mockRouteContext, - mockRouteContextWithInvalidLicense, -} from '../__fixtures__'; -import { initShareToSpacesApi } from './share_to_space'; - -describe('share to space', () => { - const spacesSavedObjects = createSpaces(); - const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); - - const setup = async () => { - const httpService = httpServiceMock.createSetupContract(); - const router = httpService.createRouter(); - const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); - const log = loggingSystemMock.create().get('spaces'); - const coreStart = coreMock.createStart(); - const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); - coreStart.savedObjects = savedObjects; - - const clientService = new SpacesClientService(jest.fn()); - clientService - .setup({ config$: Rx.of(spacesConfig) }) - .setClientRepositoryFactory(() => savedObjectsRepositoryMock); - - const service = new SpacesService(); - service.setup({ - basePath: httpService.basePath, - }); - - const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); - - const clientServiceStart = clientService.start(coreStart); - - const spacesServiceStart = service.start({ - basePath: coreStart.http.basePath, - spacesClientService: clientServiceStart, - }); - initShareToSpacesApi({ - externalRouter: router, - getStartServices: async () => [coreStart, {}, {}], - log, - getSpacesService: () => spacesServiceStart, - usageStatsServicePromise, - }); - - const [ - [shareAdd, ctsRouteHandler], - [shareRemove, resolveRouteHandler], - ] = router.post.mock.calls; - - return { - coreStart, - savedObjectsClient, - shareAdd: { - routeValidation: shareAdd.validate as RouteValidatorConfig<{}, {}, {}>, - routeHandler: ctsRouteHandler, - }, - shareRemove: { - routeValidation: shareRemove.validate as RouteValidatorConfig<{}, {}, {}>, - routeHandler: resolveRouteHandler, - }, - savedObjectsRepositoryMock, - }; - }; - - describe('POST /api/spaces/_share_saved_object_add', () => { - const object = { id: 'foo', type: 'bar' }; - - it(`returns http/403 when the license is invalid`, async () => { - const { shareAdd } = await setup(); - - const request = httpServerMock.createKibanaRequest({ method: 'post' }); - const response = await shareAdd.routeHandler( - mockRouteContextWithInvalidLicense, - request, - kibanaResponseFactory - ); - - expect(response.status).toEqual(403); - expect(response.payload).toEqual({ - message: 'License is invalid for spaces', - }); - }); - - it(`requires at least 1 space ID`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: [], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: must specify one or more space ids"`); - }); - - it(`requires space IDs to be unique`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: ['a-space', 'a-space'], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: duplicate space ids are not allowed"`); - }); - - it(`requires well-formed space IDS`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot( - `"[spaces.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` - ); - }); - - it(`allows all spaces ("*")`, async () => { - const { shareAdd } = await setup(); - const payload = { spaces: ['*'], object }; - - expect(() => - (shareAdd.routeValidation.body as ObjectType).validate(payload) - ).not.toThrowError(); - }); - - it('adds the object to the specified space(s)', async () => { - const { shareAdd, savedObjectsClient } = await setup(); - const payload = { spaces: ['a-space', 'b-space'], object }; - - const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); - const response = await shareAdd.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - expect(status).toEqual(204); - expect(savedObjectsClient.addToNamespaces).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.addToNamespaces).toHaveBeenCalledWith( - payload.object.type, - payload.object.id, - payload.spaces - ); - }); - }); - - describe('POST /api/spaces/_share_saved_object_remove', () => { - const object = { id: 'foo', type: 'bar' }; - - it(`returns http/403 when the license is invalid`, async () => { - const { shareRemove } = await setup(); - - const request = httpServerMock.createKibanaRequest({ - method: 'post', - }); - - const response = await shareRemove.routeHandler( - mockRouteContextWithInvalidLicense, - request, - kibanaResponseFactory - ); - - expect(response.status).toEqual(403); - expect(response.payload).toEqual({ - message: 'License is invalid for spaces', - }); - }); - - it(`requires at least 1 space ID`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: [], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: must specify one or more space ids"`); - }); - - it(`requires space IDs to be unique`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: ['a-space', 'a-space'], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot(`"[spaces]: duplicate space ids are not allowed"`); - }); - - it(`requires well-formed space IDS`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: ['a-space', 'a-space-invalid-!@#$%^&*()'], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).toThrowErrorMatchingInlineSnapshot( - `"[spaces.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` - ); - }); - - it(`allows all spaces ("*")`, async () => { - const { shareRemove } = await setup(); - const payload = { spaces: ['*'], object }; - - expect(() => - (shareRemove.routeValidation.body as ObjectType).validate(payload) - ).not.toThrowError(); - }); - - it('removes the object from the specified space(s)', async () => { - const { shareRemove, savedObjectsClient } = await setup(); - const payload = { spaces: ['a-space', 'b-space'], object }; - - const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); - const response = await shareRemove.routeHandler( - mockRouteContext, - request, - kibanaResponseFactory - ); - - const { status } = response; - expect(status).toEqual(204); - expect(savedObjectsClient.deleteFromNamespaces).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.deleteFromNamespaces).toHaveBeenCalledWith( - payload.object.type, - payload.object.id, - payload.spaces - ); - }); - }); -}); diff --git a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts deleted file mode 100644 index 1c6f254354cb2..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/external/share_to_space.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; - -import { ALL_SPACES_ID } from '../../../../common/constants'; -import { wrapError } from '../../../lib/errors'; -import { SPACE_ID_REGEX } from '../../../lib/space_schema'; -import { createLicensedRouteHandler } from '../../lib'; -import type { ExternalRouteDeps } from './'; - -const uniq = (arr: T[]): T[] => Array.from(new Set(arr)); -export function initShareToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getStartServices } = deps; - - const shareSchema = schema.object({ - spaces: schema.arrayOf( - schema.string({ - validate: (value) => { - if (value !== ALL_SPACES_ID && !SPACE_ID_REGEX.test(value)) { - return `lower case, a-z, 0-9, "_", and "-" are allowed, OR "*"`; - } - }, - }), - { - validate: (spaceIds) => { - if (!spaceIds.length) { - return 'must specify one or more space ids'; - } else if (uniq(spaceIds).length !== spaceIds.length) { - return 'duplicate space ids are not allowed'; - } - }, - } - ), - object: schema.object({ type: schema.string(), id: schema.string() }), - }); - - externalRouter.post( - { path: '/api/spaces/_share_saved_object_add', validate: { body: shareSchema } }, - createLicensedRouteHandler(async (_context, request, response) => { - const [startServices] = await getStartServices(); - const scopedClient = startServices.savedObjects.getScopedClient(request); - - const spaces = request.body.spaces; - const { type, id } = request.body.object; - - try { - await scopedClient.addToNamespaces(type, id, spaces); - } catch (error) { - return response.customError(wrapError(error)); - } - return response.noContent(); - }) - ); - - externalRouter.post( - { path: '/api/spaces/_share_saved_object_remove', validate: { body: shareSchema } }, - createLicensedRouteHandler(async (_context, request, response) => { - const [startServices] = await getStartServices(); - const scopedClient = startServices.savedObjects.getScopedClient(request); - - const spaces = request.body.spaces; - const { type, id } = request.body.object; - - try { - await scopedClient.deleteFromNamespaces(type, id, spaces); - } catch (error) { - return response.customError(wrapError(error)); - } - return response.noContent(); - }) - ); -} diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts new file mode 100644 index 0000000000000..06968c3bcb50e --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts @@ -0,0 +1,176 @@ +/* + * 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 * as Rx from 'rxjs'; + +import type { ObjectType } from '@kbn/config-schema'; +import type { RouteValidatorConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { + coreMock, + httpServerMock, + httpServiceMock, + loggingSystemMock, +} from 'src/core/server/mocks'; + +import { spacesConfig } from '../../../lib/__fixtures__'; +import { SpacesClientService } from '../../../spaces_client'; +import { SpacesService } from '../../../spaces_service'; +import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock'; +import { + createMockSavedObjectsRepository, + createMockSavedObjectsService, + createSpaces, + mockRouteContext, + mockRouteContextWithInvalidLicense, +} from '../__fixtures__'; +import { initUpdateObjectsSpacesApi } from './update_objects_spaces'; + +describe('update_objects_spaces', () => { + const spacesSavedObjects = createSpaces(); + const spaces = spacesSavedObjects.map((s) => ({ id: s.id, ...s.attributes })); + + const setup = async () => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter(); + const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); + const log = loggingSystemMock.create().get('spaces'); + const coreStart = coreMock.createStart(); + const { savedObjects, savedObjectsClient } = createMockSavedObjectsService(spaces); + coreStart.savedObjects = savedObjects; + + const clientService = new SpacesClientService(jest.fn()); + clientService + .setup({ config$: Rx.of(spacesConfig) }) + .setClientRepositoryFactory(() => savedObjectsRepositoryMock); + + const service = new SpacesService(); + service.setup({ + basePath: httpService.basePath, + }); + + const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract()); + + const clientServiceStart = clientService.start(coreStart); + + const spacesServiceStart = service.start({ + basePath: coreStart.http.basePath, + spacesClientService: clientServiceStart, + }); + initUpdateObjectsSpacesApi({ + externalRouter: router, + getStartServices: async () => [coreStart, {}, {}], + log, + getSpacesService: () => spacesServiceStart, + usageStatsServicePromise, + }); + + const [[updateObjectsSpaces, updateObjectsSpacesRouteHandler]] = router.post.mock.calls; + + return { + coreStart, + savedObjectsClient, + updateObjectsSpaces: { + routeValidation: updateObjectsSpaces.validate as RouteValidatorConfig<{}, {}, {}>, + routeHandler: updateObjectsSpacesRouteHandler, + }, + savedObjectsRepositoryMock, + }; + }; + + describe('POST /api/spaces/_update_objects_spaces', () => { + const objects = [{ id: 'foo', type: 'bar' }]; + + it(`returns http/403 when the license is invalid`, async () => { + const { updateObjectsSpaces } = await setup(); + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + }); + + const response = await updateObjectsSpaces.routeHandler( + mockRouteContextWithInvalidLicense, + request, + kibanaResponseFactory + ); + + expect(response.status).toEqual(403); + expect(response.payload).toEqual({ + message: 'License is invalid for spaces', + }); + }); + + it(`requires space IDs to be unique`, async () => { + const { updateObjectsSpaces } = await setup(); + const targetSpaces = ['a-space', 'a-space']; + const payload1 = { objects, spacesToAdd: targetSpaces, spacesToRemove: [] }; + const payload2 = { objects, spacesToAdd: [], spacesToRemove: targetSpaces }; + + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload1) + ).toThrowErrorMatchingInlineSnapshot(`"[spacesToAdd]: duplicate space ids are not allowed"`); + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload2) + ).toThrowErrorMatchingInlineSnapshot( + `"[spacesToRemove]: duplicate space ids are not allowed"` + ); + }); + + it(`requires well-formed space IDS`, async () => { + const { updateObjectsSpaces } = await setup(); + const targetSpaces = ['a-space', 'a-space-invalid-!@#$%^&*()']; + const payload1 = { objects, spacesToAdd: targetSpaces, spacesToRemove: [] }; + const payload2 = { objects, spacesToAdd: [], spacesToRemove: targetSpaces }; + + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload1) + ).toThrowErrorMatchingInlineSnapshot( + `"[spacesToAdd.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` + ); + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload2) + ).toThrowErrorMatchingInlineSnapshot( + `"[spacesToRemove.1]: lower case, a-z, 0-9, \\"_\\", and \\"-\\" are allowed, OR \\"*\\""` + ); + }); + + it(`allows all spaces ("*")`, async () => { + const { updateObjectsSpaces } = await setup(); + const targetSpaces = ['*']; + const payload1 = { objects, spacesToAdd: targetSpaces, spacesToRemove: [] }; + const payload2 = { objects, spacesToAdd: [], spacesToRemove: targetSpaces }; + + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload1) + ).not.toThrowError(); + expect(() => + (updateObjectsSpaces.routeValidation.body as ObjectType).validate(payload2) + ).not.toThrowError(); + }); + + it('passes arguments to the saved objects client and returns the result', async () => { + const { updateObjectsSpaces, savedObjectsClient } = await setup(); + const payload = { objects, spacesToAdd: ['a-space'], spacesToRemove: ['b-space'] }; + + const request = httpServerMock.createKibanaRequest({ body: payload, method: 'post' }); + const response = await updateObjectsSpaces.routeHandler( + mockRouteContext, + request, + kibanaResponseFactory + ); + + const { status } = response; + expect(status).toEqual(200); + expect(savedObjectsClient.updateObjectsSpaces).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + payload.spacesToAdd, + payload.spacesToRemove + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts new file mode 100644 index 0000000000000..4486d4b3ade09 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { ALL_SPACES_ID } from '../../../../common/constants'; +import { wrapError } from '../../../lib/errors'; +import { SPACE_ID_REGEX } from '../../../lib/space_schema'; +import { createLicensedRouteHandler } from '../../lib'; +import type { ExternalRouteDeps } from './'; + +export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { + const { externalRouter, getStartServices } = deps; + + const spacesSchema = schema.arrayOf( + schema.string({ + validate: (value) => { + if (value !== ALL_SPACES_ID && !SPACE_ID_REGEX.test(value)) { + return `lower case, a-z, 0-9, "_", and "-" are allowed, OR "*"`; + } + }, + }), + { + validate: (spaceIds) => { + if (uniq(spaceIds).length !== spaceIds.length) { + return 'duplicate space ids are not allowed'; + } + }, + } + ); + + externalRouter.post( + { + path: '/api/spaces/_update_objects_spaces', + validate: { + body: schema.object({ + objects: schema.arrayOf(schema.object({ type: schema.string(), id: schema.string() })), + spacesToAdd: spacesSchema, + spacesToRemove: spacesSchema, + }), + }, + }, + createLicensedRouteHandler(async (_context, request, response) => { + const [startServices] = await getStartServices(); + const scopedClient = startServices.savedObjects.getScopedClient(request); + + const { objects, spacesToAdd, spacesToRemove } = request.body; + + try { + const updateObjectsSpacesResponse = await scopedClient.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove + ); + return response.ok({ body: updateObjectsSpacesResponse }); + } catch (error) { + return response.customError(wrapError(error)); + } + }) + ); +} + +/** Returns all unique elements of an array. */ +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); +} diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index cbb71d4bbcf81..56bfe71b581ed 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -503,66 +503,6 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); }); - describe('#addToNamespaces', () => { - test(`throws error if options.namespace is specified`, async () => { - const { client } = createSpacesSavedObjectsClient(); - - await expect( - // @ts-expect-error - client.addToNamespaces(null, null, null, { namespace: 'bar' }) - ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); - }); - - test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = createSpacesSavedObjectsClient(); - const expectedReturnValue = { namespaces: ['foo', 'bar'] }; - baseClient.addToNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Object.freeze({ foo: 'bar' }); - // @ts-expect-error - const actualReturnValue = await client.addToNamespaces(type, id, namespaces, options); - - expect(actualReturnValue).toBe(expectedReturnValue); - expect(baseClient.addToNamespaces).toHaveBeenCalledWith(type, id, namespaces, { - foo: 'bar', - namespace: currentSpace.expectedNamespace, - }); - }); - }); - - describe('#deleteFromNamespaces', () => { - test(`throws error if options.namespace is specified`, async () => { - const { client } = createSpacesSavedObjectsClient(); - - await expect( - // @ts-expect-error - client.deleteFromNamespaces(null, null, null, { namespace: 'bar' }) - ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); - }); - - test(`supplements options with the current namespace`, async () => { - const { client, baseClient } = createSpacesSavedObjectsClient(); - const expectedReturnValue = { namespaces: ['foo', 'bar'] }; - baseClient.deleteFromNamespaces.mockReturnValue(Promise.resolve(expectedReturnValue)); - - const type = Symbol(); - const id = Symbol(); - const namespaces = Symbol(); - const options = Object.freeze({ foo: 'bar' }); - // @ts-expect-error - const actualReturnValue = await client.deleteFromNamespaces(type, id, namespaces, options); - - expect(actualReturnValue).toBe(expectedReturnValue); - expect(baseClient.deleteFromNamespaces).toHaveBeenCalledWith(type, id, namespaces, { - foo: 'bar', - namespace: currentSpace.expectedNamespace, - }); - }); - }); - describe('#removeReferencesTo', () => { test(`throws error if options.namespace is specified`, async () => { const { client } = createSpacesSavedObjectsClient(); @@ -681,5 +621,70 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; expect(baseClient.createPointInTimeFinder).toHaveBeenCalledWith(options, dependencies); }); }); + + describe('#collectMultiNamespaceReferences', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = createSpacesSavedObjectsClient(); + + await expect( + client.collectMultiNamespaceReferences([], { namespace: 'bar' }) + ).rejects.toThrow(ERROR_NAMESPACE_SPECIFIED); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + const expectedReturnValue = { objects: [] }; + baseClient.collectMultiNamespaceReferences.mockReturnValue( + Promise.resolve(expectedReturnValue) + ); + + const objects = [{ type: 'foo', id: 'bar' }]; + const options = Object.freeze({ foo: 'bar' }); + // @ts-expect-error + const actualReturnValue = await client.collectMultiNamespaceReferences(objects, options); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.collectMultiNamespaceReferences).toHaveBeenCalledWith(objects, { + foo: 'bar', + namespace: currentSpace.expectedNamespace, + }); + }); + }); + + describe('#updateObjectsSpaces', () => { + test(`throws error if options.namespace is specified`, async () => { + const { client } = createSpacesSavedObjectsClient(); + + await expect(client.updateObjectsSpaces([], [], [], { namespace: 'bar' })).rejects.toThrow( + ERROR_NAMESPACE_SPECIFIED + ); + }); + + test(`supplements options with the current namespace`, async () => { + const { client, baseClient } = createSpacesSavedObjectsClient(); + const expectedReturnValue = { objects: [] }; + baseClient.updateObjectsSpaces.mockReturnValue(Promise.resolve(expectedReturnValue)); + + const objects = [{ type: 'foo', id: 'bar' }]; + const spacesToAdd = ['space-x']; + const spacesToRemove = ['space-y']; + const options = Object.freeze({ foo: 'bar' }); + const actualReturnValue = await client.updateObjectsSpaces( + objects, + spacesToAdd, + spacesToRemove, + // @ts-expect-error + options + ); + + expect(actualReturnValue).toBe(expectedReturnValue); + expect(baseClient.updateObjectsSpaces).toHaveBeenCalledWith( + objects, + spacesToAdd, + spacesToRemove, + { foo: 'bar', namespace: currentSpace.expectedNamespace } + ); + }); + }); }); }); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 4254615ac7d5f..e344aa8cecf07 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -9,7 +9,6 @@ import Boom from '@hapi/boom'; import type { ISavedObjectTypeRegistry, - SavedObjectsAddToNamespacesOptions, SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -17,13 +16,17 @@ import type { SavedObjectsCheckConflictsObject, SavedObjectsClientContract, SavedObjectsClosePointInTimeOptions, + SavedObjectsCollectMultiNamespaceReferencesObject, + SavedObjectsCollectMultiNamespaceReferencesOptions, + SavedObjectsCollectMultiNamespaceReferencesResponse, SavedObjectsCreateOptions, SavedObjectsCreatePointInTimeFinderDependencies, SavedObjectsCreatePointInTimeFinderOptions, - SavedObjectsDeleteFromNamespacesOptions, SavedObjectsFindOptions, SavedObjectsOpenPointInTimeOptions, SavedObjectsRemoveReferencesToOptions, + SavedObjectsUpdateObjectsSpacesObject, + SavedObjectsUpdateObjectsSpacesOptions, SavedObjectsUpdateOptions, } from 'src/core/server'; @@ -300,86 +303,80 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { } /** - * Adds namespaces to a SavedObject + * Updates an array of objects by id * - * @param type - * @param id - * @param namespaces - * @param options + * @param {array} objects - an array ids, or an array of objects containing id, type, attributes and optionally version, references and namespace + * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } + * @example + * + * bulkUpdate([ + * { id: 'one', type: 'config', attributes: { title: 'My new title'}, version: 'd7rhfk47d=' }, + * { id: 'foo', type: 'index-pattern', attributes: {} } + * ]) */ - public async addToNamespaces( - type: string, - id: string, - namespaces: string[], - options: SavedObjectsAddToNamespacesOptions = {} + public async bulkUpdate( + objects: Array> = [], + options: SavedObjectsBaseOptions = {} ) { throwErrorIfNamespaceSpecified(options); - - return await this.client.addToNamespaces(type, id, namespaces, { + return await this.client.bulkUpdate(objects, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); } /** - * Removes namespaces from a SavedObject + * Remove outward references to given object. * * @param type * @param id - * @param namespaces * @param options */ - public async deleteFromNamespaces( + public async removeReferencesTo( type: string, id: string, - namespaces: string[], - options: SavedObjectsDeleteFromNamespacesOptions = {} + options: SavedObjectsRemoveReferencesToOptions = {} ) { throwErrorIfNamespaceSpecified(options); - - return await this.client.deleteFromNamespaces(type, id, namespaces, { + return await this.client.removeReferencesTo(type, id, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); } /** - * Updates an array of objects by id + * Gets all references and transitive references of the listed objects. Ignores any object that is not a multi-namespace type. * - * @param {array} objects - an array ids, or an array of objects containing id, type, attributes and optionally version, references and namespace - * @returns {promise} - { saved_objects: [{ id, type, version, attributes }] } - * @example - * - * bulkUpdate([ - * { id: 'one', type: 'config', attributes: { title: 'My new title'}, version: 'd7rhfk47d=' }, - * { id: 'foo', type: 'index-pattern', attributes: {} } - * ]) + * @param objects + * @param options */ - public async bulkUpdate( - objects: Array> = [], - options: SavedObjectsBaseOptions = {} - ) { + public async collectMultiNamespaceReferences( + objects: SavedObjectsCollectMultiNamespaceReferencesObject[], + options: SavedObjectsCollectMultiNamespaceReferencesOptions = {} + ): Promise { throwErrorIfNamespaceSpecified(options); - return await this.client.bulkUpdate(objects, { + return await this.client.collectMultiNamespaceReferences(objects, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); } /** - * Remove outward references to given object. + * Updates one or more objects to add and/or remove them from specified spaces. * - * @param type - * @param id + * @param objects + * @param spacesToAdd + * @param spacesToRemove * @param options */ - public async removeReferencesTo( - type: string, - id: string, - options: SavedObjectsRemoveReferencesToOptions = {} + public async updateObjectsSpaces( + objects: SavedObjectsUpdateObjectsSpacesObject[], + spacesToAdd: string[], + spacesToRemove: string[], + options: SavedObjectsUpdateObjectsSpacesOptions = {} ) { throwErrorIfNamespaceSpecified(options); - return await this.client.removeReferencesTo(type, id, { + return await this.client.updateObjectsSpaces(objects, spacesToAdd, spacesToRemove, { ...options, namespace: spaceIdToNamespace(this.spaceId), }); @@ -434,7 +431,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { * @param {object} findOptions - {@link SavedObjectsCreatePointInTimeFinderOptions} * @param {object} [dependencies] - {@link SavedObjectsCreatePointInTimeFinderDependencies} */ - createPointInTimeFinder( + createPointInTimeFinder( findOptions: SavedObjectsCreatePointInTimeFinderOptions, dependencies?: SavedObjectsCreatePointInTimeFinderDependencies ) { @@ -443,7 +440,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { // is simply a helper that calls `find`, `openPointInTimeForType`, and // `closePointInTime` internally, so namespaces will already be handled // in those methods. - return this.client.createPointInTimeFinder(findOptions, { + return this.client.createPointInTimeFinder(findOptions, { client: this, // Include dependencies last so that subsequent SO client wrappers have their settings applied. ...dependencies, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 244b294baffe5..ae61f24201ce5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22821,8 +22821,6 @@ "xpack.spaces.shareToSpace.privilegeWarningTitle": "追加の権限が必要です", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.text": "検索している{objectNoun}は新しい場所にあります。今後はこのURLを使用してください。", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.title": "新しいURLに移動しました", - "xpack.spaces.shareToSpace.shareAddSuccessTextPlural": "「{object}」は{spaceTargets}個のスペースに追加されました。", - "xpack.spaces.shareToSpace.shareAddSuccessTextSingular": "「{object}」は1つのスペースに追加されました。", "xpack.spaces.shareToSpace.shareErrorTitle": "{objectNoun}の更新エラー", "xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount}個が非表示", "xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount}個が選択済み", @@ -22833,8 +22831,6 @@ "xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.title": "すべてのスペース", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.text": "選択したスペースでのみ{objectNoun}を使用可能にします。", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.title": "スペースを選択", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextPlural": "「{object}」は{spaceTargets}個のスペースから削除されました。", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextSingular": "「{object}」は1つのスペースから削除されました。", "xpack.spaces.shareToSpace.shareSuccessTitle": "{objectNoun}を更新しました", "xpack.spaces.shareToSpace.shareToSpacesButton": "保存して閉じる", "xpack.spaces.shareToSpace.shareWarningBody": "変更は選択した各スペースに表示されます。変更を同期しない場合は、{makeACopyLink}。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8bdffce98d4ab..ecd6c0d68a94f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23182,8 +23182,6 @@ "xpack.spaces.shareToSpace.privilegeWarningTitle": "需要其他权限", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.text": "您正在寻找的{objectNoun}具有新的位置。从现在开始使用此 URL。", "xpack.spaces.shareToSpace.redirectLegacyUrlToast.title": "我们已将您重定向到新 URL", - "xpack.spaces.shareToSpace.shareAddSuccessTextPlural": "“{object}”已添加到 {spaceTargets} 个工作区。", - "xpack.spaces.shareToSpace.shareAddSuccessTextSingular": "“{object}”已添加到 1 个工作区。", "xpack.spaces.shareToSpace.shareErrorTitle": "更新 {objectNoun} 时出错", "xpack.spaces.shareToSpace.shareModeControl.hiddenCountLabel": "+{hiddenCount} 个已隐藏", "xpack.spaces.shareToSpace.shareModeControl.selectedCountLabel": "{selectedCount} 个已选择", @@ -23194,8 +23192,6 @@ "xpack.spaces.shareToSpace.shareModeControl.shareToAllSpaces.title": "所有工作区", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.text": "仅使 {objectNoun} 在选定工作区中可用。", "xpack.spaces.shareToSpace.shareModeControl.shareToExplicitSpaces.title": "选择工作区", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextPlural": "“{object}”已从 {spaceTargets} 个工作区中移除。", - "xpack.spaces.shareToSpace.shareRemoveSuccessTextSingular": "“{object}”已从 1 个工作区中移除。", "xpack.spaces.shareToSpace.shareSuccessTitle": "已更新 {objectNoun}", "xpack.spaces.shareToSpace.shareToSpacesButton": "保存并关闭", "xpack.spaces.shareToSpace.shareWarningBody": "您的更改显示在您选择的每个工作区中。如果不想同步您的更改,{makeACopyLink}。", diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts b/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts index c7af01c60fa52..19d50474fcc73 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts @@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => { idStarSpace ); - await ml.api.asignJobToSpaces(adJobIdSpace12, 'anomaly-detector', [idSpace2], idSpace1); + await ml.api.updateJobSpaces(adJobIdSpace12, 'anomaly-detector', [idSpace2], [], idSpace1); await ml.api.assertJobSpaces(adJobIdSpace12, 'anomaly-detector', [idSpace1, idSpace2]); await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/index.ts b/x-pack/test/api_integration/apis/ml/saved_objects/index.ts index a4e9458609b0c..99ef48b2337d5 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/index.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/index.ts @@ -10,11 +10,10 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('saved objects', function () { loadTestFile(require.resolve('./jobs_spaces')); - loadTestFile(require.resolve('./assign_job_to_space')); loadTestFile(require.resolve('./can_delete_job')); loadTestFile(require.resolve('./initialize')); loadTestFile(require.resolve('./status')); - loadTestFile(require.resolve('./remove_job_from_space')); loadTestFile(require.resolve('./sync')); + loadTestFile(require.resolve('./update_jobs_spaces')); }); } diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts b/x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts deleted file mode 100644 index dec4523d39535..0000000000000 --- a/x-pack/test/api_integration/apis/ml/saved_objects/remove_job_from_space.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../functional/services/ml/security_common'; -import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; -import { JobType } from '../../../../../plugins/ml/common/types/saved_objects'; - -export default ({ getService }: FtrProviderContext) => { - const ml = getService('ml'); - const spacesService = getService('spaces'); - const supertest = getService('supertestWithoutAuth'); - - const adJobId = 'fq_single'; - const idSpace1 = 'space1'; - const idSpace2 = 'space2'; - - async function runRequest( - requestBody: { - jobType: JobType; - jobIds: string[]; - spaces: string[]; - }, - expectedStatusCode: number, - user: USER, - space?: string - ) { - const { body } = await supertest - .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/remove_job_from_space`) - .auth(user, ml.securityCommon.getPasswordForUser(user)) - .set(COMMON_REQUEST_HEADERS) - .send(requestBody) - .expect(expectedStatusCode); - - return body; - } - - describe('POST saved_objects/remove_job_from_space', () => { - before(async () => { - await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); - await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); - - await ml.testResources.setKibanaTimeZoneToUTC(); - }); - - beforeEach(async () => { - await ml.api.createAnomalyDetectionJob( - ml.commonConfig.getADFqSingleMetricJobConfig(adJobId), - idSpace1 - ); - }); - - afterEach(async () => { - await ml.api.cleanMlIndices(); - await ml.testResources.cleanMLSavedObjects(); - }); - - after(async () => { - await spacesService.delete(idSpace1); - await spacesService.delete(idSpace2); - }); - - it('should remove job from same space', async () => { - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); - - const body = await runRequest( - { - jobType: 'anomaly-detector', - jobIds: [adJobId], - spaces: [idSpace1], - }, - 200, - USER.ML_POWERUSER_ALL_SPACES, - idSpace1 - ); - - expect(body).to.eql({ [adJobId]: { success: true } }); - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', []); - }); - - it('should not find job to remove from different space', async () => { - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); - - const body = await runRequest( - { - jobType: 'anomaly-detector', - jobIds: [adJobId], - spaces: [idSpace1], - }, - 200, - USER.ML_POWERUSER_ALL_SPACES, - idSpace2 - ); - - expect(body).to.eql({}); - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); - }); - }); -}; diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/assign_job_to_space.ts b/x-pack/test/api_integration/apis/ml/saved_objects/update_jobs_spaces.ts similarity index 85% rename from x-pack/test/api_integration/apis/ml/saved_objects/assign_job_to_space.ts rename to x-pack/test/api_integration/apis/ml/saved_objects/update_jobs_spaces.ts index 12bd89716c044..89233fe11dbc6 100644 --- a/x-pack/test/api_integration/apis/ml/saved_objects/assign_job_to_space.ts +++ b/x-pack/test/api_integration/apis/ml/saved_objects/update_jobs_spaces.ts @@ -27,13 +27,14 @@ export default ({ getService }: FtrProviderContext) => { requestBody: { jobType: JobType; jobIds: string[]; - spaces: string[]; + spacesToAdd: string[]; + spacesToRemove: string[]; }, expectedStatusCode: number, user: USER ) { const { body } = await supertest - .post(`/api/ml/saved_objects/assign_job_to_space`) + .post(`/api/ml/saved_objects/update_jobs_spaces`) .auth(user, ml.securityCommon.getPasswordForUser(user)) .set(COMMON_REQUEST_HEADERS) .send(requestBody) @@ -42,7 +43,7 @@ export default ({ getService }: FtrProviderContext) => { return body; } - describe('POST saved_objects/assign_job_to_space', () => { + describe('POST saved_objects/update_jobs_spaces', () => { before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); @@ -74,14 +75,15 @@ export default ({ getService }: FtrProviderContext) => { { jobType: 'anomaly-detector', jobIds: [adJobId], - spaces: [idSpace1], + spacesToAdd: [idSpace1], + spacesToRemove: [defaultSpaceId], }, 200, USER.ML_POWERUSER_SPACE1 ); expect(body).to.eql({ [adJobId]: { success: true } }); - await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [defaultSpaceId, idSpace1]); + await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [idSpace1]); }); it('should assign DFA job to space for user with access to that space', async () => { @@ -90,23 +92,25 @@ export default ({ getService }: FtrProviderContext) => { { jobType: 'data-frame-analytics', jobIds: [dfaJobId], - spaces: [idSpace1], + spacesToAdd: [idSpace1], + spacesToRemove: [defaultSpaceId], }, 200, USER.ML_POWERUSER_SPACE1 ); expect(body).to.eql({ [dfaJobId]: { success: true } }); - await ml.api.assertJobSpaces(dfaJobId, 'data-frame-analytics', [defaultSpaceId, idSpace1]); + await ml.api.assertJobSpaces(dfaJobId, 'data-frame-analytics', [idSpace1]); }); - it('should fail to assign AD job to space the user has no access to', async () => { + it('should fail to update AD job spaces for space the user has no access to', async () => { await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [defaultSpaceId]); const body = await runRequest( { jobType: 'anomaly-detector', jobIds: [adJobId], - spaces: [idSpace2], + spacesToAdd: [idSpace2], + spacesToRemove: [], }, 200, USER.ML_POWERUSER_SPACE1 @@ -116,13 +120,14 @@ export default ({ getService }: FtrProviderContext) => { await ml.api.assertJobSpaces(adJobId, 'anomaly-detector', [defaultSpaceId]); }); - it('should fail to assign DFA job to space the user has no access to', async () => { + it('should fail to update DFA job spaces for space the user has no access to', async () => { await ml.api.assertJobSpaces(dfaJobId, 'data-frame-analytics', [defaultSpaceId]); const body = await runRequest( { jobType: 'data-frame-analytics', jobIds: [dfaJobId], - spaces: [idSpace2], + spacesToAdd: [idSpace2], + spacesToRemove: [], }, 200, USER.ML_POWERUSER_SPACE1 diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index c0e3dedd8e191..d341a27455a3c 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -892,26 +892,17 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { await this.waitForAnalyticsState(dfaConfig.id, DATA_FRAME_TASK_STATE.STOPPED); }, - async asignJobToSpaces(jobId: string, jobType: JobType, spacesToAdd: string[], space?: string) { - const { body } = await kbnSupertest - .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/assign_job_to_space`) - .set(COMMON_REQUEST_HEADERS) - .send({ jobType, jobIds: [jobId], spaces: spacesToAdd }) - .expect(200); - - expect(body).to.eql({ [jobId]: { success: true } }); - }, - - async removeJobFromSpaces( + async updateJobSpaces( jobId: string, jobType: JobType, + spacesToAdd: string[], spacesToRemove: string[], space?: string ) { const { body } = await kbnSupertest - .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/remove_job_from_space`) + .post(`${space ? `/s/${space}` : ''}/api/ml/saved_objects/update_jobs_spaces`) .set(COMMON_REQUEST_HEADERS) - .send({ jobType, jobIds: [jobId], spaces: spacesToRemove }) + .send({ jobType, jobIds: [jobId], spacesToAdd, spacesToRemove }) .expect(200); expect(body).to.eql({ [jobId]: { success: true } }); diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 5fac012d5e8b9..d83c550c15ff6 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -544,6 +544,7 @@ "type": "legacy-url-alias", "updated_at": "2017-09-21T18:51:23.794Z", "legacy-url-alias": { + "sourceId": "alias-match", "targetNamespace": "space_1", "targetType": "resolvetype", "targetId": "alias-match-newid" @@ -561,6 +562,7 @@ "type": "legacy-url-alias", "updated_at": "2017-09-21T18:51:23.794Z", "legacy-url-alias": { + "sourceId": "disabled", "targetNamespace": "space_1", "targetType": "resolvetype", "targetId": "alias-match-newid", @@ -611,6 +613,7 @@ "type": "legacy-url-alias", "updated_at": "2017-09-21T18:51:23.794Z", "legacy-url-alias": { + "sourceId": "conflict", "targetNamespace": "space_1", "targetType": "resolvetype", "targetId": "conflict-newid" diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 5ce6c0ce6b7c5..ed52be26c7e53 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -388,6 +388,9 @@ }, "type": "sharedtype", "namespaces": ["default"], + "references": [ + { "type": "sharedtype", "id": "each_space", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" @@ -405,12 +408,33 @@ }, "type": "sharedtype", "namespaces": ["space_1"], + "references": [ + { "type": "sharedtype", "id": "each_space", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" } } +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:space_1:sharedtype:default_only", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "default_only", + "targetNamespace": "space_1", + "targetType": "sharedtype", + "targetId": "space_1_only" + } + } + } +} + { "type": "doc", "value": { @@ -422,12 +446,52 @@ }, "type": "sharedtype", "namespaces": ["space_2"], + "references": [ + { "type": "sharedtype", "id": "each_space", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" } } +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:space_2:sharedtype:default_only", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "default_only", + "targetNamespace": "space_2", + "targetType": "sharedtype", + "targetId": "space_2_only" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "legacy-url-alias:other_space:sharedtype:default_only", + "index": ".kibana", + "source": { + "type": "legacy-url-alias", + "updated_at": "2017-09-21T18:51:23.794Z", + "legacy-url-alias": { + "sourceId": "default_only", + "targetNamespace": "other_space", + "targetType": "sharedtype", + "targetId": "other_id", + "disabled": true + } + } + } +} + { "type": "doc", "value": { @@ -490,6 +554,12 @@ }, "type": "sharedtype", "namespaces": ["default", "space_1", "space_2"], + "references": [ + { "type": "sharedtype", "id": "default_only", "name": "refname" }, + { "type": "sharedtype", "id": "space_1_only", "name": "refname" }, + { "type": "sharedtype", "id": "space_2_only", "name": "refname" }, + { "type": "sharedtype", "id": "all_spaces", "name": "refname" } + ], "updated_at": "2017-09-21T18:59:16.270Z" }, "type": "doc" diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index f26edf71b482c..e264e574a3cea 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -37,7 +37,10 @@ interface CopyToSpaceTests { withConflictsResponse: (resp: TestResponse) => Promise; noConflictsResponse: (resp: TestResponse) => Promise; }; - multiNamespaceTestCases: (overwrite: boolean) => CopyToSpaceMultiNamespaceTest[]; + multiNamespaceTestCases: ( + overwrite: boolean, + createNewCopies: boolean + ) => CopyToSpaceMultiNamespaceTest[]; } interface CopyToSpaceTestDefinition { @@ -427,7 +430,7 @@ export function copyToSpaceTestSuiteFactory( const createMultiNamespaceTestCases = ( spaceId: string, outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized' - ) => (overwrite: boolean): CopyToSpaceMultiNamespaceTest[] => { + ) => (overwrite: boolean, createNewCopies: boolean): CopyToSpaceMultiNamespaceTest[] => { // the status code of the HTTP response differs depending on the error type // a 403 error actually comes back as an HTTP 200 response const statusCode = outcome === 'noAccess' ? 403 : 200; @@ -451,6 +454,17 @@ export function copyToSpaceTestSuiteFactory( }); }; + const expectNewCopyResponse = (response: TestResponse, sourceId: string, title: string) => { + const { success, successCount, successResults, errors } = getResult(response); + expect(success).to.eql(true); + expect(successCount).to.eql(1); + const destinationId = successResults![0].destinationId; + expect(destinationId).to.match(v4); + const meta = { title, icon: 'beaker' }; + expect(successResults).to.eql([{ type, id: sourceId, meta, destinationId }]); + expect(errors).to.be(undefined); + }; + return [ { testTitle: 'copying with no conflict', @@ -458,14 +472,10 @@ export function copyToSpaceTestSuiteFactory( statusCode, response: async (response: TestResponse) => { if (outcome === 'authorized') { - const { success, successCount, successResults, errors } = getResult(response); - expect(success).to.eql(true); - expect(successCount).to.eql(1); - const destinationId = successResults![0].destinationId; - expect(destinationId).to.match(v4); - const meta = { title: 'A shared saved-object in one space', icon: 'beaker' }; - expect(successResults).to.eql([{ type, id: noConflictId, meta, destinationId }]); - expect(errors).to.be(undefined); + const title = 'A shared saved-object in one space'; + // It doesn't matter if createNewCopies is enabled or not, a new copy will be created because two objects cannot exist with the same ID. + // Note: if createNewCopies is disabled, the new object will have an originId property that matches the source ID, but this is not included in the HTTP response. + expectNewCopyResponse(response, noConflictId, title); } else if (outcome === 'noAccess') { expectRouteForbiddenResponse(response); } else { @@ -479,22 +489,23 @@ export function copyToSpaceTestSuiteFactory( objects: [{ type, id: exactMatchId }], statusCode, response: async (response: TestResponse) => { - if (outcome === 'authorized') { + if (outcome === 'authorized' || (outcome === 'unauthorizedWrite' && !createNewCopies)) { + // If the user is authorized to read in the current space, and is authorized to read in the destination space but not to write + // (outcome === 'unauthorizedWrite'), *and* createNewCopies is not enabled, the object will be skipped (because it already + // exists in the destination space) and the user will encounter an empty success result. + // On the other hand, if the user is authorized to read in the current space but not the destination space (outcome === + // 'unauthorizedRead'), the copy attempt will proceed because they are not aware that the object already exists in the + // destination space. In that case, they will encounter a 403 error. const { success, successCount, successResults, errors } = getResult(response); const title = 'A shared saved-object in the default, space_1, and space_2 spaces'; - const meta = { title, icon: 'beaker' }; - if (overwrite) { - expect(success).to.eql(true); - expect(successCount).to.eql(1); - expect(successResults).to.eql([{ type, id: exactMatchId, meta, overwrite: true }]); - expect(errors).to.be(undefined); + if (createNewCopies) { + expectNewCopyResponse(response, exactMatchId, title); } else { - expect(success).to.eql(false); + // It doesn't matter if overwrite is enabled or not, the object will not be copied because it already exists in the destination space + expect(success).to.eql(true); expect(successCount).to.eql(0); expect(successResults).to.be(undefined); - expect(errors).to.eql([ - { error: { type: 'conflict' }, type, id: exactMatchId, title, meta }, - ]); + expect(errors).to.be(undefined); } } else if (outcome === 'noAccess') { expectRouteForbiddenResponse(response); @@ -514,7 +525,9 @@ export function copyToSpaceTestSuiteFactory( const title = 'A shared saved-object in one space'; const meta = { title, icon: 'beaker' }; const destinationId = 'conflict_1_space_2'; - if (overwrite) { + if (createNewCopies) { + expectNewCopyResponse(response, inexactMatchId, title); + } else if (overwrite) { expect(success).to.eql(true); expect(successCount).to.eql(1); expect(successResults).to.eql([ @@ -550,27 +563,34 @@ export function copyToSpaceTestSuiteFactory( response: async (response: TestResponse) => { if (outcome === 'authorized') { const { success, successCount, successResults, errors } = getResult(response); - const updatedAt = '2017-09-21T18:59:16.270Z'; - const destinations = [ - // response should be sorted by updatedAt in descending order - { id: 'conflict_2_space_2', title: 'A shared saved-object in one space', updatedAt }, - { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, - ]; - expect(success).to.eql(false); - expect(successCount).to.eql(0); - expect(successResults).to.be(undefined); - expect(errors).to.eql([ - { - error: { type: 'ambiguous_conflict', destinations }, - type, - id: ambiguousConflictId, - title: 'A shared saved-object in one space', - meta: { + const title = 'A shared saved-object in one space'; + if (createNewCopies) { + expectNewCopyResponse(response, ambiguousConflictId, title); + } else { + // It doesn't matter if overwrite is enabled or not, the object will not be copied because there are two matches in the destination space + const updatedAt = '2017-09-21T18:59:16.270Z'; + const destinations = [ + // response should be sorted by updatedAt in descending order + { + id: 'conflict_2_space_2', title: 'A shared saved-object in one space', - icon: 'beaker', + updatedAt, }, - }, - ]); + { id: 'conflict_2_all', title: 'A shared saved-object in all spaces', updatedAt }, + ]; + expect(success).to.eql(false); + expect(successCount).to.eql(0); + expect(successResults).to.be(undefined); + expect(errors).to.eql([ + { + error: { type: 'ambiguous_conflict', destinations }, + type, + id: ambiguousConflictId, + title, + meta: { title, icon: 'beaker' }, + }, + ]); + } } else if (outcome === 'noAccess') { expectRouteForbiddenResponse(response); } else { @@ -726,15 +746,19 @@ export function copyToSpaceTestSuiteFactory( }); }); - [false, true].forEach((overwrite) => { + [ + [false, false], + [false, true], // createNewCopies enabled + [true, false], // overwrite enabled + // we don't specify tese cases with both overwrite and createNewCopies enabled, since overwrite won't matter in that scenario + ].forEach(([overwrite, createNewCopies]) => { const spaces = ['space_2']; const includeReferences = false; - const createNewCopies = false; - describe(`multi-namespace types with overwrite=${overwrite}`, () => { + describe(`multi-namespace types with overwrite=${overwrite} and createNewCopies=${createNewCopies}`, () => { before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - const testCases = tests.multiNamespaceTestCases(overwrite); + const testCases = tests.multiNamespaceTestCases(overwrite, createNewCopies); testCases.forEach(({ testTitle, objects, statusCode, response }) => { it(`should return ${statusCode} when ${testTitle}`, async () => { return supertest diff --git a/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts new file mode 100644 index 0000000000000..a10e28d52924e --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/get_shareable_references.ts @@ -0,0 +1,270 @@ +/* + * 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 { deepFreeze } from '@kbn/std'; +import { SuperTest } from 'supertest'; +import { + SavedObjectsCollectMultiNamespaceReferencesResponse, + SavedObjectReferenceWithContext, +} from '../../../../../src/core/server'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface GetShareableReferencesTestDefinition extends TestDefinition { + request: { + objects: Array<{ type: string; id: string }>; + }; +} +export type GetShareableReferencesTestSuite = TestSuite; +export interface GetShareableReferencesTestCase { + objects: Array<{ type: string; id: string }>; + expectedResults: SavedObjectReferenceWithContext[]; +} + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +export const TEST_CASE_OBJECTS: Record = deepFreeze({ + SHAREABLE_TYPE: { type: 'sharedtype', id: CASES.EACH_SPACE.id }, // contains references to four other objects + SHAREABLE_TYPE_DOES_NOT_EXIST: { type: 'sharedtype', id: 'does-not-exist' }, + NON_SHAREABLE_TYPE: { type: 'dashboard', id: 'my_dashboard' }, // one of these exists in each space +}); +// Expected results for each space are defined here since they are used in multiple test suites +export const EXPECTED_RESULTS: Record = { + IN_DEFAULT_SPACE: [ + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, + spaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + inboundReferences: [{ type: 'sharedtype', id: CASES.DEFAULT_ONLY.id, name: 'refname' }], // only reflects inbound reference that exist in the default space + }, + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + spaces: [], + inboundReferences: [], + isMissing: true, // doesn't exist anywhere + }, + { ...TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, spaces: [], inboundReferences: [] }, // not missing, but has an empty spaces array because it is not a shareable type + { + type: 'sharedtype', + id: CASES.DEFAULT_ONLY.id, + spaces: [DEFAULT_SPACE_ID], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + spacesWithMatchingAliases: [SPACE_1_ID, SPACE_2_ID], // aliases with a matching targetType and sourceId exist in two other spaces + }, + { + type: 'sharedtype', + id: CASES.SPACE_1_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in the default space + }, + { + type: 'sharedtype', + id: CASES.SPACE_2_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in the default space + }, + { + type: 'sharedtype', + id: CASES.ALL_SPACES.id, + spaces: ['*'], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + ], + IN_SPACE_1: [ + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, + spaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + inboundReferences: [{ type: 'sharedtype', id: CASES.SPACE_1_ONLY.id, name: 'refname' }], // only reflects inbound reference that exist in space 1 + }, + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + spaces: [], + inboundReferences: [], + isMissing: true, // doesn't exist anywhere + }, + { ...TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, spaces: [], inboundReferences: [] }, // not missing, but has an empty spaces array because it is not a shareable type + { + type: 'sharedtype', + id: CASES.DEFAULT_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 1 + }, + { + type: 'sharedtype', + id: CASES.SPACE_1_ONLY.id, + spaces: [SPACE_1_ID], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + { + type: 'sharedtype', + id: CASES.SPACE_2_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 1 + }, + { + type: 'sharedtype', + id: CASES.ALL_SPACES.id, + spaces: ['*'], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + ], + IN_SPACE_2: [ + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, + spaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + inboundReferences: [{ type: 'sharedtype', id: CASES.SPACE_2_ONLY.id, name: 'refname' }], // only reflects inbound reference that exist in space 2 + }, + { + ...TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + spaces: [], + inboundReferences: [], + isMissing: true, // doesn't exist anywhere + }, + { ...TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, spaces: [], inboundReferences: [] }, // not missing, but has an empty spaces array because it is not a shareable type + { + type: 'sharedtype', + id: CASES.DEFAULT_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 2 + }, + { + type: 'sharedtype', + id: CASES.SPACE_1_ONLY.id, + spaces: [], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + isMissing: true, // doesn't exist in space 2 + }, + { + type: 'sharedtype', + id: CASES.SPACE_2_ONLY.id, + spaces: [SPACE_2_ID], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + { + type: 'sharedtype', + id: CASES.ALL_SPACES.id, + spaces: ['*'], + inboundReferences: [{ ...TEST_CASE_OBJECTS.SHAREABLE_TYPE, name: 'refname' }], + }, + ], +}; + +const createRequest = ({ objects }: GetShareableReferencesTestCase) => ({ objects }); +const getTestTitle = ({ objects }: GetShareableReferencesTestCase) => { + const objStr = objects.map(({ type, id }) => `${type}:${id}`).join(','); + return `{objects: [${objStr}]}`; +}; +const getRedactedSpaces = (authorizedSpace: string | undefined, spaces: string[]) => { + if (!authorizedSpace) { + return spaces; // if authorizedSpace is undefined, we should not redact any spaces + } + const redactedSpaces = spaces.map((x) => (x !== authorizedSpace && x !== '*' ? '?' : x)); + return redactedSpaces.sort((a, b) => (a === '?' ? 1 : b === '?' ? -1 : 0)); // unknown spaces are always at the end of the array +}; + +export function getShareableReferencesTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); + const expectResponseBody = ( + testCase: GetShareableReferencesTestCase, + statusCode: 200 | 403, + authorizedSpace?: string + ): ExpectResponseBody => async (response: Record) => { + if (statusCode === 403) { + const types = testCase.objects.map((x) => x.type); + await expectForbidden(types)(response); + } else { + const { expectedResults } = testCase; + const apiResponse = response.body as SavedObjectsCollectMultiNamespaceReferencesResponse; + expect(apiResponse.objects).to.have.length(expectedResults.length); + expectedResults.forEach((expectedResult, i) => { + const { spaces, spacesWithMatchingAliases } = expectedResult; + const expectedSpaces = getRedactedSpaces(authorizedSpace, spaces); + const expectedSpacesWithMatchingAliases = + spacesWithMatchingAliases && + getRedactedSpaces(authorizedSpace, spacesWithMatchingAliases); + const expected = { + ...expectedResult, + spaces: expectedSpaces, + ...(expectedSpacesWithMatchingAliases && { + spacesWithMatchingAliases: expectedSpacesWithMatchingAliases, + }), + }; + expect(apiResponse.objects[i]).to.eql(expected); + }); + } + }; + const createTestDefinitions = ( + testCases: GetShareableReferencesTestCase | GetShareableReferencesTestCase[], + forbidden: boolean, + options: { + /** If defined, will expect results to have redacted any spaces that do not match this one. */ + authorizedSpace?: string; + responseBodyOverride?: ExpectResponseBody; + } = {} + ): GetShareableReferencesTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + return cases.map((x) => ({ + title: getTestTitle(x), + responseStatusCode, + request: createRequest(x), + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options.authorizedSpace), + })); + }; + + const makeGetShareableReferencesTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: GetShareableReferencesTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_get_shareable_references`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeGetShareableReferencesTest(describe); + // @ts-ignore + addTests.only = makeGetShareableReferencesTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/share_add.ts b/x-pack/test/spaces_api_integration/common/suites/share_add.ts deleted file mode 100644 index bec951bff67a5..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/share_add.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; -import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { SPACES } from '../lib/spaces'; -import { - expectResponses, - getUrlPrefix, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { - ExpectResponseBody, - TestDefinition, - TestSuite, -} from '../../../saved_object_api_integration/common/lib/types'; - -export interface ShareAddTestDefinition extends TestDefinition { - request: { spaces: string[]; object: { type: string; id: string } }; -} -export type ShareAddTestSuite = TestSuite; -export interface ShareAddTestCase { - id: string; - namespaces: string[]; - failure?: 400 | 403 | 404; -} - -const TYPE = 'sharedtype'; -const createRequest = ({ id, namespaces }: ShareAddTestCase) => ({ - spaces: namespaces, - object: { type: TYPE, id }, -}); -const getTestTitle = ({ id, namespaces }: ShareAddTestCase) => - `{id: ${id}, namespaces: [${namespaces.join(',')}]}`; - -export function shareAddTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); - const expectResponseBody = (testCase: ShareAddTestCase): ExpectResponseBody => async ( - response: Record - ) => { - const { id, failure } = testCase; - const object = response.body; - if (failure === 403) { - await expectForbidden(TYPE)(response); - } else if (failure === 404) { - const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); - expect(object.error).to.eql(error.output.payload.error); - expect(object.statusCode).to.eql(error.output.payload.statusCode); - } else { - // success - expect(object).to.eql({}); - } - }; - const createTestDefinitions = ( - testCases: ShareAddTestCase | ShareAddTestCase[], - forbidden: boolean, - options?: { - responseBodyOverride?: ExpectResponseBody; - } - ): ShareAddTestDefinition[] => { - let cases = Array.isArray(testCases) ? testCases : [testCases]; - if (forbidden) { - // override the expected result in each test case - cases = cases.map((x) => ({ ...x, failure: 403 })); - } - return cases.map((x) => ({ - title: getTestTitle(x), - responseStatusCode: x.failure ?? 204, - request: createRequest(x), - responseBody: options?.responseBodyOverride || expectResponseBody(x), - })); - }; - - const makeShareAddTest = (describeFn: Mocha.SuiteFunction) => ( - description: string, - definition: ShareAddTestSuite - ) => { - const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; - - describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - for (const test of tests) { - it(`should return ${test.responseStatusCode} ${test.title}`, async () => { - const requestBody = test.request; - await supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_add`) - .auth(user?.username, user?.password) - .send(requestBody) - .expect(test.responseStatusCode) - .then(test.responseBody); - }); - } - }); - }; - - const addTests = makeShareAddTest(describe); - // @ts-ignore - addTests.only = makeShareAddTest(describe.only); - - return { - addTests, - createTestDefinitions, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/share_remove.ts b/x-pack/test/spaces_api_integration/common/suites/share_remove.ts deleted file mode 100644 index 8b29c7e4d8bed..0000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/share_remove.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; -import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { SPACES } from '../lib/spaces'; -import { - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { - ExpectResponseBody, - TestDefinition, - TestSuite, -} from '../../../saved_object_api_integration/common/lib/types'; - -export interface ShareRemoveTestDefinition extends TestDefinition { - request: { spaces: string[]; object: { type: string; id: string } }; -} -export type ShareRemoveTestSuite = TestSuite; -export interface ShareRemoveTestCase { - id: string; - namespaces: string[]; - failure?: 400 | 403 | 404; -} - -const TYPE = 'sharedtype'; -const createRequest = ({ id, namespaces }: ShareRemoveTestCase) => ({ - spaces: namespaces, - object: { type: TYPE, id }, -}); - -export function shareRemoveTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); - const expectResponseBody = (testCase: ShareRemoveTestCase): ExpectResponseBody => async ( - response: Record - ) => { - const { id, failure } = testCase; - const object = response.body; - if (failure === 403) { - await expectForbidden(TYPE)(response); - } else if (failure === 404) { - const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); - expect(object.error).to.eql(error.output.payload.error); - expect(object.statusCode).to.eql(error.output.payload.statusCode); - } else { - // success - expect(object).to.eql({}); - } - }; - const createTestDefinitions = ( - testCases: ShareRemoveTestCase | ShareRemoveTestCase[], - forbidden: boolean, - options?: { - responseBodyOverride?: ExpectResponseBody; - } - ): ShareRemoveTestDefinition[] => { - let cases = Array.isArray(testCases) ? testCases : [testCases]; - if (forbidden) { - // override the expected result in each test case - cases = cases.map((x) => ({ ...x, failure: 403 })); - } - return cases.map((x) => ({ - title: getTestTitle({ ...x, type: TYPE }), - responseStatusCode: x.failure ?? 204, - request: createRequest(x), - responseBody: options?.responseBodyOverride || expectResponseBody(x), - })); - }; - - const makeShareRemoveTest = (describeFn: Mocha.SuiteFunction) => ( - description: string, - definition: ShareRemoveTestSuite - ) => { - const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; - - describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - for (const test of tests) { - it(`should return ${test.responseStatusCode} ${test.title}`, async () => { - const requestBody = test.request; - await supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/_share_saved_object_remove`) - .auth(user?.username, user?.password) - .send(requestBody) - .expect(test.responseStatusCode) - .then(test.responseBody); - }); - } - }); - }; - - const addTests = makeShareRemoveTest(describe); - // @ts-ignore - addTests.only = makeShareRemoveTest(describe.only); - - return { - addTests, - createTestDefinitions, - }; -} diff --git a/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts new file mode 100644 index 0000000000000..7664deb6b0bdf --- /dev/null +++ b/x-pack/test/spaces_api_integration/common/suites/update_objects_spaces.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { without, uniq } from 'lodash'; +import { SuperTest } from 'supertest'; +import { + SavedObjectsErrorHelpers, + SavedObjectsUpdateObjectsSpacesResponse, +} from '../../../../../src/core/server'; +import { SPACES } from '../lib/spaces'; +import { + expectResponses, + getUrlPrefix, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + ExpectResponseBody, + TestDefinition, + TestSuite, +} from '../../../saved_object_api_integration/common/lib/types'; + +export interface UpdateObjectsSpacesTestDefinition extends TestDefinition { + request: { + objects: Array<{ type: string; id: string }>; + spacesToAdd: string[]; + spacesToRemove: string[]; + }; +} +export type UpdateObjectsSpacesTestSuite = TestSuite; +export interface UpdateObjectsSpacesTestCase { + objects: Array<{ + id: string; + existingNamespaces: string[]; + failure?: 400 | 404; + }>; + spacesToAdd: string[]; + spacesToRemove: string[]; +} + +const TYPE = 'sharedtype'; +const createRequest = ({ objects, spacesToAdd, spacesToRemove }: UpdateObjectsSpacesTestCase) => ({ + objects: objects.map(({ id }) => ({ type: TYPE, id })), + spacesToAdd, + spacesToRemove, +}); +const getTestTitle = ({ objects, spacesToAdd, spacesToRemove }: UpdateObjectsSpacesTestCase) => { + const objStr = objects.map(({ id }) => id).join(','); + const addStr = spacesToAdd.join(','); + const remStr = spacesToRemove.join(','); + return `{objects: [${objStr}], spacesToAdd: [${addStr}], spacesToRemove: [${remStr}]}`; +}; + +export function updateObjectsSpacesTestSuiteFactory(esArchiver: any, supertest: SuperTest) { + const expectForbidden = expectResponses.forbiddenTypes('share_to_space'); + const expectResponseBody = ( + testCase: UpdateObjectsSpacesTestCase, + statusCode: 200 | 403, + authorizedSpace?: string + ): ExpectResponseBody => async (response: Record) => { + if (statusCode === 403) { + await expectForbidden(TYPE)(response); + } else { + const { objects, spacesToAdd, spacesToRemove } = testCase; + const apiResponse = response.body as SavedObjectsUpdateObjectsSpacesResponse; + objects.forEach(({ id, existingNamespaces, failure }, i) => { + const object = apiResponse.objects[i]; + if (failure === 404) { + const error = SavedObjectsErrorHelpers.createGenericNotFoundError(TYPE, id); + expect(object.error).to.eql(error.output.payload); + } else { + // success + const expectedSpaces = without( + uniq([...existingNamespaces, ...spacesToAdd]), + ...spacesToRemove + ).map((x) => (authorizedSpace && x !== authorizedSpace && x !== '*' ? '?' : x)); + + const result = apiResponse.objects[i]; + expect(result.type).to.eql(TYPE); + expect(result.id).to.eql(id); + expect(result.spaces.sort()).to.eql(expectedSpaces.sort()); + } + }); + } + }; + const createTestDefinitions = ( + testCases: UpdateObjectsSpacesTestCase | UpdateObjectsSpacesTestCase[], + forbidden: boolean, + options: { + /** If defined, will expect results to have redacted any spaces that do not match this one. */ + authorizedSpace?: string; + responseBodyOverride?: ExpectResponseBody; + } = {} + ): UpdateObjectsSpacesTestDefinition[] => { + const cases = Array.isArray(testCases) ? testCases : [testCases]; + const responseStatusCode = forbidden ? 403 : 200; + return cases.map((x) => ({ + title: getTestTitle(x), + responseStatusCode, + request: createRequest(x), + responseBody: + options?.responseBodyOverride || + expectResponseBody(x, responseStatusCode, options.authorizedSpace), + })); + }; + + const makeUpdateObjectsSpacesTest = (describeFn: Mocha.SuiteFunction) => ( + description: string, + definition: UpdateObjectsSpacesTestSuite + ) => { + const { user, spaceId = SPACES.DEFAULT.spaceId, tests } = definition; + + describeFn(description, () => { + before(() => esArchiver.load('saved_objects/spaces')); + after(() => esArchiver.unload('saved_objects/spaces')); + + for (const test of tests) { + it(`should return ${test.responseStatusCode} ${test.title}`, async () => { + const requestBody = test.request; + await supertest + .post(`${getUrlPrefix(spaceId)}/api/spaces/_update_objects_spaces`) + .auth(user?.username, user?.password) + .send(requestBody) + .expect(test.responseStatusCode) + .then(test.responseBody); + }); + } + }); + }; + + const addTests = makeUpdateObjectsSpacesTest(describe); + // @ts-ignore + addTests.only = makeUpdateObjectsSpacesTest(describe.only); + + return { + addTests, + createTestDefinitions, + }; +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts new file mode 100644 index 0000000000000..d3466dd511e82 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_shareable_references.ts @@ -0,0 +1,86 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { + getShareableReferencesTestSuiteFactory, + GetShareableReferencesTestCase, + GetShareableReferencesTestDefinition, + TEST_CASE_OBJECTS, + EXPECTED_RESULTS, +} from '../../common/suites/get_shareable_references'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (spaceId: string): GetShareableReferencesTestCase[] => { + const objects = [ + // requested objects are the same for each space + TEST_CASE_OBJECTS.SHAREABLE_TYPE, + TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, + ]; + + if (spaceId === DEFAULT_SPACE_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_DEFAULT_SPACE }]; + } else if (spaceId === SPACE_1_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_1 }]; + } else if (spaceId === SPACE_2_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_2 }]; + } + throw new Error(`Unexpected test case for space '${spaceId}'!`); +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = getShareableReferencesTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return { + unauthorized: createTestDefinitions(testCases, true), + authorizedThisSpace: createTestDefinitions(testCases, false, { authorizedSpace: spaceId }), + authorizedGlobally: createTestDefinitions(testCases, false), + }; + }; + + describe('_get_shareable_references', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); + const _addTests = (user: TestUser, tests: GetShareableReferencesTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedThisSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { + _addTests(user, authorizedGlobally); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index 3a775b0579a20..4bb4d10eaabf8 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -25,9 +25,9 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); + loadTestFile(require.resolve('./get_shareable_references')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./share_add')); - loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_objects_spaces')); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts deleted file mode 100644 index 050cb81874cd3..0000000000000 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_add.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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 { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { - shareAddTestSuiteFactory, - ShareAddTestDefinition, - ShareAddTestCase, -} from '../../common/suites/share_add'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -const createTestCases = (spaceId: string) => { - const namespaces = [spaceId]; - return [ - // Test cases to check adding the target namespace to different saved objects - { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.ALL_SPACES, namespaces }, - { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, - // Test case to check adding all spaces ("*") to a saved object - { ...CASES.EACH_SPACE, namespaces: ['*'] }, - // Test cases to check adding multiple namespaces to different saved objects that exist in one space - // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object - // More permutations are covered in the corresponding spaces_only test suite - { - ...CASES.DEFAULT_ONLY, - namespaces: [SPACE_1_ID, SPACE_2_ID], - ...fail404(spaceId !== DEFAULT_SPACE_ID), - }, - { - ...CASES.SPACE_1_ONLY, - namespaces: [DEFAULT_SPACE_ID, SPACE_2_ID], - ...fail404(spaceId !== SPACE_1_ID), - }, - { - ...CASES.SPACE_2_ONLY, - namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], - ...fail404(spaceId !== SPACE_2_ID), - }, - ]; -}; -const calculateSingleSpaceAuthZ = ( - testCases: ReturnType, - spaceId: string -) => { - const targetsAllSpaces: ShareAddTestCase[] = []; - const targetsOtherSpace: ShareAddTestCase[] = []; - const doesntExistInThisSpace: ShareAddTestCase[] = []; - const existsInThisSpace: ShareAddTestCase[] = []; - - for (const testCase of testCases) { - const { namespaces, existingNamespaces } = testCase; - if (namespaces.includes('*')) { - targetsAllSpaces.push(testCase); - } else if (!namespaces.includes(spaceId) || namespaces.length > 1) { - targetsOtherSpace.push(testCase); - } else if (!existingNamespaces.includes(spaceId)) { - doesntExistInThisSpace.push(testCase); - } else { - existsInThisSpace.push(testCase); - } - } - return { targetsAllSpaces, targetsOtherSpace, doesntExistInThisSpace, existsInThisSpace }; -}; -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); - const createTests = (spaceId: string) => { - const testCases = createTestCases(spaceId); - const thisSpace = calculateSingleSpaceAuthZ(testCases, spaceId); - const otherSpaceId = spaceId === DEFAULT_SPACE_ID ? SPACE_1_ID : DEFAULT_SPACE_ID; - const otherSpace = calculateSingleSpaceAuthZ(testCases, otherSpaceId); - return { - unauthorized: createTestDefinitions(testCases, true), - authorizedInSpace: [ - createTestDefinitions(thisSpace.targetsAllSpaces, true), - createTestDefinitions(thisSpace.targetsOtherSpace, true), - createTestDefinitions(thisSpace.doesntExistInThisSpace, false), - createTestDefinitions(thisSpace.existsInThisSpace, false), - ].flat(), - authorizedInOtherSpace: [ - createTestDefinitions(thisSpace.targetsAllSpaces, true), - createTestDefinitions(otherSpace.targetsOtherSpace, true), - // If the preflight GET request fails, it will return a 404 error; users who are authorized to share saved objects in the target - // space(s) but are not authorized to share saved objects in this space will see a 403 error instead of 404. This is a safeguard to - // prevent potential information disclosure of the spaces that a given saved object may exist in. - createTestDefinitions(otherSpace.doesntExistInThisSpace, true), - createTestDefinitions(otherSpace.existsInThisSpace, false), - ].flat(), - authorized: createTestDefinitions(testCases, false), - }; - }; - - describe('_share_saved_object_add', () => { - getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { - const suffix = ` targeting the ${spaceId} space`; - const { unauthorized, authorizedInSpace, authorizedInOtherSpace, authorized } = createTests( - spaceId - ); - const _addTests = (user: TestUser, tests: ShareAddTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - _addTests(users.allAtSpace, authorizedInSpace); - _addTests(users.allAtOtherSpace, authorizedInOtherSpace); - [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { - _addTests(user, authorized); - }); - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts deleted file mode 100644 index a5f18cf129d4c..0000000000000 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/share_remove.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { - shareRemoveTestSuiteFactory, - ShareRemoveTestCase, - ShareRemoveTestDefinition, -} from '../../common/suites/share_remove'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -const createTestCases = (spaceId: string) => { - // Test cases to check removing the target namespace from different saved objects - let namespaces = [spaceId]; - const singleSpace = [ - { id: CASES.DEFAULT_ONLY.id, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { id: CASES.SPACE_1_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { id: CASES.SPACE_2_ONLY.id, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { id: CASES.EACH_SPACE.id, namespaces }, - { id: CASES.DOES_NOT_EXIST.id, namespaces, ...fail404() }, - ] as ShareRemoveTestCase[]; - - namespaces = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - const multipleSpaces = [ - // Test case to check removing all spaces from a saved object that exists in all spaces; - // It fails the second time because the object no longer exists - { ...CASES.ALL_SPACES, namespaces: ['*'] }, - { ...CASES.ALL_SPACES, namespaces: ['*'], ...fail404() }, - // Test cases to check removing all three namespaces from different saved objects that exist in two spaces - // These are non-exhaustive, they only check some cases -- each object will result in a 404, either because - // it never existed in the target namespace, or it was removed in one of the test cases above - // More permutations are covered in the corresponding spaces_only test suite - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces, ...fail404() }, - { id: CASES.DEFAULT_AND_SPACE_2.id, namespaces, ...fail404() }, - { id: CASES.SPACE_1_AND_SPACE_2.id, namespaces, ...fail404() }, - ] as ShareRemoveTestCase[]; - - const allCases = singleSpace.concat(multipleSpaces); - return { singleSpace, multipleSpaces, allCases }; -}; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); - const createTests = (spaceId: string) => { - const { singleSpace, multipleSpaces, allCases } = createTestCases(spaceId); - return { - unauthorized: createTestDefinitions(allCases, true), - authorizedThisSpace: [ - createTestDefinitions(singleSpace, false), - createTestDefinitions(multipleSpaces, true), - ].flat(), - authorizedGlobally: createTestDefinitions(allCases, false), - }; - }; - - describe('_share_saved_object_remove', () => { - getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { - const suffix = ` targeting the ${spaceId} space`; - const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); - const _addTests = (user: TestUser, tests: ShareRemoveTestDefinition[]) => { - addTests(`${user.description}${suffix}`, { user, spaceId, tests }); - }; - - [ - users.noAccess, - users.legacyAll, - users.dualRead, - users.readGlobally, - users.readAtSpace, - users.allAtOtherSpace, - ].forEach((user) => { - _addTests(user, unauthorized); - }); - _addTests(users.allAtSpace, authorizedThisSpace); - [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { - _addTests(user, authorizedGlobally); - }); - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts new file mode 100644 index 0000000000000..36f50aa165e72 --- /dev/null +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/update_objects_spaces.ts @@ -0,0 +1,170 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { TestUser } from '../../../saved_object_api_integration/common/lib/types'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { + updateObjectsSpacesTestSuiteFactory, + UpdateObjectsSpacesTestDefinition, + UpdateObjectsSpacesTestCase, +} from '../../common/suites/update_objects_spaces'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +const createTestCases = (spaceId: string): UpdateObjectsSpacesTestCase[] => { + const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + return [ + // Test case to check adding and removing all spaces ("*") to a saved object + { + objects: [CASES.EACH_SPACE], + spacesToAdd: ['*'], + spacesToRemove: [], + }, + { + objects: [{ id: CASES.EACH_SPACE.id, existingNamespaces: [...eachSpace, '*'] }], + spacesToAdd: [], + spacesToRemove: ['*'], + }, + + // Test cases to check adding and removing multiple namespaces to different saved objects that exist in one space + // These are non-exhaustive, they only check cases for adding two additional namespaces to a saved object + // More permutations are covered in the corresponding spaces_only test suite + { + objects: [{ ...CASES.DEFAULT_ONLY, ...fail404(spaceId !== DEFAULT_SPACE_ID) }], + spacesToAdd: [SPACE_1_ID, SPACE_2_ID], + spacesToRemove: [], + }, + { + objects: [{ ...CASES.SPACE_1_ONLY, ...fail404(spaceId !== SPACE_1_ID) }], + spacesToAdd: [DEFAULT_SPACE_ID, SPACE_2_ID], + spacesToRemove: [], + }, + { + objects: [{ ...CASES.SPACE_2_ONLY, ...fail404(spaceId !== SPACE_2_ID) }], + spacesToAdd: [DEFAULT_SPACE_ID, SPACE_1_ID], + spacesToRemove: [], + }, + { + objects: [ + { + id: CASES.DEFAULT_ONLY.id, + existingNamespaces: eachSpace, + ...fail404(spaceId !== DEFAULT_SPACE_ID), + }, + { + id: CASES.SPACE_1_ONLY.id, + existingNamespaces: eachSpace, + ...fail404(spaceId !== SPACE_1_ID), + }, + { + id: CASES.SPACE_2_ONLY.id, + existingNamespaces: eachSpace, + ...fail404(spaceId !== SPACE_2_ID), + }, + ], + spacesToAdd: [], + spacesToRemove: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID], + }, + + // Test cases to check adding and removing the target namespace to different saved objects + { + objects: [ + { ...CASES.DEFAULT_AND_SPACE_1, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + CASES.ALL_SPACES, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ], + spacesToAdd: [spaceId], + spacesToRemove: [], + }, + { + objects: [ + { ...CASES.DEFAULT_AND_SPACE_1, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + { id: CASES.ALL_SPACES.id, existingNamespaces: ['*', spaceId] }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ], + spacesToAdd: [], + spacesToRemove: [spaceId], + }, + ]; +}; +const calculateSingleSpaceAuthZ = (testCases: UpdateObjectsSpacesTestCase[], spaceId: string) => { + const targetsThisSpace: UpdateObjectsSpacesTestCase[] = []; + const targetsOtherSpace: UpdateObjectsSpacesTestCase[] = []; + + for (const testCase of testCases) { + const { spacesToAdd, spacesToRemove } = testCase; + const spacesToAddOrRemove = [...spacesToAdd, ...spacesToRemove]; + if (spacesToAddOrRemove.length === 1 && spacesToAddOrRemove[0] === spaceId) { + targetsThisSpace.push(testCase); + } else { + targetsOtherSpace.push(testCase); + } + } + return { targetsThisSpace, targetsOtherSpace }; +}; +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + const { targetsThisSpace, targetsOtherSpace } = calculateSingleSpaceAuthZ(testCases, spaceId); + return { + unauthorized: createTestDefinitions(testCases, true), + authorizedThisSpace: [ + createTestDefinitions(targetsOtherSpace, true), + createTestDefinitions(targetsThisSpace, false, { authorizedSpace: spaceId }), + ].flat(), + authorizedGlobally: createTestDefinitions(testCases, false), + }; + }; + + describe('_update_objects_spaces', () => { + getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { + const suffix = ` targeting the ${spaceId} space`; + const { unauthorized, authorizedThisSpace, authorizedGlobally } = createTests(spaceId); + const _addTests = (user: TestUser, tests: UpdateObjectsSpacesTestDefinition[]) => { + addTests(`${user.description}${suffix}`, { user, spaceId, tests }); + }; + + [ + users.noAccess, + users.legacyAll, + users.dualRead, + users.readGlobally, + users.readAtSpace, + users.allAtOtherSpace, + ].forEach((user) => { + _addTests(user, unauthorized); + }); + _addTests(users.allAtSpace, authorizedThisSpace); + [users.dualAll, users.allGlobally, users.superuser].forEach((user) => { + _addTests(user, authorizedGlobally); + }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts new file mode 100644 index 0000000000000..5eec1dda83e5a --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_shareable_references.ts @@ -0,0 +1,62 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { getTestScenarios } from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { + getShareableReferencesTestSuiteFactory, + GetShareableReferencesTestCase, + TEST_CASE_OBJECTS, + EXPECTED_RESULTS, +} from '../../common/suites/get_shareable_references'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (spaceId: string): GetShareableReferencesTestCase[] => { + const objects = [ + // requested objects are the same for each space + TEST_CASE_OBJECTS.SHAREABLE_TYPE, + TEST_CASE_OBJECTS.SHAREABLE_TYPE_DOES_NOT_EXIST, + TEST_CASE_OBJECTS.NON_SHAREABLE_TYPE, + ]; + + if (spaceId === DEFAULT_SPACE_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_DEFAULT_SPACE }]; + } else if (spaceId === SPACE_1_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_1 }]; + } else if (spaceId === SPACE_2_ID) { + return [{ objects, expectedResults: EXPECTED_RESULTS.IN_SPACE_2 }]; + } + throw new Error(`Unexpected test case for space '${spaceId}'!`); +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = getShareableReferencesTestSuiteFactory( + esArchiver, + supertest + ); + const createTests = (spaceId: string) => { + const testCases = createTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + + describe('_get_shareable_references', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + }); +} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 6c52f731289e7..489e2c2d22ffa 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -17,9 +17,9 @@ export default function spacesOnlyTestSuite({ loadTestFile }: FtrProviderContext loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); + loadTestFile(require.resolve('./get_shareable_references')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./share_add')); - loadTestFile(require.resolve('./share_remove')); loadTestFile(require.resolve('./update')); + loadTestFile(require.resolve('./update_objects_spaces')); }); } diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts deleted file mode 100644 index 77af9221d6b9c..0000000000000 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_add.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { shareAddTestSuiteFactory } from '../../common/suites/share_add'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -/** - * Single-namespace test cases - * @param spaceId the namespace to add to each saved object - */ -const createSingleTestCases = (spaceId: string) => { - const namespaces = ['some-space-id']; - return [ - { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.EACH_SPACE, namespaces }, - { ...CASES.ALL_SPACES, namespaces }, - { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, - ]; -}; -/** - * Multi-namespace test cases - * These are non-exhaustive, but they check different permutations of saved objects and spaces to add - */ -const createMultiTestCases = () => { - const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; - const allSpaces = ['*']; - // for each of the cases below, test adding each space and all spaces to the object - const one = [ - { id: CASES.DEFAULT_ONLY.id, namespaces: eachSpace }, - { id: CASES.DEFAULT_ONLY.id, namespaces: allSpaces }, - ]; - const two = [ - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces: eachSpace }, - { id: CASES.DEFAULT_AND_SPACE_1.id, namespaces: allSpaces }, - ]; - const three = [ - { id: CASES.EACH_SPACE.id, namespaces: eachSpace }, - { id: CASES.EACH_SPACE.id, namespaces: allSpaces }, - ]; - const four = [ - { id: CASES.ALL_SPACES.id, namespaces: eachSpace }, - { id: CASES.ALL_SPACES.id, namespaces: allSpaces }, - ]; - return { one, two, three, four }; -}; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareAddTestSuiteFactory(esArchiver, supertest); - const createSingleTests = (spaceId: string) => { - const testCases = createSingleTestCases(spaceId); - return createTestDefinitions(testCases, false); - }; - const createMultiTests = () => { - const testCases = createMultiTestCases(); - return { - one: createTestDefinitions(testCases.one, false), - two: createTestDefinitions(testCases.two, false), - three: createTestDefinitions(testCases.three, false), - four: createTestDefinitions(testCases.four, false), - }; - }; - - describe('_share_saved_object_add', () => { - getTestScenarios().spaces.forEach(({ spaceId }) => { - const tests = createSingleTests(spaceId); - addTests(`targeting the ${spaceId} space`, { spaceId, tests }); - }); - const { one, two, three, four } = createMultiTests(); - addTests('for a saved object in the default space', { tests: one }); - addTests('for a saved object in the default and space_1 spaces', { tests: two }); - addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); - addTests('for a saved object in all spaces', { tests: four }); - }); -} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts deleted file mode 100644 index 22e18e7308f6b..0000000000000 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/share_remove.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 { SPACES } from '../../common/lib/spaces'; -import { - testCaseFailures, - getTestScenarios, -} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; -import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; -import { shareRemoveTestSuiteFactory } from '../../common/suites/share_remove'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -const { - DEFAULT: { spaceId: DEFAULT_SPACE_ID }, - SPACE_1: { spaceId: SPACE_1_ID }, - SPACE_2: { spaceId: SPACE_2_ID }, -} = SPACES; -const { fail404 } = testCaseFailures; - -/** - * Single-namespace test cases - * @param spaceId the namespace to remove from each saved object - */ -const createSingleTestCases = (spaceId: string) => { - const namespaces = [spaceId]; - return [ - { ...CASES.DEFAULT_ONLY, namespaces, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SPACE_1_ONLY, namespaces, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SPACE_2_ONLY, namespaces, ...fail404(spaceId !== SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_1, namespaces, ...fail404(spaceId === SPACE_2_ID) }, - { ...CASES.DEFAULT_AND_SPACE_2, namespaces, ...fail404(spaceId === SPACE_1_ID) }, - { ...CASES.SPACE_1_AND_SPACE_2, namespaces, ...fail404(spaceId === DEFAULT_SPACE_ID) }, - { ...CASES.EACH_SPACE, namespaces }, - { ...CASES.DOES_NOT_EXIST, namespaces, ...fail404() }, - ]; -}; -/** - * Multi-namespace test cases - * These are non-exhaustive, but they check different permutations of saved objects and spaces to remove - */ -const createMultiTestCases = () => { - const nonExistentSpaceId = 'does_not_exist'; // space that doesn't exist - let id = CASES.DEFAULT_ONLY.id; - const one = [ - { id, namespaces: [nonExistentSpaceId] }, - { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID] }, - { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this saved object no longer exists - ]; - id = CASES.DEFAULT_AND_SPACE_1.id; - const two = [ - { id, namespaces: [DEFAULT_SPACE_ID, nonExistentSpaceId] }, - // this saved object will not be found in the context of the current namespace ('default') - { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID - { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces does contain SPACE_1_ID - ]; - id = CASES.EACH_SPACE.id; - const three = [ - { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, nonExistentSpaceId] }, - // this saved object will not be found in the context of the current namespace ('default') - { id, namespaces: [DEFAULT_SPACE_ID], ...fail404() }, // this object's namespaces no longer contains DEFAULT_SPACE_ID - { id, namespaces: [SPACE_1_ID], ...fail404() }, // this object's namespaces no longer contains SPACE_1_ID - { id, namespaces: [SPACE_2_ID], ...fail404() }, // this object's namespaces does contain SPACE_2_ID - ]; - id = CASES.ALL_SPACES.id; - const four = [ - { id, namespaces: [DEFAULT_SPACE_ID, SPACE_1_ID, nonExistentSpaceId] }, - // this saved object will still be found in the context of the current namespace ('default') - { id, namespaces: ['*'] }, - // this object no longer exists - { id, namespaces: ['*'], ...fail404() }, - ]; - return { one, two, three, four }; -}; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const { addTests, createTestDefinitions } = shareRemoveTestSuiteFactory(esArchiver, supertest); - const createSingleTests = (spaceId: string) => { - const testCases = createSingleTestCases(spaceId); - return createTestDefinitions(testCases, false); - }; - const createMultiTests = () => { - const testCases = createMultiTestCases(); - return { - one: createTestDefinitions(testCases.one, false), - two: createTestDefinitions(testCases.two, false), - three: createTestDefinitions(testCases.three, false), - four: createTestDefinitions(testCases.four, false), - }; - }; - - describe('_share_saved_object_remove', () => { - getTestScenarios().spaces.forEach(({ spaceId }) => { - const tests = createSingleTests(spaceId); - addTests(`targeting the ${spaceId} space`, { spaceId, tests }); - }); - const { one, two, three, four } = createMultiTests(); - addTests('for a saved object in the default space', { tests: one }); - addTests('for a saved object in the default and space_1 spaces', { tests: two }); - addTests('for a saved object in the default, space_1, and space_2 spaces', { tests: three }); - addTests('for a saved object in all spaces', { tests: four }); - }); -} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts new file mode 100644 index 0000000000000..865d5eca22cbd --- /dev/null +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/update_objects_spaces.ts @@ -0,0 +1,143 @@ +/* + * 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 { SPACES } from '../../common/lib/spaces'; +import { + testCaseFailures, + getTestScenarios, +} from '../../../saved_object_api_integration/common/lib/saved_object_test_utils'; +import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../../common/lib/saved_object_test_cases'; +import { updateObjectsSpacesTestSuiteFactory } from '../../common/suites/update_objects_spaces'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const { fail404 } = testCaseFailures; + +/** + * Single-part test cases, which can be run in a single batch + * @param spaceId the space in which the test will take place (and the space the object will be removed from) + */ +const createSinglePartTestCases = (spaceId: string) => { + const spacesToAdd = ['some-space-id']; + const spacesToRemove = [spaceId]; + return { + objects: [ + { ...CASES.DEFAULT_ONLY, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SPACE_1_ONLY, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SPACE_2_ONLY, ...fail404(spaceId !== SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_1, ...fail404(spaceId === SPACE_2_ID) }, + { ...CASES.DEFAULT_AND_SPACE_2, ...fail404(spaceId === SPACE_1_ID) }, + { ...CASES.SPACE_1_AND_SPACE_2, ...fail404(spaceId === DEFAULT_SPACE_ID) }, + CASES.EACH_SPACE, + CASES.ALL_SPACES, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ], + spacesToAdd, + spacesToRemove, + }; +}; +/** + * Multi-part test cases, which have to be run sequentially + * These are non-exhaustive, but they check different permutations of saved objects and spaces to add + */ +const createMultiPartTestCases = () => { + const nonExistentSpace = 'does_not_exist'; // space that doesn't exist + const eachSpace = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + const group1 = [ + // first, add this object to each space and remove it from nonExistentSpace + // this will succeed even though the object already exists in the default space and it doesn't exist in nonExistentSpace + { objects: [CASES.DEFAULT_ONLY], spacesToAdd: eachSpace, spacesToRemove: [nonExistentSpace] }, + // second, add this object to nonExistentSpace and all spaces, and remove it from the default space + { + objects: [{ id: CASES.DEFAULT_ONLY.id, existingNamespaces: eachSpace }], + spacesToAdd: [nonExistentSpace, '*'], + spacesToRemove: [DEFAULT_SPACE_ID], + }, + // third, remove the object from all spaces + // the object is still accessible in the context of the default space because it currently exists in all spaces + { + objects: [ + { + id: CASES.DEFAULT_ONLY.id, + existingNamespaces: [SPACE_1_ID, SPACE_2_ID, nonExistentSpace, '*'], + }, + ], + spacesToAdd: [], + spacesToRemove: ['*'], + }, + // fourth, remove the object from space_1 + // this will fail because, even though the object still exists, it no longer exists in the context of the default space + { + objects: [ + { + id: CASES.DEFAULT_ONLY.id, + existingNamespaces: [SPACE_1_ID, SPACE_2_ID, nonExistentSpace], + ...fail404(), + }, + ], + spacesToAdd: [], + spacesToRemove: [SPACE_1_ID], + }, + ]; + const group2 = [ + // first, add this object to space_2 and remove it from space_1 + { + objects: [CASES.DEFAULT_AND_SPACE_1], + spacesToAdd: [SPACE_2_ID], + spacesToRemove: [SPACE_1_ID], + }, + // second, remove this object from the default space and space_2 + // since the object would no longer exist in any spaces, it will be deleted + { + objects: [ + { id: CASES.DEFAULT_AND_SPACE_1.id, existingNamespaces: [DEFAULT_SPACE_ID, SPACE_2_ID] }, + ], + spacesToAdd: [], + spacesToRemove: [DEFAULT_SPACE_ID, SPACE_1_ID], + }, + // fourth, add the object to the default space + // this will fail because the object no longer exists + { + objects: [{ id: CASES.DEFAULT_AND_SPACE_1.id, existingNamespaces: [], ...fail404() }], + spacesToAdd: [DEFAULT_SPACE_ID], + spacesToRemove: [], + }, + ]; + return [...group1, ...group2]; +}; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const { addTests, createTestDefinitions } = updateObjectsSpacesTestSuiteFactory( + esArchiver, + supertest + ); + const createSinglePartTests = (spaceId: string) => { + const testCases = createSinglePartTestCases(spaceId); + return createTestDefinitions(testCases, false); + }; + const createMultiPartTests = () => { + const testCases = createMultiPartTestCases(); + return createTestDefinitions(testCases, false); + }; + + describe('_update_objects_spaces', () => { + getTestScenarios().spaces.forEach(({ spaceId }) => { + const tests = createSinglePartTests(spaceId); + addTests(`targeting the ${spaceId} space`, { spaceId, tests }); + }); + const multiPartTests = createMultiPartTests(); + addTests('multi-part tests in the default space', { tests: multiPartTests }); + }); +} From 1a2df55d1506bcc3022ff469d227e35c1e4cd728 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 14 May 2021 21:12:20 +0100 Subject: [PATCH 064/186] chore(NA): moving @kbn/i18n into bazel (#99390) * chore(NA): moving @kbn/i18n into bazel * chore(NA): include javascript locales.js files * chore(NA): remove build scripts * chore(NA): remove node types on browser Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-i18n/BUILD.bazel | 154 ++++++++++++++++++ packages/kbn-i18n/package.json | 7 +- packages/kbn-i18n/scripts/build.js | 85 ---------- packages/kbn-i18n/tsconfig.browser.json | 21 +++ packages/kbn-i18n/tsconfig.json | 7 +- packages/kbn-interpreter/package.json | 3 - packages/kbn-monaco/package.json | 3 - packages/kbn-test/package.json | 1 - packages/kbn-ui-shared-deps/package.json | 1 - x-pack/package.json | 1 - yarn.lock | 2 +- 14 files changed, 184 insertions(+), 105 deletions(-) create mode 100644 packages/kbn-i18n/BUILD.bazel delete mode 100644 packages/kbn-i18n/scripts/build.js create mode 100644 packages/kbn-i18n/tsconfig.browser.json diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 92dc2a1a24377..06a5326d7f89a 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -79,6 +79,7 @@ yarn kbn watch-bazel - @kbn/eslint-import-resolver-kibana - @kbn/eslint-plugin-eslint - @kbn/expect +- @kbn/i18n - @kbn/legacy-logging - @kbn/logging - @kbn/securitysolution-constants diff --git a/package.json b/package.json index b79724dbb63bc..eddb6e6697347 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", - "@kbn/i18n": "link:packages/kbn-i18n", + "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index a9c87043575fa..889e34a1d9c22 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -21,6 +21,7 @@ filegroup( "//packages/kbn-eslint-import-resolver-kibana:build", "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", + "//packages/kbn-i18n:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-plugin-generator:build", diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel new file mode 100644 index 0000000000000..d71f7d78b1221 --- /dev/null +++ b/packages/kbn-i18n/BUILD.bazel @@ -0,0 +1,154 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-i18n" +PKG_REQUIRE_NAME = "@kbn/i18n" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/core/locales.js", + "types/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/__snapshots__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "angular/package.json", + "react/package.json", + "package.json", + "GUIDELINE.md", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//del", + "@npm//getopts", + "@npm//intl-format-cache", + "@npm//intl-messageformat", + "@npm//intl-relativeformat", + "@npm//prop-types", + "@npm//react", + "@npm//react-intl", + "@npm//supports-color", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/angular", + "@npm//@types/intl-relativeformat", + "@npm//@types/jest", + "@npm//@types/prop-types", + "@npm//@types/react", + "@npm//@types/react-intl", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = True, + declaration_dir = "types", + declaration_map = True, + incremental = True, + out_dir = "node", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + incremental = True, + out_dir = "web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +filegroup( + name = "tsc_types", + srcs = [":tsc"], + output_group = "types", +) + +filegroup( + name = "target_files", + srcs = [ + ":tsc", + ":tsc_browser", + ":tsc_types", + ], +) + +pkg_npm( + name = "target", + deps = [ + ":target_files", + ], +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":target"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 1f9d21f724ea8..36b625b1097bf 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -5,10 +5,5 @@ "types": "./target/types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --source-maps", - "kbn:watch": "node scripts/build --watch --source-maps" - } + "private": true } \ No newline at end of file diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js deleted file mode 100644 index 62ef2f59239d0..0000000000000 --- a/packages/kbn-i18n/scripts/build.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { resolve } = require('path'); - -const del = require('del'); -const supportsColor = require('supports-color'); -const { run, withProcRunner } = require('@kbn/dev-utils'); - -const ROOT_DIR = resolve(__dirname, '..'); -const BUILD_DIR = resolve(ROOT_DIR, 'target'); - -const padRight = (width, str) => - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -run( - async ({ log, flags }) => { - await withProcRunner(log, async (proc) => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['web', 'node'].map((subTask) => - proc.run(padRight(10, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.ts,.js,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE - ? [] - : ['--source-maps', 'inline']), - ], - wait: true, - env: { - ...env, - BABEL_ENV: subTask, - }, - cwd, - }) - ), - - proc.run(padRight(10, 'tsc'), { - cmd: 'tsc', - args: [ - ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), - ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); - }); - }, - { - description: 'Simple build tool for @kbn/i18n package', - flags: { - boolean: ['watch', 'source-maps'], - help: ` - --watch Run in watch mode - --source-maps Include sourcemaps - `, - }, - } -); diff --git a/packages/kbn-i18n/tsconfig.browser.json b/packages/kbn-i18n/tsconfig.browser.json new file mode 100644 index 0000000000000..9ee4aeed8da21 --- /dev/null +++ b/packages/kbn-i18n/tsconfig.browser.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target/web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-i18n/src" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "types/intl_format_cache.d.ts", + "types/intl_relativeformat.d.ts" + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index 9d4cb8c9b0972..ddb21915eac50 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, - "outDir": "./target/types", - "emitDeclarationOnly": true, + "allowJs": true, + "incremental": true, + "declarationDir": "./target/types", + "outDir": "./target/node", "declaration": true, "declarationMap": true, "sourceMap": true, diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 997fbb0eb8a4f..fc0936f4b5f53 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -8,8 +8,5 @@ "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --dev --watch" - }, - "dependencies": { - "@kbn/i18n": "link:../kbn-i18n" } } \ No newline at end of file diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index 75f1d74f1c9c9..e818351e7e470 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -9,8 +9,5 @@ "build": "node ./scripts/build.js", "kbn:bootstrap": "yarn build --dev", "build:antlr4ts": "../../node_modules/antlr4ts-cli/antlr4ts ./src/painless/antlr/painless_lexer.g4 ./src/painless/antlr/painless_parser.g4 && node ./scripts/fix_generated_antlr.js" - }, - "dependencies": { - "@kbn/i18n": "link:../kbn-i18n" } } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index a668d8c1f8588..e8e42de3114aa 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/i18n": "link:../kbn-i18n", "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 54d983bf1bf44..c284be4487a5f 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,6 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@kbn/i18n": "link:../kbn-i18n", "@kbn/monaco": "link:../kbn-monaco" } } \ No newline at end of file diff --git a/x-pack/package.json b/x-pack/package.json index 91caae7a976e4..04f808c89764d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -32,7 +32,6 @@ "@kbn/test": "link:../packages/kbn-test" }, "dependencies": { - "@kbn/i18n": "link:../packages/kbn-i18n", "@kbn/interpreter": "link:../packages/kbn-interpreter", "@kbn/ui-framework": "link:../packages/kbn-ui-framework" } diff --git a/yarn.lock b/yarn.lock index 4857c7c908293..dda4855e13d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2658,7 +2658,7 @@ version "0.0.0" uid "" -"@kbn/i18n@link:packages/kbn-i18n": +"@kbn/i18n@link:bazel-bin/packages/kbn-i18n/npm_module": version "0.0.0" uid "" From c7b6577fbabafda469390ebc73186f04bf9f8778 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 14 May 2021 21:14:45 +0100 Subject: [PATCH 065/186] chore(NA): moving @kbn/server-http-tools into bazel (#100153) --- .../monorepo-packages.asciidoc | 1 + package.json | 2 +- packages/BUILD.bazel | 1 + packages/kbn-cli-dev-mode/package.json | 1 - packages/kbn-server-http-tools/BUILD.bazel | 90 +++++++++++++++++++ packages/kbn-server-http-tools/package.json | 10 +-- packages/kbn-server-http-tools/tsconfig.json | 3 +- yarn.lock | 2 +- 8 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 packages/kbn-server-http-tools/BUILD.bazel diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 06a5326d7f89a..e81875d7893dd 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -89,6 +89,7 @@ yarn kbn watch-bazel - kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils - @kbn/securitysolution-utils +- @kbn/server-http-tools - @kbn/std - @kbn/telemetry-utils - @kbn/tinymath diff --git a/package.json b/package.json index eddb6e6697347..8024ecafde769 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", - "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", + "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools/npm_module", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 889e34a1d9c22..76250d8a1e864 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -32,6 +32,7 @@ filegroup( "//packages/kbn-securitysolution-io-ts-utils:build", "//packages/kbn-securitysolution-utils:build", "//packages/kbn-securitysolution-es-utils:build", + "//packages/kbn-server-http-tools:build", "//packages/kbn-std:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index 0401e6a82e11a..dd491de55c075 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/server-http-tools": "link:../kbn-server-http-tools", "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel new file mode 100644 index 0000000000000..61570969c85f1 --- /dev/null +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -0,0 +1,90 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-server-http-tools" +PKG_REQUIRE_NAME = "@kbn/server-http-tools" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "//packages/kbn-crypto", + "@npm//@hapi/hapi", + "@npm//@hapi/hoek", + "@npm//joi", + "@npm//moment", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/hapi__hapi", + "@npm//@types/joi", + "@npm//@types/node", + "@npm//@types/uuid", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json index c44bf17079aab..7ec52743f027e 100644 --- a/packages/kbn-server-http-tools/package.json +++ b/packages/kbn-server-http-tools/package.json @@ -4,13 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "rm -rf target && ../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - }, - "devDependencies": { - "@kbn/utility-types": "link:../kbn-utility-types" - } + "private": true } diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index 2f3e4626a04ce..034cbd2334919 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-server-http-tools/src" }, diff --git a/yarn.lock b/yarn.lock index dda4855e13d96..1f7c47baccf1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2727,7 +2727,7 @@ version "0.0.0" uid "" -"@kbn/server-http-tools@link:packages/kbn-server-http-tools": +"@kbn/server-http-tools@link:bazel-bin/packages/kbn-server-http-tools/npm_module": version "0.0.0" uid "" From 28fd4fe3b3601eca2967c7937c6e896df10ff4ff Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Fri, 14 May 2021 15:38:20 -0500 Subject: [PATCH 066/186] [index patterns] deprecate IIndexPattern and IFieldType interfaces (#100013) * deprecate IIndexPattern and IFieldType * update api docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/kibana-plugin-plugins-data-public.ifieldtype.md | 5 +++++ .../kibana-plugin-plugins-data-public.iindexpattern.md | 5 ++++- .../plugins/data/public/kibana-plugin-plugins-data-public.md | 2 +- .../server/kibana-plugin-plugins-data-server.ifieldtype.md | 5 +++++ src/plugins/data/common/index_patterns/fields/types.ts | 4 ++++ src/plugins/data/common/index_patterns/types.ts | 3 ++- src/plugins/data/public/public.api.md | 4 ++-- src/plugins/data/server/server.api.md | 2 +- 8 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 2b3d3df1ec8d0..4e3dea5549b56 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -4,6 +4,11 @@ ## IFieldType interface +> Warning: This API is now obsolete. +> +> Use IndexPatternField or FieldSpec instead +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 3a78395b42754..bf7f88ab37039 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -4,7 +4,10 @@ ## IIndexPattern interface -IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided +> Warning: This API is now obsolete. +> +> IIndexPattern allows for an IndexPattern OR an index pattern saved object Use IndexPattern or IndexPatternSpec instead +> Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 58a225a3a4bc3..7f5a042e0ab81 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -67,7 +67,7 @@ | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-public.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) | | -| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | IIndexPattern allows for an IndexPattern OR an index pattern saved object too ambiguous, should be avoided | +| [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) | | | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 48836a1b620b8..5ac48d26a85d6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -4,6 +4,11 @@ ## IFieldType interface +> Warning: This API is now obsolete. +> +> Use IndexPatternField or FieldSpec instead +> + Signature: ```typescript diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index fa8f6c3bc1dc8..565dd6d926948 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -8,6 +8,10 @@ import { FieldSpec, IFieldSubType, IndexPattern } from '../..'; +/** + * @deprecated + * Use IndexPatternField or FieldSpec instead + */ export interface IFieldType { name: string; type: string; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index c906b809b08c4..0fcdea1a878eb 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -25,8 +25,9 @@ export interface RuntimeField { } /** + * @deprecated * IIndexPattern allows for an IndexPattern OR an index pattern saved object - * too ambiguous, should be avoided + * Use IndexPattern or IndexPatternSpec instead */ export interface IIndexPattern { fields: IFieldType[]; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 54cea5e09121b..8561d7bf8d6f5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1178,7 +1178,7 @@ export interface IFieldSubType { // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export interface IFieldType { // (undocumented) aggregatable?: boolean; @@ -1222,7 +1222,7 @@ export interface IFieldType { // Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated (undocumented) export interface IIndexPattern { // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts // diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index dbb49825b2409..ffdff2e33cf9c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -692,7 +692,7 @@ export interface IFieldSubType { // Warning: (ae-missing-release-tag) "IFieldType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated (undocumented) export interface IFieldType { // (undocumented) aggregatable?: boolean; From 592a1a61beeb19cf5cb33a4d4adf5c3baab4ffc9 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 14 May 2021 17:07:21 -0400 Subject: [PATCH 067/186] [Lens] Create managedReference type for formulas (#99729) * [Lens] Create managedReference type for formulas * Fix test failures * Fix i18n types * Delete managedReference when replacing * Tests for formula * Refactoring from code review Co-authored-by: Joe Reuter Co-authored-by: Marco Liberati Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../workspace_panel/workspace_panel.test.tsx | 198 +--- .../dimension_panel/dimension_editor.tsx | 8 +- .../dimension_panel/dimension_panel.test.tsx | 97 +- .../droppable/droppable.test.ts | 6 +- .../droppable/on_drop_handler.ts | 2 +- .../dimension_panel/reference_editor.test.tsx | 25 + .../dimension_panel/reference_editor.tsx | 8 +- .../indexpattern.test.ts | 113 +- .../operations/__mocks__/index.ts | 4 +- .../definitions/calculations/counter_rate.tsx | 8 +- .../calculations/cumulative_sum.tsx | 8 +- .../definitions/calculations/differences.tsx | 8 +- .../calculations/moving_average.tsx | 22 +- .../definitions/calculations/utils.test.ts | 4 +- .../operations/definitions/cardinality.tsx | 16 +- .../operations/definitions/count.tsx | 6 +- .../definitions/date_histogram.test.tsx | 1 + .../operations/definitions/date_histogram.tsx | 5 +- .../definitions/filters/filters.test.tsx | 1 + .../definitions/formula/formula.test.tsx | 987 ++++++++++++++++++ .../definitions/formula/formula.tsx | 155 +++ .../definitions/formula/generate.ts | 90 ++ .../operations/definitions/formula/index.ts | 10 + .../operations/definitions/formula/math.tsx | 111 ++ .../operations/definitions/formula/parse.ts | 210 ++++ .../operations/definitions/formula/types.ts | 25 + .../operations/definitions/formula/util.ts | 317 ++++++ .../definitions/formula/validation.ts | 687 ++++++++++++ .../operations/definitions/helpers.test.ts | 2 +- .../operations/definitions/helpers.tsx | 52 +- .../operations/definitions/index.ts | 115 +- .../definitions/last_value.test.tsx | 1 + .../operations/definitions/last_value.tsx | 11 +- .../operations/definitions/metrics.tsx | 16 +- .../definitions/percentile.test.tsx | 39 +- .../operations/definitions/percentile.tsx | 9 +- .../definitions/ranges/ranges.test.tsx | 1 + .../definitions/terms/terms.test.tsx | 1 + .../operations/layer_helpers.test.ts | 294 +++++- .../operations/layer_helpers.ts | 204 +++- .../operations/mocks.ts | 2 +- .../operations/operations.test.ts | 8 + .../operations/operations.ts | 71 +- .../indexpattern_datasource/to_expression.ts | 43 +- .../public/indexpattern_datasource/utils.ts | 14 +- 45 files changed, 3634 insertions(+), 381 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index e741b9ee243db..baa9d45a431ea 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -29,12 +29,7 @@ import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { fromExpression } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; -import { - DataPublicPluginStart, - esFilters, - IFieldType, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +import { esFilters, IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; @@ -55,6 +50,25 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) { return core; } +function getDefaultProps() { + return { + activeDatasourceId: 'mock', + datasourceStates: {}, + datasourceMap: {}, + framePublicAPI: createMockFramePublicAPI(), + activeVisualizationId: 'vis', + visualizationState: {}, + dispatch: () => {}, + ExpressionRenderer: createExpressionRendererMock(), + core: createCoreStartWithPermissions(), + plugins: { + uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), + }, + getSuggestionForField: () => undefined, + }; +} + describe('workspace_panel', () => { let mockVisualization: jest.Mocked; let mockVisualization2: jest.Mocked; @@ -62,21 +76,18 @@ describe('workspace_panel', () => { let expressionRendererMock: jest.Mock; let uiActionsMock: jest.Mocked; - let dataMock: jest.Mocked; let trigger: jest.Mocked; let instance: ReactWrapper; beforeEach(() => { + // These are used in specific tests to assert function calls trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked; uiActionsMock = uiActionsPluginMock.createStartContract(); - dataMock = dataPluginMock.createStartContract(); uiActionsMock.getTrigger.mockReturnValue(trigger); mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource('a'); - expressionRendererMock = createExpressionRendererMock(); }); @@ -87,23 +98,14 @@ describe('workspace_panel', () => { it('should render an explanatory text if no visualization is active', () => { instance = mount( {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -111,20 +113,10 @@ describe('workspace_panel', () => { it('should render an explanatory text if the visualization does not produce an expression', () => { instance = mount( null }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -135,20 +127,10 @@ describe('workspace_panel', () => { it('should render an explanatory text if the datasource does not produce an expression', () => { instance = mount( 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -166,7 +148,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -204,10 +180,11 @@ describe('workspace_panel', () => { }; mockDatasource.toExpression.mockReturnValue('datasource'); mockDatasource.getLayers.mockReturnValue(['first']); + const props = getDefaultProps(); instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} + plugins={{ ...props.plugins, uiActions: uiActionsMock }} /> ); @@ -251,7 +223,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} dispatch={dispatch} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -298,7 +265,7 @@ describe('workspace_panel', () => { instance = mount( { mock2: mockDatasource2, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -382,7 +343,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -439,7 +394,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -494,7 +443,7 @@ describe('workspace_panel', () => { }; instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -532,7 +474,7 @@ describe('workspace_panel', () => { instance = mount( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} // Use cannot navigate to the management page core={createCoreStartWithPermissions({ navLinks: { management: false }, management: { kibana: { indexPatterns: true } }, })} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -575,7 +512,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} // user can go to management, but indexPatterns management is not accessible core={createCoreStartWithPermissions({ navLinks: { management: true }, management: { kibana: { indexPatterns: false } }, })} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -621,7 +552,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -663,7 +587,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: mockVisualization, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -707,7 +624,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: mockVisualization, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -748,7 +658,7 @@ describe('workspace_panel', () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); @@ -787,7 +690,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -832,7 +729,7 @@ describe('workspace_panel', () => { await act(async () => { instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - visualizationState={{}} - dispatch={() => {}} ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} - getSuggestionForField={() => undefined} /> ); }); @@ -900,7 +791,7 @@ describe('workspace_panel', () => { dropTargetsByOrder={undefined} > { mock: mockDatasource, }} framePublicAPI={frame} - activeVisualizationId={'vis'} visualizationMap={{ vis: mockVisualization, vis2: mockVisualization2, }} - visualizationState={{}} dispatch={mockDispatch} - ExpressionRenderer={expressionRendererMock} - core={createCoreStartWithPermissions()} - plugins={{ uiActions: uiActionsMock, data: dataMock }} getSuggestionForField={mockGetSuggestionForField} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index b74e97df4a895..d84d418ff231c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -151,6 +151,7 @@ export function DimensionEditor(props: DimensionEditorProps) { const possibleOperations = useMemo(() => { return Object.values(operationDefinitionMap) + .filter(({ hidden }) => !hidden) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) @@ -242,6 +243,7 @@ export function DimensionEditor(props: DimensionEditorProps) { onClick() { if ( operationDefinitionMap[operationType].input === 'none' || + operationDefinitionMap[operationType].input === 'managedReference' || operationDefinitionMap[operationType].input === 'fullReference' ) { // Clear invalid state because we are reseting to a valid column @@ -319,7 +321,8 @@ export function DimensionEditor(props: DimensionEditorProps) { // Need to workout early on the error to decide whether to show this or an help text const fieldErrorMessage = - (selectedOperationDefinition?.input !== 'fullReference' || + ((selectedOperationDefinition?.input !== 'fullReference' && + selectedOperationDefinition?.input !== 'managedReference') || (incompleteOperation && operationDefinitionMap[incompleteOperation].input === 'field')) && getErrorMessage( selectedColumn, @@ -447,6 +450,7 @@ export function DimensionEditor(props: DimensionEditorProps) { currentColumn={state.layers[layerId].columns[columnId]} dateRange={dateRange} indexPattern={currentIndexPattern} + operationDefinitionMap={operationDefinitionMap} {...services} /> @@ -586,7 +590,7 @@ export function DimensionEditor(props: DimensionEditorProps) { function getErrorMessage( selectedColumn: IndexPatternColumn | undefined, incompleteOperation: boolean, - input: 'none' | 'field' | 'fullReference' | undefined, + input: 'none' | 'field' | 'fullReference' | 'managedReference' | undefined, fieldInvalid: boolean ) { if (selectedColumn && incompleteOperation) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index f80b12aecabde..333caf259fe2f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -25,14 +25,13 @@ import { import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test/jest'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { generateId } from '../../id_generator'; import { IndexPatternPrivateState } from '../types'; import { IndexPatternColumn, replaceColumn } from '../operations'; import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { getFieldByNameFactory } from '../pure_helpers'; -import { DimensionEditor } from './dimension_editor'; -import { AdvancedOptions } from './advanced_options'; import { Filtering } from './filtering'; jest.mock('../loader'); @@ -48,6 +47,7 @@ jest.mock('lodash', () => { debounce: (fn: unknown) => fn, }; }); +jest.mock('../../id_generator'); const fields = [ { @@ -388,6 +388,15 @@ describe('IndexPatternDimensionEditorPanel', () => { ); }); + it('should not display hidden operation types', () => { + wrapper = mount(); + + const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || []; + + expect(items.find(({ id }) => id === 'math')).toBeUndefined(); + expect(items.find(({ id }) => id === 'formula')).toBeUndefined(); + }); + it('should indicate that reference-based operations are not compatible when they are incomplete', () => { wrapper = mount( { } it('should not show custom options if time scaling is not available', () => { - wrapper = shallow( + wrapper = mount( { })} /> ); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-time-scaling-enable"]') + wrapper.find('[data-test-subj="indexPattern-time-scaling-enable"]').hostNodes() ).toHaveLength(0); }); it('should show custom options if time scaling is available', () => { - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-time-scaling-enable"]') + wrapper.find('[data-test-subj="indexPattern-time-scaling-enable"]').hostNodes() ).toHaveLength(1); }); @@ -1114,14 +1121,15 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should allow to set time scaling initially', () => { const props = getProps({}); - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() .find('[data-test-subj="indexPattern-time-scaling-enable"]') - .prop('onClick')!({} as MouseEvent); + .hostNodes() + .simulate('click'); expect(props.setState).toHaveBeenCalledWith( { ...props.state, @@ -1205,6 +1213,10 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should allow to change time scaling', () => { const props = getProps({ timeScale: 's', label: 'Count of records per second' }); wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); wrapper .find('[data-test-subj="indexPattern-time-scaling-unit"]') .find(EuiSelect) @@ -1321,33 +1333,32 @@ describe('IndexPatternDimensionEditorPanel', () => { } it('should not show custom options if time scaling is not available', () => { - wrapper = shallow( + wrapper = mount( ); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-filter-by-enable"]') + wrapper.find('[data-test-subj="indexPattern-advanced-popover"]').hostNodes() ).toHaveLength(0); }); it('should show custom options if filtering is available', () => { - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); expect( - wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() - .find('[data-test-subj="indexPattern-filter-by-enable"]') + wrapper.find('[data-test-subj="indexPattern-filter-by-enable"]').hostNodes() ).toHaveLength(1); }); @@ -1364,14 +1375,15 @@ describe('IndexPatternDimensionEditorPanel', () => { it('should allow to set filter initially', () => { const props = getProps({}); - wrapper = shallow(); + wrapper = mount(); + wrapper + .find('[data-test-subj="indexPattern-advanced-popover"]') + .hostNodes() + .simulate('click'); wrapper - .find(DimensionEditor) - .dive() - .find(AdvancedOptions) - .dive() .find('[data-test-subj="indexPattern-filter-by-enable"]') - .prop('onClick')!({} as MouseEvent); + .hostNodes() + .simulate('click'); expect(props.setState).toHaveBeenCalledWith( { ...props.state, @@ -1934,6 +1946,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should hide the top level field selector when switching from non-reference to reference', () => { + (generateId as jest.Mock).mockReturnValue(`second`); wrapper = mount(); expect(wrapper.find('ReferenceEditor')).toHaveLength(0); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index 9410843c0811a..a77a980257c88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -904,7 +904,7 @@ describe('IndexPatternDimensionEditorPanel', () => { layers: { first: { ...testState.layers.first, - columnOrder: ['ref1', 'col1', 'ref1Copy', 'col1Copy'], + columnOrder: ['col1', 'ref1', 'ref1Copy', 'col1Copy'], columns: { ref1: testState.layers.first.columns.ref1, col1: testState.layers.first.columns.col1, @@ -974,7 +974,7 @@ describe('IndexPatternDimensionEditorPanel', () => { layers: { first: { ...testState.layers.first, - columnOrder: ['ref1', 'ref2', 'col1', 'ref1Copy', 'ref2Copy', 'col1Copy'], + columnOrder: ['col1', 'ref1', 'ref2', 'ref1Copy', 'col1Copy', 'ref2Copy'], columns: { ref1: testState.layers.first.columns.ref1, ref2: testState.layers.first.columns.ref2, @@ -1061,8 +1061,8 @@ describe('IndexPatternDimensionEditorPanel', () => { 'col1', 'innerRef1Copy', 'ref1Copy', - 'ref2Copy', 'col1Copy', + 'ref2Copy', ], columns: { innerRef1: testState.layers.first.columns.innerRef1, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts index f65557d4ed6a9..e09c3e904f535 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/on_drop_handler.ts @@ -114,7 +114,7 @@ function onMoveCompatible( const modifiedLayer = copyColumn({ layer, - columnId, + targetId: columnId, sourceColumnId: droppedItem.columnId, sourceColumn, shouldDeleteSource, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index f17adf9be39f3..645b6bfe70a97 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -304,6 +304,31 @@ describe('reference editor', () => { ); }); + it('should not display hidden sub-function types', () => { + // This may happen for saved objects after changing the type of a field + wrapper = mount( + true, + }} + /> + ); + + const subFunctionSelect = wrapper + .find('[data-test-subj="indexPattern-reference-function"]') + .first(); + + expect(subFunctionSelect.prop('isInvalid')).toEqual(true); + expect(subFunctionSelect.prop('selectedOptions')).not.toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'math' })]) + ); + expect(subFunctionSelect.prop('selectedOptions')).not.toEqual( + expect.arrayContaining([expect.objectContaining({ value: 'formula' })]) + ); + }); + it('should hide the function selector when using a field-only selection style', () => { wrapper = mount( void; @@ -92,6 +92,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { const operationByField: Partial>> = {}; const fieldByOperation: Partial>> = {}; Object.values(operationDefinitionMap) + .filter(({ hidden }) => !hidden) .sort((op1, op2) => { return op1.displayName.localeCompare(op2.displayName); }) @@ -197,6 +198,10 @@ export function ReferenceEditor(props: ReferenceEditorProps) { return; } + if (selectionStyle === 'hidden') { + return null; + } + const selectedOption = incompleteOperation ? [functionOptions.find(({ value }) => value === incompleteOperation)!] : column @@ -340,6 +345,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { columnId={columnId} indexPattern={currentIndexPattern} dateRange={dateRange} + operationDefinitionMap={operationDefinitionMap} {...services} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index b1ff7b36b47a3..c0a502df14234 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -15,7 +15,7 @@ import { Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; import { operationDefinitionMap, getErrorMessages } from './operations'; -import { createMockedReferenceOperation } from './operations/mocks'; +import { createMockedFullReference } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; @@ -289,6 +289,30 @@ describe('IndexPattern Data Source', () => { expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null); }); + it('should generate an empty expression when there is a formula without aggs', async () => { + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Formula', + dataType: 'number', + isBucketed: false, + operationType: 'formula', + references: [], + params: {}, + }, + }, + }, + }, + }; + const state = enrichBaseState(queryBaseState); + expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null); + }); + it('should generate an expression for an aggregated query', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', @@ -817,7 +841,7 @@ describe('IndexPattern Data Source', () => { describe('references', () => { beforeEach(() => { // @ts-expect-error we are inserting an invalid type - operationDefinitionMap.testReference = createMockedReferenceOperation(); + operationDefinitionMap.testReference = createMockedFullReference(); // @ts-expect-error we are inserting an invalid type operationDefinitionMap.testReference.toExpression.mockReturnValue(['mock']); @@ -900,6 +924,91 @@ describe('IndexPattern Data Source', () => { }), }); }); + + it('should topologically sort references', () => { + // This is a real example of count() + count() + const queryBaseState: IndexPatternBaseState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['date', 'count', 'formula', 'countX0', 'math'], + columns: { + count: { + label: 'count', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + customLabel: true, + }, + date: { + label: 'timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + }, + }, + formula: { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + formula: 'count() + count()', + isFormulaBroken: false, + }, + references: ['math'], + }, + countX0: { + label: 'countX0', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + customLabel: true, + }, + math: { + label: 'math', + dataType: 'number', + operationType: 'math', + isBucketed: false, + scale: 'ratio', + params: { + tinymathAst: { + type: 'function', + name: 'add', + // @ts-expect-error String args are not valid tinymath, but signals something unique to Lens + args: ['countX0', 'count'], + location: { + min: 0, + max: 17, + }, + text: 'count() + count()', + }, + }, + references: ['countX0', 'count'], + customLabel: true, + }, + }, + }, + }, + }; + + const state = enrichBaseState(queryBaseState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + const chainLength = ast.chain.length; + expect(ast.chain[chainLength - 2].arguments.name).toEqual(['math']); + expect(ast.chain[chainLength - 1].arguments.id).toEqual(['formula']); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts index 6ac208913af2e..40d7e3ef94ad6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts @@ -12,6 +12,7 @@ const actualMocks = jest.requireActual('../mocks'); jest.spyOn(actualOperations.operationDefinitionMap.date_histogram, 'paramEditor'); jest.spyOn(actualOperations.operationDefinitionMap.terms, 'onOtherColumnChanged'); +jest.spyOn(actualHelpers, 'copyColumn'); jest.spyOn(actualHelpers, 'insertOrReplaceColumn'); jest.spyOn(actualHelpers, 'insertNewColumn'); jest.spyOn(actualHelpers, 'replaceColumn'); @@ -30,6 +31,7 @@ export const { } = actualOperations; export const { + copyColumn, insertOrReplaceColumn, insertNewColumn, replaceColumn, @@ -50,4 +52,4 @@ export const { export const { adjustTimeScaleLabelSuffix, DEFAULT_TIME_SCALE } = actualTimeScaleUtils; -export const { createMockedReferenceOperation } = actualMocks; +export const { createMockedFullReference } = actualMocks; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx index c57f70ba1b58b..fc9504f003198 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/counter_rate.tsx @@ -17,7 +17,7 @@ import { } from './utils'; import { DEFAULT_TIME_SCALE } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const ofName = buildLabelFunction((name?: string) => { return i18n.translate('xpack.lens.indexPattern.CounterRateOf', { @@ -50,7 +50,7 @@ export const counterRateOperation: OperationDefinition< selectionStyle: 'field', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], specificOperations: ['max'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, @@ -76,7 +76,7 @@ export const counterRateOperation: OperationDefinition< toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'lens_counter_rate'); }, - buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }) => { + buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }, columnParams) => { const metric = layer.columns[referenceIds[0]]; const timeScale = previousColumn?.timeScale || DEFAULT_TIME_SCALE; return { @@ -92,7 +92,7 @@ export const counterRateOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx index 7cec1fa0d4bbc..2adb9a1376f60 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/cumulative_sum.tsx @@ -15,7 +15,7 @@ import { hasDateField, } from './utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const ofName = (name?: string) => { return i18n.translate('xpack.lens.indexPattern.cumulativeSumOf', { @@ -48,7 +48,7 @@ export const cumulativeSumOperation: OperationDefinition< selectionStyle: 'field', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], specificOperations: ['count', 'sum'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, @@ -73,7 +73,7 @@ export const cumulativeSumOperation: OperationDefinition< toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'cumulative_sum'); }, - buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }) => { + buildColumn: ({ referenceIds, previousColumn, layer, indexPattern }, columnParams) => { const ref = layer.columns[referenceIds[0]]; return { label: ofName( @@ -85,7 +85,7 @@ export const cumulativeSumOperation: OperationDefinition< operationType: 'cumulative_sum', isBucketed: false, scale: 'ratio', - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), references: referenceIds, params: getFormatFromPreviousColumn(previousColumn), }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx index bef3fbc2e48ae..06555a9b41c2f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/differences.tsx @@ -17,7 +17,7 @@ import { } from './utils'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { OperationDefinition } from '..'; -import { getFormatFromPreviousColumn } from '../helpers'; +import { getFormatFromPreviousColumn, getFilter } from '../helpers'; const OPERATION_NAME = 'differences'; @@ -52,7 +52,7 @@ export const derivativeOperation: OperationDefinition< selectionStyle: 'full', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], @@ -71,7 +71,7 @@ export const derivativeOperation: OperationDefinition< toExpression: (layer, columnId) => { return dateBasedOperationToExpression(layer, columnId, 'derivative'); }, - buildColumn: ({ referenceIds, previousColumn, layer }) => { + buildColumn: ({ referenceIds, previousColumn, layer }, columnParams) => { const ref = layer.columns[referenceIds[0]]; return { label: ofName(ref?.label, previousColumn?.timeScale), @@ -81,7 +81,7 @@ export const derivativeOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale: previousColumn?.timeScale, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx index 46cc64c2bc518..8d18a2752fd7e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx @@ -19,7 +19,12 @@ import { hasDateField, } from './utils'; import { updateColumnParam } from '../../layer_helpers'; -import { getFormatFromPreviousColumn, isValidNumber, useDebounceWithOptions } from '../helpers'; +import { + getFormatFromPreviousColumn, + isValidNumber, + useDebounceWithOptions, + getFilter, +} from '../helpers'; import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils'; import { HelpPopover, HelpPopoverButton } from '../../../help_popover'; import type { OperationDefinition, ParamEditorProps } from '..'; @@ -37,6 +42,8 @@ const ofName = buildLabelFunction((name?: string) => { }); }); +const WINDOW_DEFAULT_VALUE = 5; + export type MovingAverageIndexPatternColumn = FormattedIndexPatternColumn & ReferenceBasedIndexPatternColumn & { operationType: 'moving_average'; @@ -58,10 +65,11 @@ export const movingAverageOperation: OperationDefinition< selectionStyle: 'full', requiredReferences: [ { - input: ['field'], + input: ['field', 'managedReference'], validateMetadata: (meta) => meta.dataType === 'number' && !meta.isBucketed, }, ], + operationParams: [{ name: 'window', type: 'number', required: true }], getPossibleOperation: (indexPattern) => { if (hasDateField(indexPattern)) { return { @@ -79,8 +87,12 @@ export const movingAverageOperation: OperationDefinition< window: [(layer.columns[columnId] as MovingAverageIndexPatternColumn).params.window], }); }, - buildColumn: ({ referenceIds, previousColumn, layer }) => { + buildColumn: ( + { referenceIds, previousColumn, layer }, + columnParams = { window: WINDOW_DEFAULT_VALUE } + ) => { const metric = layer.columns[referenceIds[0]]; + const { window = WINDOW_DEFAULT_VALUE } = columnParams; return { label: ofName(metric?.label, previousColumn?.timeScale), dataType: 'number', @@ -89,9 +101,9 @@ export const movingAverageOperation: OperationDefinition< scale: 'ratio', references: referenceIds, timeScale: previousColumn?.timeScale, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: { - window: 5, + window, ...getFormatFromPreviousColumn(previousColumn), }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts index 4c1101d4c8a79..7a6f96d705b0c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts @@ -7,7 +7,7 @@ import { checkReferences } from './utils'; import { operationDefinitionMap } from '..'; -import { createMockedReferenceOperation } from '../../mocks'; +import { createMockedFullReference } from '../../mocks'; // Mock prevents issue with circular loading jest.mock('..'); @@ -15,7 +15,7 @@ jest.mock('..'); describe('utils', () => { beforeEach(() => { // @ts-expect-error test-only operation type - operationDefinitionMap.testReference = createMockedReferenceOperation(); + operationDefinitionMap.testReference = createMockedFullReference(); }); describe('checkReferences', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index fa1691ba9a78e..e77357a6f441a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -11,7 +11,12 @@ import { buildExpressionFunction } from '../../../../../../../src/plugins/expres import { OperationDefinition } from './index'; import { FormattedIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; -import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers'; +import { + getFormatFromPreviousColumn, + getInvalidFieldMessage, + getSafeName, + getFilter, +} from './helpers'; const supportedTypes = new Set([ 'string', @@ -71,8 +76,13 @@ export const cardinalityOperation: OperationDefinition ofName(getSafeName(column.sourceField, indexPattern)), - buildColumn({ field, previousColumn }) { + buildColumn({ field, previousColumn }, columnParams) { return { label: ofName(field.displayName), dataType: 'number', @@ -80,7 +90,7 @@ export const cardinalityOperation: OperationDefinition adjustTimeScaleLabelSuffix(countLabel, undefined, column.timeScale), - buildColumn({ field, previousColumn }) { + buildColumn({ field, previousColumn }, columnParams) { return { label: adjustTimeScaleLabelSuffix(countLabel, undefined, previousColumn?.timeScale), dataType: 'number', @@ -61,7 +61,7 @@ export const countOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index bd7a270fd7ad8..affb84484c820 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -58,6 +58,7 @@ export const dateHistogramOperation: OperationDefinition< }), input: 'field', priority: 5, // Highest priority level used + operationParams: [{ name: 'interval', type: 'string', required: false }], getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getHelpMessage: (props) => , @@ -75,8 +76,8 @@ export const dateHistogramOperation: OperationDefinition< } }, getDefaultLabel: (column, indexPattern) => getSafeName(column.sourceField, indexPattern), - buildColumn({ field }) { - let interval = autoInterval; + buildColumn({ field }, columnParams) { + let interval = columnParams?.interval ?? autoInterval; let timeZone: string | undefined; if (field.aggregationRestrictions && field.aggregationRestrictions.date_histogram) { interval = restrictedInterval(field.aggregationRestrictions) as string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index ae097ada0f3b7..46fddd9b1ffbf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -27,6 +27,7 @@ const defaultProps = { data: dataPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), + operationDefinitionMap: {}, }; // mocking random id generator function diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx new file mode 100644 index 0000000000000..4a511e14d59e0 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -0,0 +1,987 @@ +/* + * 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 { createMockedIndexPattern } from '../../../mocks'; +import { formulaOperation, GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { FormulaIndexPatternColumn } from './formula'; +import { regenerateLayerFromAst } from './parse'; +import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../../types'; +import { tinymathFunctions } from './util'; + +jest.mock('../../layer_helpers', () => { + return { + getColumnOrder: ({ columns }: { columns: Record }) => + Object.keys(columns), + }; +}); + +const operationDefinitionMap: Record = { + average: ({ + input: 'field', + buildColumn: ({ field }: { field: IndexPatternField }) => ({ + label: 'avg', + dataType: 'number', + operationType: 'average', + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + timeScale: false, + }), + } as unknown) as GenericOperationDefinition, + terms: { input: 'field' } as GenericOperationDefinition, + sum: { input: 'field' } as GenericOperationDefinition, + last_value: { input: 'field' } as GenericOperationDefinition, + max: { input: 'field' } as GenericOperationDefinition, + count: ({ + input: 'field', + filterable: true, + buildColumn: ({ field }: { field: IndexPatternField }) => ({ + label: 'avg', + dataType: 'number', + operationType: 'count', + sourceField: field.name, + isBucketed: false, + scale: 'ratio', + timeScale: false, + }), + } as unknown) as GenericOperationDefinition, + derivative: { input: 'fullReference' } as GenericOperationDefinition, + moving_average: ({ + input: 'fullReference', + operationParams: [{ name: 'window', type: 'number', required: true }], + buildColumn: ({ references }: { references: string[] }) => ({ + label: 'moving_average', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + timeScale: false, + params: { window: 5 }, + references, + }), + getErrorMessage: () => ['mock error'], + } as unknown) as GenericOperationDefinition, + cumulative_sum: { input: 'fullReference' } as GenericOperationDefinition, +}; + +describe('formula', () => { + let layer: IndexPatternLayer; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + }, + }; + }); + + describe('buildColumn', () => { + let indexPattern: IndexPattern; + + beforeEach(() => { + layer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Average', + dataType: 'number', + operationType: 'average', + isBucketed: false, + scale: 'ratio', + sourceField: 'bytes', + }, + }, + }; + indexPattern = createMockedIndexPattern(); + }); + + it('should start with an empty formula if no previous column is detected', () => { + expect( + formulaOperation.buildColumn({ + layer: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: {}, + references: [], + }); + }); + + it('should move into Formula previous operation', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: layer.columns.col1, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { isFormulaBroken: false, formula: 'average(bytes)' }, + references: [], + }); + }); + + it('it should move over explicit format param if set', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + ...layer.columns.col1, + params: { + format: { + id: 'number', + params: { + decimals: 2, + }, + }, + }, + } as IndexPatternColumn, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: 'average(bytes)', + format: { + id: 'number', + params: { + decimals: 2, + }, + }, + }, + references: [], + }); + }); + + it('it should move over kql arguments if set', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + ...layer.columns.col1, + filter: { + language: 'kuery', + // Need to test with multiple replaces due to string replace + query: `category.keyword: "Men's Clothing" or category.keyword: "Men's Shoes"`, + }, + } as IndexPatternColumn, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: `average(bytes, kql='category.keyword: "Men\\'s Clothing" or category.keyword: "Men\\'s Shoes"')`, + }, + references: [], + }); + }); + + it('it should move over lucene arguments without', () => { + expect( + formulaOperation.buildColumn({ + previousColumn: { + ...layer.columns.col1, + operationType: 'count', + sourceField: 'Records', + filter: { + language: 'lucene', + query: `*`, + }, + } as IndexPatternColumn, + layer, + indexPattern, + }) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: `count(lucene='*')`, + }, + references: [], + }); + }); + + it('should move over previous operation parameter if set - only numeric', () => { + expect( + formulaOperation.buildColumn( + { + previousColumn: { + label: 'Moving Average', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + references: ['col2'], + timeScale: 'd', + params: { window: 3 }, + }, + layer: { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: { + label: 'Moving Average', + dataType: 'number', + operationType: 'moving_average', + isBucketed: false, + scale: 'ratio', + references: ['col2'], + timeScale: 'd', + params: { window: 3 }, + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'col1X0', + operationType: 'average', + scale: 'ratio', + sourceField: 'bytes', + timeScale: 'd', + }, + }, + }, + indexPattern, + }, + {}, + operationDefinitionMap + ) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { + isFormulaBroken: false, + formula: 'moving_average(average(bytes), window=3)', + }, + references: [], + }); + }); + + it('should not move previous column configuration if not numeric', () => { + expect( + formulaOperation.buildColumn( + { + previousColumn: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + layer: { + indexPatternId: '1', + columnOrder: [], + columns: { + col1: { + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 3, + orderDirection: 'asc', + }, + sourceField: 'category', + }, + }, + }, + indexPattern, + }, + {}, + operationDefinitionMap + ) + ).toEqual({ + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: {}, + references: [], + }); + }); + }); + + describe('regenerateLayerFromAst()', () => { + let indexPattern: IndexPattern; + let currentColumn: FormulaIndexPatternColumn; + + function testIsBrokenFormula(formula: string) { + expect( + regenerateLayerFromAst( + formula, + layer, + 'col1', + currentColumn, + indexPattern, + operationDefinitionMap + ).newLayer + ).toEqual({ + ...layer, + columns: { + ...layer.columns, + col1: { + ...currentColumn, + params: { + ...currentColumn.params, + formula, + isFormulaBroken: true, + }, + }, + }, + }); + } + + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + currentColumn = { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula: '', isFormulaBroken: false }, + references: [], + }; + }); + + it('should mutate the layer with new columns for valid formula expressions', () => { + expect( + regenerateLayerFromAst( + 'average(bytes)', + layer, + 'col1', + currentColumn, + indexPattern, + operationDefinitionMap + ).newLayer + ).toEqual({ + ...layer, + columnOrder: ['col1X0', 'col1X1', 'col1'], + columns: { + ...layer.columns, + col1: { + ...currentColumn, + references: ['col1X1'], + params: { + ...currentColumn.params, + formula: 'average(bytes)', + isFormulaBroken: false, + }, + }, + col1X0: { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'col1X0', + operationType: 'average', + scale: 'ratio', + sourceField: 'bytes', + timeScale: false, + }, + col1X1: { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'col1X1', + operationType: 'math', + params: { + tinymathAst: 'col1X0', + }, + references: ['col1X0'], + scale: 'ratio', + }, + }, + }); + }); + + it('returns no change but error if the formula cannot be parsed', () => { + const formulas = [ + '+', + 'average((', + 'average((bytes)', + 'average(bytes) +', + 'average(""', + 'moving_average(average(bytes), window=)', + 'average(bytes) + moving_average(average(bytes), window=)', + ]; + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if field is used with no Lens wrapping operation', () => { + testIsBrokenFormula('bytes'); + }); + + it('returns no change but error if at least one field in the formula is missing', () => { + const formulas = [ + 'noField', + 'average(noField)', + 'noField + 1', + 'derivative(average(noField))', + 'average(bytes) + derivative(average(noField))', + ]; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if at least one operation in the formula is missing', () => { + const formulas = [ + 'noFn()', + 'noFn(bytes)', + 'average(bytes) + noFn()', + 'derivative(noFn())', + 'noFn() + noFnTwo()', + 'noFn(noFnTwo())', + 'noFn() + noFnTwo() + 5', + 'average(bytes) + derivative(noFn())', + 'derivative(average(bytes) + noFn())', + ]; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if one operation has the wrong first argument', () => { + const formulas = [ + 'average(7)', + 'average()', + 'average(average(bytes))', + 'average(1 + 2)', + 'average(bytes + 5)', + 'average(bytes + bytes)', + 'derivative(7)', + 'derivative(bytes + 7)', + 'derivative(bytes + bytes)', + 'derivative(bytes + average(bytes))', + 'derivative(bytes + 7 + average(bytes))', + ]; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if an argument is passed to count operation', () => { + const formulas = ['count(7)', 'count("bytes")', 'count(bytes)']; + + for (const formula of formulas) { + testIsBrokenFormula(formula); + } + }); + + it('returns no change but error if a required parameter is not passed to the operation in formula', () => { + const formula = 'moving_average(average(bytes))'; + testIsBrokenFormula(formula); + }); + + it('returns no change but error if a required parameter passed with the wrong type in formula', () => { + const formula = 'moving_average(average(bytes), window="m")'; + testIsBrokenFormula(formula); + }); + + it('returns error if a required parameter is passed multiple time', () => { + const formula = 'moving_average(average(bytes), window=7, window=3)'; + testIsBrokenFormula(formula); + }); + + it('returns error if a math operation has less arguments than required', () => { + const formula = 'pow(5)'; + testIsBrokenFormula(formula); + }); + + it('returns error if a math operation has the wrong argument type', () => { + const formula = 'pow(bytes)'; + testIsBrokenFormula(formula); + }); + + it('returns the locations of each function', () => { + expect( + regenerateLayerFromAst( + 'moving_average(average(bytes), window=7) + count()', + layer, + 'col1', + currentColumn, + indexPattern, + operationDefinitionMap + ).locations + ).toEqual({ + col1X0: { min: 15, max: 29 }, + col1X2: { min: 0, max: 41 }, + col1X3: { min: 43, max: 50 }, + }); + }); + }); + + describe('getErrorMessage', () => { + let indexPattern: IndexPattern; + + function getNewLayerWithFormula(formula: string, isBroken = true): IndexPatternLayer { + return { + columns: { + col1: { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: { formula, isFormulaBroken: isBroken }, + references: [], + }, + }, + columnOrder: [], + indexPatternId: '', + }; + } + beforeEach(() => { + indexPattern = createMockedIndexPattern(); + }); + + it('returns undefined if count is passed without arguments', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('count()'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns undefined if count is passed with only a named argument', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`count(kql='*')`, false), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns a syntax error if the kql argument does not parse', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(`count(kql='invalid: "')`, false), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + `Expected "(", "{", value, whitespace but """ found. +invalid: " +---------^`, + ]); + }); + + it('returns undefined if a field operation is passed with the correct first argument', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('average(bytes)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + // note that field names can be wrapped in quotes as well + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('average("bytes")'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns undefined if a fullReference operation is passed with the correct first argument', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('derivative(average(bytes))'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('derivative(average("bytes"))'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns undefined if a fullReference operation is passed with the arguments', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes), window=7)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns an error if field is used with no Lens wrapping operation', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('bytes'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The field bytes cannot be used without operation`]); + + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('bytes + bytes'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The operation add does not accept any field as argument`]); + }); + + it('returns an error if parsing a syntax invalid formula', () => { + const formulas = [ + '+', + 'average((', + 'average((bytes)', + 'average(bytes) +', + 'average(""', + 'moving_average(average(bytes), window=)', + ]; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([`The Formula ${formula} cannot be parsed`]); + } + }); + + it('returns an error if the field is missing', () => { + const formulas = [ + 'noField', + 'average(noField)', + 'noField + 1', + 'derivative(average(noField))', + ]; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Field noField not found']); + } + }); + + it('returns an error with plural form correctly handled', () => { + const formulas = ['noField + noField2', 'noField + 1 + noField2']; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Fields noField, noField2 not found']); + } + }); + + it('returns an error if an operation is unknown', () => { + const formulas = ['noFn()', 'noFn(bytes)', 'average(bytes) + noFn()', 'derivative(noFn())']; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Operation noFn not found']); + } + + const multipleFnFormulas = ['noFn() + noFnTwo()', 'noFn(noFnTwo())']; + + for (const formula of multipleFnFormulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['Operations noFn, noFnTwo not found']); + } + }); + + it('returns an error if field operation in formula have the wrong first argument', () => { + const formulas = [ + 'average(7)', + 'average()', + 'average(average(bytes))', + 'average(1 + 2)', + 'average(bytes + 5)', + 'average(bytes + bytes)', + 'derivative(7)', + ]; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual( + // some formulas may contain more errors + expect.arrayContaining([ + expect.stringMatching( + `The first argument for ${formula.substring(0, formula.indexOf('('))}` + ), + ]) + ); + } + }); + + it('returns an error if an argument is passed to count() operation', () => { + const formulas = ['count(7)', 'count("bytes")', 'count(bytes)']; + + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['The operation count does not accept any field as argument']); + } + }); + + it('returns an error if an operation with required parameters does not receive them', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes))'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + 'The operation moving_average in the Formula is missing the following parameters: window', + ]); + + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes), myparam=7)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + 'The operation moving_average in the Formula is missing the following parameters: window', + ]); + }); + + it('returns an error if a parameter is passed to an operation with no parameters', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('average(bytes, myparam=7)'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(['The operation average does not accept any parameter']); + }); + + it('returns an error if the parameter passed to an operation is of the wrong type', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula('moving_average(average(bytes), window="m")'), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([ + 'The parameters for the operation moving_average in the Formula are of the wrong type: window', + ]); + }); + + it('returns no error for the demo formula example', () => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(` + moving_average( + cumulative_sum( + 7 * clamp(sum(bytes), 0, last_value(memory) + max(memory)) + ), window=10 + ) + `), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + }); + + it('returns no error if a math operation is passed to fullReference operations', () => { + const formulas = [ + 'derivative(7+1)', + 'derivative(7+average(bytes))', + 'moving_average(7+average(bytes), window=7)', + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + } + }); + + it('returns errors if math operations are used with no arguments', () => { + const formulas = [ + 'derivative(7+1)', + 'derivative(7+average(bytes))', + 'moving_average(7+average(bytes), window=7)', + ]; + for (const formula of formulas) { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual(undefined); + } + }); + + // there are 4 types of errors for math functions: + // * no argument passed + // * too many arguments passed + // * field passed + // * missing argument + const errors = [ + (operation: string) => + `The first argument for ${operation} should be a operation name. Found ()`, + (operation: string) => `The operation ${operation} has too many arguments`, + (operation: string) => `The operation ${operation} does not accept any field as argument`, + (operation: string) => { + const required = tinymathFunctions[operation].positionalArguments.filter( + ({ optional }) => !optional + ); + return `The operation ${operation} in the Formula is missing ${ + required.length - 1 + } arguments: ${required + .slice(1) + .map(({ name }) => name) + .join(', ')}`; + }, + ]; + // we'll try to map all of these here in this test + for (const fn of Object.keys(tinymathFunctions)) { + it(`returns an error for the math functions available: ${fn}`, () => { + const nArgs = tinymathFunctions[fn].positionalArguments; + // start with the first 3 types + const formulas = [ + `${fn}()`, + `${fn}(1, 2, 3, 4, 5)`, + // to simplify a bit, add the required number of args by the function filled with the field name + `${fn}(${Array(nArgs.length).fill('bytes').join(', ')})`, + ]; + // add the fourth check only for those functions with more than 1 arg required + const enableFourthCheck = nArgs.filter(({ optional }) => !optional).length > 1; + if (enableFourthCheck) { + formulas.push(`${fn}(1)`); + } + formulas.forEach((formula, i) => { + expect( + formulaOperation.getErrorMessage!( + getNewLayerWithFormula(formula), + 'col1', + indexPattern, + operationDefinitionMap + ) + ).toEqual([errors[i](fn)]); + }); + }); + } + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx new file mode 100644 index 0000000000000..de7ecb4bc75da --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.tsx @@ -0,0 +1,155 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { OperationDefinition } from '../index'; +import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { IndexPattern } from '../../../types'; +import { runASTValidation, tryToParse } from './validation'; +import { regenerateLayerFromAst } from './parse'; +import { generateFormula } from './generate'; + +const defaultLabel = i18n.translate('xpack.lens.indexPattern.formulaLabel', { + defaultMessage: 'Formula', +}); + +export interface FormulaIndexPatternColumn extends ReferenceBasedIndexPatternColumn { + operationType: 'formula'; + params: { + formula?: string; + isFormulaBroken?: boolean; + // last value on numeric fields can be formatted + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const formulaOperation: OperationDefinition< + FormulaIndexPatternColumn, + 'managedReference' +> = { + type: 'formula', + displayName: defaultLabel, + getDefaultLabel: (column, indexPattern) => defaultLabel, + input: 'managedReference', + hidden: true, + getDisabledStatus(indexPattern: IndexPattern) { + return undefined; + }, + getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap) { + const column = layer.columns[columnId] as FormulaIndexPatternColumn; + if (!column.params.formula || !operationDefinitionMap) { + return; + } + const { root, error } = tryToParse(column.params.formula); + if (error || !root) { + return [error!.message]; + } + + const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap); + return errors.length ? errors.map(({ message }) => message) : undefined; + }, + getPossibleOperation() { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + }, + toExpression: (layer, columnId) => { + const currentColumn = layer.columns[columnId] as FormulaIndexPatternColumn; + const params = currentColumn.params; + // TODO: improve this logic + const useDisplayLabel = currentColumn.label !== defaultLabel; + const label = !params?.isFormulaBroken + ? useDisplayLabel + ? currentColumn.label + : params?.formula + : ''; + + return [ + { + type: 'function', + function: 'mapColumn', + arguments: { + id: [columnId], + name: [label || ''], + exp: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'math', + arguments: { + expression: [ + currentColumn.references.length ? `"${currentColumn.references[0]}"` : ``, + ], + }, + }, + ], + }, + ], + }, + }, + ]; + }, + buildColumn({ previousColumn, layer, indexPattern }, _, operationDefinitionMap) { + let previousFormula = ''; + if (previousColumn) { + previousFormula = generateFormula( + previousColumn, + layer, + previousFormula, + operationDefinitionMap + ); + } + // carry over the format settings from previous operation for seamless transfer + // NOTE: this works only for non-default formatters set in Lens + let prevFormat = {}; + if (previousColumn?.params && 'format' in previousColumn.params) { + prevFormat = { format: previousColumn.params.format }; + } + return { + label: 'Formula', + dataType: 'number', + operationType: 'formula', + isBucketed: false, + scale: 'ratio', + params: previousFormula + ? { formula: previousFormula, isFormulaBroken: false, ...prevFormat } + : { ...prevFormat }, + references: [], + }; + }, + isTransferable: () => { + return true; + }, + createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap) { + const currentColumn = layer.columns[sourceId] as FormulaIndexPatternColumn; + const tempLayer = { + ...layer, + columns: { + ...layer.columns, + [targetId]: { ...currentColumn }, + }, + }; + const { newLayer } = regenerateLayerFromAst( + currentColumn.params.formula ?? '', + tempLayer, + targetId, + currentColumn, + indexPattern, + operationDefinitionMap + ); + return newLayer; + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts new file mode 100644 index 0000000000000..e44cd50ae9c41 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts @@ -0,0 +1,90 @@ +/* + * 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 { isObject } from 'lodash'; +import { GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { IndexPatternLayer } from '../../../types'; + +// Just handle two levels for now +type OperationParams = Record>; + +export function getSafeFieldName(fieldName: string | undefined) { + // clean up the "Records" field for now + if (!fieldName || fieldName === 'Records') { + return ''; + } + return fieldName; +} + +export function generateFormula( + previousColumn: ReferenceBasedIndexPatternColumn | IndexPatternColumn, + layer: IndexPatternLayer, + previousFormula: string, + operationDefinitionMap: Record | undefined +) { + if ('references' in previousColumn) { + const metric = layer.columns[previousColumn.references[0]]; + if (metric && 'sourceField' in metric && metric.dataType === 'number') { + const fieldName = getSafeFieldName(metric.sourceField); + // TODO need to check the input type from the definition + previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`; + } + } else { + if (previousColumn && 'sourceField' in previousColumn && previousColumn.dataType === 'number') { + previousFormula += `${previousColumn.operationType}(${getSafeFieldName( + previousColumn?.sourceField + )}`; + } + } + const formulaNamedArgs = extractParamsForFormula(previousColumn, operationDefinitionMap); + if (formulaNamedArgs.length) { + previousFormula += + ', ' + formulaNamedArgs.map(({ name, value }) => `${name}=${value}`).join(', '); + } + if (previousColumn.filter) { + if (previousColumn.operationType !== 'count') { + previousFormula += ', '; + } + previousFormula += + (previousColumn.filter.language === 'kuery' ? 'kql=' : 'lucene=') + + `'${previousColumn.filter.query.replace(/'/g, `\\'`)}'`; // replace all + } + if (previousFormula) { + // close the formula at the end + previousFormula += ')'; + } + return previousFormula; +} + +function extractParamsForFormula( + column: IndexPatternColumn | ReferenceBasedIndexPatternColumn, + operationDefinitionMap: Record | undefined +) { + if (!operationDefinitionMap) { + return []; + } + const def = operationDefinitionMap[column.operationType]; + if ('operationParams' in def && column.params) { + return (def.operationParams || []).flatMap(({ name, required }) => { + const value = (column.params as OperationParams)![name]; + if (isObject(value)) { + return Object.keys(value).map((subName) => ({ + name: `${name}-${subName}`, + value: value[subName] as string | number, + required, + })); + } + return { + name, + value, + required, + }; + }); + } + return []; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts new file mode 100644 index 0000000000000..bafde0d37b3e9 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { formulaOperation, FormulaIndexPatternColumn } from './formula'; +export { regenerateLayerFromAst } from './parse'; +export { mathOperation, MathIndexPatternColumn } from './math'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx new file mode 100644 index 0000000000000..527af324b5b05 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/math.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TinymathAST } from '@kbn/tinymath'; +import { OperationDefinition } from '../index'; +import { ReferenceBasedIndexPatternColumn } from '../column_types'; +import { IndexPattern } from '../../../types'; + +export interface MathIndexPatternColumn extends ReferenceBasedIndexPatternColumn { + operationType: 'math'; + params: { + tinymathAst: TinymathAST | string; + // last value on numeric fields can be formatted + format?: { + id: string; + params?: { + decimals: number; + }; + }; + }; +} + +export const mathOperation: OperationDefinition = { + type: 'math', + displayName: 'Math', + hidden: true, + getDefaultLabel: (column, indexPattern) => 'Math', + input: 'managedReference', + getDisabledStatus(indexPattern: IndexPattern) { + return undefined; + }, + getPossibleOperation() { + return { + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }; + }, + toExpression: (layer, columnId) => { + const column = layer.columns[columnId] as MathIndexPatternColumn; + return [ + { + type: 'function', + function: 'mapColumn', + arguments: { + id: [columnId], + name: [columnId], + exp: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'math', + arguments: { + expression: [astToString(column.params.tinymathAst)], + onError: ['null'], + }, + }, + ], + }, + ], + }, + }, + ]; + }, + buildColumn() { + return { + label: 'Math', + dataType: 'number', + operationType: 'math', + isBucketed: false, + scale: 'ratio', + params: { + tinymathAst: '', + }, + references: [], + }; + }, + isTransferable: (column, newIndexPattern) => { + // TODO has to check all children + return true; + }, + createCopy: (layer) => { + return { ...layer }; + }, +}; + +function astToString(ast: TinymathAST | string): string | number { + if (typeof ast === 'number') { + return ast; + } + if (typeof ast === 'string') { + // Double quotes around uuids like 1234-5678X2 to avoid ambiguity + return `"${ast}"`; + } + if (ast.type === 'variable') { + return ast.value; + } + if (ast.type === 'namedArgument') { + if (ast.name === 'kql' || ast.name === 'lucene') { + return `${ast.name}='${ast.value}'`; + } + return `${ast.name}=${ast.value}`; + } + return `${ast.name}(${ast.args.map(astToString).join(',')})`; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts new file mode 100644 index 0000000000000..3bfc6fcbfc011 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -0,0 +1,210 @@ +/* + * 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 { isObject } from 'lodash'; +import type { TinymathAST, TinymathVariable, TinymathLocation } from '@kbn/tinymath'; +import { OperationDefinition, GenericOperationDefinition, IndexPatternColumn } from '../index'; +import { IndexPattern, IndexPatternLayer } from '../../../types'; +import { mathOperation } from './math'; +import { documentField } from '../../../document_field'; +import { runASTValidation, shouldHaveFieldArgument, tryToParse } from './validation'; +import { findVariables, getOperationParams, groupArgsByType } from './util'; +import { FormulaIndexPatternColumn } from './formula'; +import { getColumnOrder } from '../../layer_helpers'; + +function getManagedId(mainId: string, index: number) { + return `${mainId}X${index}`; +} + +function parseAndExtract( + text: string, + layer: IndexPatternLayer, + columnId: string, + indexPattern: IndexPattern, + operationDefinitionMap: Record +) { + const { root, error } = tryToParse(text); + if (error || !root) { + return { extracted: [], isValid: false }; + } + // before extracting the data run the validation task and throw if invalid + const errors = runASTValidation(root, layer, indexPattern, operationDefinitionMap); + if (errors.length) { + return { extracted: [], isValid: false }; + } + /* + { name: 'add', args: [ { name: 'abc', args: [5] }, 5 ] } + */ + const extracted = extractColumns(columnId, operationDefinitionMap, root, layer, indexPattern); + return { extracted, isValid: true }; +} + +function extractColumns( + idPrefix: string, + operations: Record, + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern +): Array<{ column: IndexPatternColumn; location?: TinymathLocation }> { + const columns: Array<{ column: IndexPatternColumn; location?: TinymathLocation }> = []; + + function parseNode(node: TinymathAST) { + if (typeof node === 'number' || node.type !== 'function') { + // leaf node + return node; + } + + const nodeOperation = operations[node.name]; + if (!nodeOperation) { + // it's a regular math node + const consumedArgs = node.args.map(parseNode).filter(Boolean) as Array< + number | TinymathVariable + >; + return { + ...node, + args: consumedArgs, + }; + } + + // split the args into types for better TS experience + const { namedArguments, variables, functions } = groupArgsByType(node.args); + + // operation node + if (nodeOperation.input === 'field') { + const [fieldName] = variables.filter((v): v is TinymathVariable => isObject(v)); + // a validation task passed before executing this and checked already there's a field + const field = shouldHaveFieldArgument(node) + ? indexPattern.getFieldByName(fieldName.value)! + : documentField; + + const mappedParams = getOperationParams(nodeOperation, namedArguments || []); + + const newCol = (nodeOperation as OperationDefinition< + IndexPatternColumn, + 'field' + >).buildColumn( + { + layer, + indexPattern, + field, + }, + mappedParams + ); + const newColId = getManagedId(idPrefix, columns.length); + newCol.customLabel = true; + newCol.label = newColId; + columns.push({ column: newCol, location: node.location }); + // replace by new column id + return newColId; + } + + if (nodeOperation.input === 'fullReference') { + const [referencedOp] = functions; + const consumedParam = parseNode(referencedOp); + + const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = subNodeVariables.map(({ value }) => value); + mathColumn.params.tinymathAst = consumedParam!; + columns.push({ column: mathColumn }); + mathColumn.customLabel = true; + mathColumn.label = getManagedId(idPrefix, columns.length - 1); + + const mappedParams = getOperationParams(nodeOperation, namedArguments || []); + const newCol = (nodeOperation as OperationDefinition< + IndexPatternColumn, + 'fullReference' + >).buildColumn( + { + layer, + indexPattern, + referenceIds: [getManagedId(idPrefix, columns.length - 1)], + }, + mappedParams + ); + const newColId = getManagedId(idPrefix, columns.length); + newCol.customLabel = true; + newCol.label = newColId; + columns.push({ column: newCol, location: node.location }); + // replace by new column id + return newColId; + } + } + const root = parseNode(ast); + if (root === undefined) { + return []; + } + const variables = findVariables(root); + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = variables.map(({ value }) => value); + mathColumn.params.tinymathAst = root!; + const newColId = getManagedId(idPrefix, columns.length); + mathColumn.customLabel = true; + mathColumn.label = newColId; + columns.push({ column: mathColumn }); + return columns; +} + +export function regenerateLayerFromAst( + text: string, + layer: IndexPatternLayer, + columnId: string, + currentColumn: FormulaIndexPatternColumn, + indexPattern: IndexPattern, + operationDefinitionMap: Record +) { + const { extracted, isValid } = parseAndExtract( + text, + layer, + columnId, + indexPattern, + operationDefinitionMap + ); + + const columns = { ...layer.columns }; + + const locations: Record = {}; + + Object.keys(columns).forEach((k) => { + if (k.startsWith(columnId)) { + delete columns[k]; + } + }); + + extracted.forEach(({ column, location }, index) => { + columns[getManagedId(columnId, index)] = column; + if (location) locations[getManagedId(columnId, index)] = location; + }); + + columns[columnId] = { + ...currentColumn, + params: { + ...currentColumn.params, + formula: text, + isFormulaBroken: !isValid, + }, + references: !isValid ? [] : [getManagedId(columnId, extracted.length - 1)], + }; + + return { + newLayer: { + ...layer, + columns, + columnOrder: getColumnOrder({ + ...layer, + columns, + }), + }, + locations, + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts new file mode 100644 index 0000000000000..ce853dec1d951 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + TinymathAST, + TinymathFunction, + TinymathNamedArgument, + TinymathVariable, +} from 'packages/kbn-tinymath'; + +export type GroupedNodes = { + [Key in TinymathNamedArgument['type']]: TinymathNamedArgument[]; +} & + { + [Key in TinymathVariable['type']]: Array; + } & + { + [Key in TinymathFunction['type']]: TinymathFunction[]; + }; + +export type TinymathNodeTypes = Exclude; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts new file mode 100644 index 0000000000000..5d9a8647eb7ab --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -0,0 +1,317 @@ +/* + * 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 { groupBy, isObject } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import type { + TinymathAST, + TinymathFunction, + TinymathNamedArgument, + TinymathVariable, +} from 'packages/kbn-tinymath'; +import type { OperationDefinition, IndexPatternColumn } from '../index'; +import type { GroupedNodes } from './types'; + +export function groupArgsByType(args: TinymathAST[]) { + const { namedArgument, variable, function: functions } = groupBy( + args, + (arg: TinymathAST) => { + return isObject(arg) ? arg.type : 'variable'; + } + ) as GroupedNodes; + // better naming + return { + namedArguments: namedArgument || [], + variables: variable || [], + functions: functions || [], + }; +} + +export function getValueOrName(node: TinymathAST) { + if (!isObject(node)) { + return node; + } + if (node.type !== 'function') { + return node.value; + } + return node.name; +} + +export function getOperationParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +): Record { + const formalArgs: Record = (operation.operationParams || []).reduce( + (memo: Record, { name, type }) => { + memo[name] = type; + return memo; + }, + {} + ); + + return params.reduce>((args, { name, value }) => { + if (formalArgs[name]) { + args[name] = value; + } + if (operation.filterable && (name === 'kql' || name === 'lucene')) { + args[name] = value; + } + return args; + }, {}); +} + +// Todo: i18n everything here +export const tinymathFunctions: Record< + string, + { + positionalArguments: Array<{ + name: string; + optional?: boolean; + }>; + // help: React.ReactElement; + // Help is in Markdown format + help: string; + } +> = { + add: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with + symbol +Example: ${'`count() + sum(bytes)`'} +Example: ${'`add(count(), 5)`'} + `, + }, + subtract: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with ${'`-`'} symbol +Example: ${'`subtract(sum(bytes), avg(bytes))`'} + `, + }, + multiply: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with ${'`*`'} symbol +Example: ${'`multiply(sum(bytes), 2)`'} + `, + }, + divide: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }) }, + { name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }) }, + ], + help: ` +Also works with ${'`/`'} symbol +Example: ${'`ceil(sum(bytes))`'} + `, + }, + abs: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Absolute value +Example: ${'`abs(sum(bytes))`'} + `, + }, + cbrt: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Cube root of value +Example: ${'`cbrt(sum(bytes))`'} + `, + }, + ceil: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Ceiling of value, rounds up +Example: ${'`ceil(sum(bytes))`'} + `, + }, + clamp: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { name: i18n.translate('xpack.lens.formula.min', { defaultMessage: 'min' }) }, + { name: i18n.translate('xpack.lens.formula.max', { defaultMessage: 'max' }) }, + ], + help: ` +Limits the value from a minimum to maximum +Example: ${'`ceil(sum(bytes))`'} + `, + }, + cube: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Limits the value from a minimum to maximum +Example: ${'`ceil(sum(bytes))`'} + `, + }, + exp: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Raises e to the nth power. +Example: ${'`exp(sum(bytes))`'} + `, + }, + fix: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +For positive values, takes the floor. For negative values, takes the ceiling. +Example: ${'`fix(sum(bytes))`'} + `, + }, + floor: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Round down to nearest integer value +Example: ${'`floor(sum(bytes))`'} + `, + }, + log: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + optional: true, + }, + ], + help: ` +Logarithm with optional base. The natural base e is used as default. +Example: ${'`log(sum(bytes))`'} +Example: ${'`log(sum(bytes), 2)`'} + `, + }, + // TODO: check if this is valid for Tinymath + // log10: { + // positionalArguments: [ + // { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + // ], + // help: ` + // Base 10 logarithm. + // Example: ${'`log10(sum(bytes))`'} + // `, + // }, + mod: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + optional: true, + }, + ], + help: ` +Remainder after dividing the function by a number +Example: ${'`mod(sum(bytes), 2)`'} + `, + }, + pow: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.base', { defaultMessage: 'base' }), + }, + ], + help: ` +Raises the value to a certain power. The second argument is required +Example: ${'`pow(sum(bytes), 3)`'} + `, + }, + round: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + { + name: i18n.translate('xpack.lens.formula.decimals', { defaultMessage: 'decimals' }), + optional: true, + }, + ], + help: ` +Rounds to a specific number of decimal places, default of 0 +Example: ${'`round(sum(bytes))`'} +Example: ${'`round(sum(bytes), 2)`'} + `, + }, + sqrt: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Square root of a positive value only +Example: ${'`sqrt(sum(bytes))`'} + `, + }, + square: { + positionalArguments: [ + { name: i18n.translate('xpack.lens.formula.expression', { defaultMessage: 'expression' }) }, + ], + help: ` +Raise the value to the 2nd power +Example: ${'`square(sum(bytes))`'} + `, + }, +}; + +export function isMathNode(node: TinymathAST) { + return isObject(node) && node.type === 'function' && tinymathFunctions[node.name]; +} + +export function findMathNodes(root: TinymathAST | string): TinymathFunction[] { + function flattenMathNodes(node: TinymathAST | string): TinymathFunction[] { + if (!isObject(node) || node.type !== 'function' || !isMathNode(node)) { + return []; + } + return [node, ...node.args.flatMap(flattenMathNodes)].filter(Boolean); + } + return flattenMathNodes(root); +} + +// traverse a tree and find all string leaves +export function findVariables(node: TinymathAST | string): TinymathVariable[] { + if (typeof node === 'string') { + return [ + { + type: 'variable', + value: node, + text: node, + location: { min: 0, max: 0 }, + }, + ]; + } + if (node == null) { + return []; + } + if (typeof node === 'number' || node.type === 'namedArgument') { + return []; + } + if (node.type === 'variable') { + // leaf node + return [node]; + } + return node.args.flatMap(findVariables); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts new file mode 100644 index 0000000000000..5145c7959f1bb --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/validation.ts @@ -0,0 +1,687 @@ +/* + * 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 { isObject } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { parse, TinymathLocation } from '@kbn/tinymath'; +import type { TinymathAST, TinymathFunction, TinymathNamedArgument } from '@kbn/tinymath'; +import { esKuery, esQuery } from '../../../../../../../../src/plugins/data/public'; +import { + findMathNodes, + findVariables, + getOperationParams, + getValueOrName, + groupArgsByType, + isMathNode, + tinymathFunctions, +} from './util'; + +import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index'; +import type { IndexPattern, IndexPatternLayer } from '../../../types'; +import type { TinymathNodeTypes } from './types'; + +interface ValidationErrors { + missingField: { message: string; type: { variablesLength: number; variablesList: string } }; + missingOperation: { + message: string; + type: { operationLength: number; operationsList: string }; + }; + missingParameter: { + message: string; + type: { operation: string; params: string }; + }; + wrongTypeParameter: { + message: string; + type: { operation: string; params: string }; + }; + wrongFirstArgument: { + message: string; + type: { operation: string; type: string; argument: string | number }; + }; + cannotAcceptParameter: { message: string; type: { operation: string } }; + shouldNotHaveField: { message: string; type: { operation: string } }; + tooManyArguments: { message: string; type: { operation: string } }; + fieldWithNoOperation: { + message: string; + type: { field: string }; + }; + failedParsing: { message: string; type: { expression: string } }; + duplicateArgument: { + message: string; + type: { operation: string; params: string }; + }; + missingMathArgument: { + message: string; + type: { operation: string; count: number; params: string }; + }; +} +type ErrorTypes = keyof ValidationErrors; +type ErrorValues = ValidationErrors[K]['type']; + +export interface ErrorWrapper { + message: string; + locations: TinymathLocation[]; + severity?: 'error' | 'warning'; +} + +export function isParsingError(message: string) { + return message.includes('Failed to parse expression'); +} + +function findFunctionNodes(root: TinymathAST | string): TinymathFunction[] { + function flattenFunctionNodes(node: TinymathAST | string): TinymathFunction[] { + if (!isObject(node) || node.type !== 'function') { + return []; + } + return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(Boolean); + } + return flattenFunctionNodes(root); +} + +export function hasInvalidOperations( + node: TinymathAST | string, + operations: Record +): { names: string[]; locations: TinymathLocation[] } { + const nodes = findFunctionNodes(node).filter((v) => !isMathNode(v) && !operations[v.name]); + return { + // avoid duplicates + names: Array.from(new Set(nodes.map(({ name }) => name))), + locations: nodes.map(({ location }) => location), + }; +} + +export const getQueryValidationError = ( + query: string, + language: 'kql' | 'lucene', + indexPattern: IndexPattern +): string | undefined => { + try { + if (language === 'kql') { + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query), indexPattern); + } else { + esQuery.luceneStringToDsl(query); + } + return; + } catch (e) { + return e.message; + } +}; + +function getMessageFromId({ + messageId, + values: { ...values }, + locations, +}: { + messageId: K; + values: ErrorValues; + locations: TinymathLocation[]; +}): ErrorWrapper { + let message: string; + // Use a less strict type instead of doing a typecast on each message type + const out = (values as unknown) as Record; + switch (messageId) { + case 'wrongFirstArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationWrongFirstArgument', { + defaultMessage: + 'The first argument for {operation} should be a {type} name. Found {argument}', + values: { operation: out.operation, type: out.type, argument: out.argument }, + }); + break; + case 'shouldNotHaveField': + message = i18n.translate('xpack.lens.indexPattern.formulaFieldNotRequired', { + defaultMessage: 'The operation {operation} does not accept any field as argument', + values: { operation: out.operation }, + }); + break; + case 'cannotAcceptParameter': + message = i18n.translate('xpack.lens.indexPattern.formulaParameterNotRequired', { + defaultMessage: 'The operation {operation} does not accept any parameter', + values: { operation: out.operation }, + }); + break; + case 'missingParameter': + message = i18n.translate('xpack.lens.indexPattern.formulaExpressionNotHandled', { + defaultMessage: + 'The operation {operation} in the Formula is missing the following parameters: {params}', + values: { operation: out.operation, params: out.params }, + }); + break; + case 'wrongTypeParameter': + message = i18n.translate('xpack.lens.indexPattern.formulaExpressionWrongType', { + defaultMessage: + 'The parameters for the operation {operation} in the Formula are of the wrong type: {params}', + values: { operation: out.operation, params: out.params }, + }); + break; + case 'duplicateArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaOperationDuplicateParams', { + defaultMessage: + 'The parameters for the operation {operation} have been declared multiple times: {params}', + values: { operation: out.operation, params: out.params }, + }); + break; + case 'missingField': + message = i18n.translate('xpack.lens.indexPattern.formulaFieldNotFound', { + defaultMessage: + '{variablesLength, plural, one {Field} other {Fields}} {variablesList} not found', + values: { variablesLength: out.variablesLength, variablesList: out.variablesList }, + }); + break; + case 'missingOperation': + message = i18n.translate('xpack.lens.indexPattern.operationsNotFound', { + defaultMessage: + '{operationLength, plural, one {Operation} other {Operations}} {operationsList} not found', + values: { operationLength: out.operationLength, operationsList: out.operationsList }, + }); + break; + case 'fieldWithNoOperation': + message = i18n.translate('xpack.lens.indexPattern.fieldNoOperation', { + defaultMessage: 'The field {field} cannot be used without operation', + values: { field: out.field }, + }); + break; + case 'failedParsing': + message = i18n.translate('xpack.lens.indexPattern.formulaExpressionParseError', { + defaultMessage: 'The Formula {expression} cannot be parsed', + values: { expression: out.expression }, + }); + break; + case 'tooManyArguments': + message = i18n.translate('xpack.lens.indexPattern.formulaWithTooManyArguments', { + defaultMessage: 'The operation {operation} has too many arguments', + values: { operation: out.operation }, + }); + break; + case 'missingMathArgument': + message = i18n.translate('xpack.lens.indexPattern.formulaMathMissingArgument', { + defaultMessage: + 'The operation {operation} in the Formula is missing {count} arguments: {params}', + values: { operation: out.operation, count: out.count, params: out.params }, + }); + break; + // case 'mathRequiresFunction': + // message = i18n.translate('xpack.lens.indexPattern.formulaMathRequiresFunctionLabel', { + // defaultMessage; 'The function {name} requires an Elasticsearch function', + // values: { ...values }, + // }); + // break; + default: + message = 'no Error found'; + break; + } + + return { message, locations }; +} + +export function tryToParse( + formula: string +): { root: TinymathAST; error: null } | { root: null; error: ErrorWrapper } { + let root; + try { + root = parse(formula); + } catch (e) { + return { + root: null, + error: getMessageFromId({ + messageId: 'failedParsing', + values: { + expression: formula, + }, + locations: [], + }), + }; + } + return { root, error: null }; +} + +export function runASTValidation( + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern, + operations: Record +) { + return [ + ...checkMissingVariableOrFunctions(ast, layer, indexPattern, operations), + ...runFullASTValidation(ast, layer, indexPattern, operations), + ]; +} + +function checkVariableEdgeCases(ast: TinymathAST, missingVariables: Set) { + const invalidVariableErrors = []; + if (isObject(ast) && ast.type === 'variable' && !missingVariables.has(ast.value)) { + invalidVariableErrors.push( + getMessageFromId({ + messageId: 'fieldWithNoOperation', + values: { + field: ast.value, + }, + locations: [ast.location], + }) + ); + } + return invalidVariableErrors; +} + +function checkMissingVariableOrFunctions( + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern, + operations: Record +): ErrorWrapper[] { + const missingErrors: ErrorWrapper[] = []; + const missingOperations = hasInvalidOperations(ast, operations); + + if (missingOperations.names.length) { + missingErrors.push( + getMessageFromId({ + messageId: 'missingOperation', + values: { + operationLength: missingOperations.names.length, + operationsList: missingOperations.names.join(', '), + }, + locations: missingOperations.locations, + }) + ); + } + const missingVariables = findVariables(ast).filter( + // filter empty string as well? + ({ value }) => !indexPattern.getFieldByName(value) && !layer.columns[value] + ); + + // need to check the arguments here: check only strings for now + if (missingVariables.length) { + missingErrors.push( + getMessageFromId({ + messageId: 'missingField', + values: { + variablesLength: missingVariables.length, + variablesList: missingVariables.map(({ value }) => value).join(', '), + }, + locations: missingVariables.map(({ location }) => location), + }) + ); + } + const invalidVariableErrors = checkVariableEdgeCases( + ast, + new Set(missingVariables.map(({ value }) => value)) + ); + return [...missingErrors, ...invalidVariableErrors]; +} + +function getQueryValidationErrors( + namedArguments: TinymathNamedArgument[] | undefined, + indexPattern: IndexPattern +): ErrorWrapper[] { + const errors: ErrorWrapper[] = []; + (namedArguments ?? []).forEach((arg) => { + if (arg.name === 'kql' || arg.name === 'lucene') { + const message = getQueryValidationError(arg.value, arg.name, indexPattern); + if (message) { + errors.push({ + message, + locations: [arg.location], + }); + } + } + }); + return errors; +} + +function validateNameArguments( + node: TinymathFunction, + nodeOperation: + | OperationDefinition + | OperationDefinition, + namedArguments: TinymathNamedArgument[] | undefined, + indexPattern: IndexPattern +) { + const errors = []; + const missingParams = getMissingParams(nodeOperation, namedArguments); + if (missingParams.length) { + errors.push( + getMessageFromId({ + messageId: 'missingParameter', + values: { + operation: node.name, + params: missingParams.map(({ name }) => name).join(', '), + }, + locations: [node.location], + }) + ); + } + const wrongTypeParams = getWrongTypeParams(nodeOperation, namedArguments); + if (wrongTypeParams.length) { + errors.push( + getMessageFromId({ + messageId: 'wrongTypeParameter', + values: { + operation: node.name, + params: wrongTypeParams.map(({ name }) => name).join(', '), + }, + locations: [node.location], + }) + ); + } + const duplicateParams = getDuplicateParams(namedArguments); + if (duplicateParams.length) { + errors.push( + getMessageFromId({ + messageId: 'duplicateArgument', + values: { + operation: node.name, + params: duplicateParams.join(', '), + }, + locations: [node.location], + }) + ); + } + const queryValidationErrors = getQueryValidationErrors(namedArguments, indexPattern); + if (queryValidationErrors.length) { + errors.push(...queryValidationErrors); + } + return errors; +} + +function runFullASTValidation( + ast: TinymathAST, + layer: IndexPatternLayer, + indexPattern: IndexPattern, + operations: Record +): ErrorWrapper[] { + const missingVariables = findVariables(ast).filter( + // filter empty string as well? + ({ value }) => !indexPattern.getFieldByName(value) && !layer.columns[value] + ); + const missingVariablesSet = new Set(missingVariables.map(({ value }) => value)); + + function validateNode(node: TinymathAST): ErrorWrapper[] { + if (!isObject(node) || node.type !== 'function') { + return []; + } + const nodeOperation = operations[node.name]; + const errors: ErrorWrapper[] = []; + const { namedArguments, functions, variables } = groupArgsByType(node.args); + const [firstArg] = node?.args || []; + + if (!nodeOperation) { + errors.push(...validateMathNodes(node, missingVariablesSet)); + // carry on with the validation for all the functions within the math operation + if (functions?.length) { + return errors.concat(functions.flatMap((fn) => validateNode(fn))); + } + } else { + if (nodeOperation.input === 'field') { + if (shouldHaveFieldArgument(node)) { + if (!isFirstArgumentValidType(firstArg, 'variable')) { + if (isMathNode(firstArg)) { + errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'field', + argument: `math operation`, + }, + locations: [node.location], + }) + ); + } else { + errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'field', + argument: getValueOrName(firstArg), + }, + locations: [node.location], + }) + ); + } + } + } else { + // Named arguments only + if (functions?.length || variables?.length) { + errors.push( + getMessageFromId({ + messageId: 'shouldNotHaveField', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } + } + if (!canHaveParams(nodeOperation) && namedArguments.length) { + errors.push( + getMessageFromId({ + messageId: 'cannotAcceptParameter', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } else { + const argumentsErrors = validateNameArguments( + node, + nodeOperation, + namedArguments, + indexPattern + ); + if (argumentsErrors.length) { + errors.push(...argumentsErrors); + } + } + return errors; + } + if (nodeOperation.input === 'fullReference') { + // What about fn(7 + 1)? We may want to allow that + // In general this should be handled down the Esaggs route rather than here + if ( + !isFirstArgumentValidType(firstArg, 'function') || + (isMathNode(firstArg) && validateMathNodes(firstArg, missingVariablesSet).length) + ) { + errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'operation', + argument: getValueOrName(firstArg), + }, + locations: [node.location], + }) + ); + } + if (!canHaveParams(nodeOperation) && namedArguments.length) { + errors.push( + getMessageFromId({ + messageId: 'cannotAcceptParameter', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } else { + const argumentsErrors = validateNameArguments( + node, + nodeOperation, + namedArguments, + indexPattern + ); + if (argumentsErrors.length) { + errors.push(...argumentsErrors); + } + } + } + return errors.concat(validateNode(functions[0])); + } + return errors; + } + + return validateNode(ast); +} + +export function canHaveParams( + operation: + | OperationDefinition + | OperationDefinition +) { + return Boolean((operation.operationParams || []).length) || operation.filterable; +} + +export function getInvalidParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + return validateParams(operation, params).filter( + ({ isMissing, isCorrectType, isRequired }) => (isMissing && isRequired) || !isCorrectType + ); +} + +export function getMissingParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + return validateParams(operation, params).filter( + ({ isMissing, isRequired }) => isMissing && isRequired + ); +} + +export function getWrongTypeParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + return validateParams(operation, params).filter( + ({ isCorrectType, isMissing }) => !isCorrectType && !isMissing + ); +} + +function getDuplicateParams(params: TinymathNamedArgument[] = []) { + const uniqueArgs = Object.create(null); + for (const { name } of params) { + const counter = uniqueArgs[name] || 0; + uniqueArgs[name] = counter + 1; + } + const uniqueNames = Object.keys(uniqueArgs); + if (params.length > uniqueNames.length) { + return uniqueNames.filter((name) => uniqueArgs[name] > 1); + } + return []; +} + +export function validateParams( + operation: + | OperationDefinition + | OperationDefinition, + params: TinymathNamedArgument[] = [] +) { + const paramsObj = getOperationParams(operation, params); + const formalArgs = [...(operation.operationParams ?? [])]; + if (operation.filterable) { + formalArgs.push( + { name: 'kql', type: 'string', required: false }, + { name: 'lucene', type: 'string', required: false } + ); + } + return formalArgs.map(({ name, type, required }) => ({ + name, + isMissing: !(name in paramsObj), + isCorrectType: typeof paramsObj[name] === type, + isRequired: required, + })); +} + +export function shouldHaveFieldArgument(node: TinymathFunction) { + return !['count'].includes(node.name); +} + +export function isFirstArgumentValidType(arg: TinymathAST, type: TinymathNodeTypes['type']) { + return isObject(arg) && arg.type === type; +} + +export function validateMathNodes(root: TinymathAST, missingVariableSet: Set) { + const mathNodes = findMathNodes(root); + const errors: ErrorWrapper[] = []; + mathNodes.forEach((node: TinymathFunction) => { + const { positionalArguments } = tinymathFunctions[node.name]; + if (!node.args.length) { + // we can stop here + return errors.push( + getMessageFromId({ + messageId: 'wrongFirstArgument', + values: { + operation: node.name, + type: 'operation', + argument: `()`, + }, + locations: [node.location], + }) + ); + } + + if (node.args.length > positionalArguments.length) { + errors.push( + getMessageFromId({ + messageId: 'tooManyArguments', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } + + // no need to iterate all the arguments, one field is anough to trigger the error + const hasFieldAsArgument = positionalArguments.some((requirements, index) => { + const arg = node.args[index]; + if (arg != null && typeof arg !== 'number') { + return arg.type === 'variable' && !missingVariableSet.has(arg.value); + } + }); + if (hasFieldAsArgument) { + errors.push( + getMessageFromId({ + messageId: 'shouldNotHaveField', + values: { + operation: node.name, + }, + locations: [node.location], + }) + ); + } + + const mandatoryArguments = positionalArguments.filter(({ optional }) => !optional); + // if there is only 1 mandatory arg, this is already handled by the wrongFirstArgument check + if (mandatoryArguments.length > 1 && node.args.length < mandatoryArguments.length) { + const missingArgs = positionalArguments.filter( + ({ name, optional }, i) => !optional && node.args[i] == null + ); + errors.push( + getMessageFromId({ + messageId: 'missingMathArgument', + values: { + operation: node.name, + count: mandatoryArguments.length - node.args.length, + params: missingArgs.map(({ name }) => name).join(', '), + }, + locations: [node.location], + }) + ); + } + }); + return errors; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts index bff997c8a81e8..bf24e31ad4f59 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.test.ts @@ -37,7 +37,7 @@ describe('helpers', () => { createMockedIndexPattern() ); expect(messages).toHaveLength(1); - expect(messages![0]).toEqual('Field timestamp was not found'); + expect(messages![0]).toEqual('Field timestamp is of the wrong type'); }); it('returns no message if all fields are matching', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index b7e92a0b54952..f719ac4250912 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -54,14 +54,37 @@ export function getInvalidFieldMessage( operationDefinition.getPossibleOperationForField(field) !== undefined ) ); - return isInvalid - ? [ - i18n.translate('xpack.lens.indexPattern.fieldNotFound', { - defaultMessage: 'Field {invalidField} was not found', - values: { invalidField: sourceField }, + + const isWrongType = Boolean( + sourceField && + operationDefinition && + field && + !operationDefinition.isTransferable( + column as IndexPatternColumn, + indexPattern, + operationDefinitionMap + ) + ); + if (isInvalid) { + if (isWrongType) { + return [ + i18n.translate('xpack.lens.indexPattern.fieldWrongType', { + defaultMessage: 'Field {invalidField} is of the wrong type', + values: { + invalidField: sourceField, + }, }), - ] - : undefined; + ]; + } + return [ + i18n.translate('xpack.lens.indexPattern.fieldNotFound', { + defaultMessage: 'Field {invalidField} was not found', + values: { invalidField: sourceField }, + }), + ]; + } + + return undefined; } export function getSafeName(name: string, indexPattern: IndexPattern): string { @@ -100,3 +123,18 @@ export function getFormatFromPreviousColumn(previousColumn: IndexPatternColumn | ? { format: previousColumn.params.format } : undefined; } + +export function getFilter( + previousColumn: IndexPatternColumn | undefined, + columnParams: { kql?: string | undefined; lucene?: string | undefined } | undefined +) { + let filter = previousColumn?.filter; + if (columnParams) { + if ('kql' in columnParams) { + filter = { query: columnParams.kql ?? '', language: 'kuery' }; + } else if ('lucene' in columnParams) { + filter = { query: columnParams.lucene ?? '', language: 'lucene' }; + } + } + return filter; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 37bd64251ed81..a7402bc13c0a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -35,6 +35,12 @@ import { MovingAverageIndexPatternColumn, } from './calculations'; import { countOperation, CountIndexPatternColumn } from './count'; +import { + mathOperation, + MathIndexPatternColumn, + formulaOperation, + FormulaIndexPatternColumn, +} from './formula'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; import { OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; @@ -66,7 +72,9 @@ export type IndexPatternColumn = | CumulativeSumIndexPatternColumn | CounterRateIndexPatternColumn | DerivativeIndexPatternColumn - | MovingAverageIndexPatternColumn; + | MovingAverageIndexPatternColumn + | MathIndexPatternColumn + | FormulaIndexPatternColumn; export type FieldBasedIndexPatternColumn = Extract; @@ -115,6 +123,8 @@ const internalOperationDefinitions = [ counterRateOperation, derivativeOperation, movingAverageOperation, + mathOperation, + formulaOperation, ]; export { termsOperation } from './terms'; @@ -131,6 +141,7 @@ export { derivativeOperation, movingAverageOperation, } from './calculations'; +export { formulaOperation } from './formula/formula'; /** * Properties passed to the operation-specific part of the popover editor @@ -147,6 +158,7 @@ export interface ParamEditorProps { http: HttpSetup; dateRange: DateRange; data: DataPublicPluginStart; + operationDefinitionMap: Record; } export interface HelpProps { @@ -198,7 +210,11 @@ interface BaseOperationDefinitionProps { * If this function returns false, the column is removed when switching index pattern * for a layer */ - isTransferable: (column: C, newIndexPattern: IndexPattern) => boolean; + isTransferable: ( + column: C, + newIndexPattern: IndexPattern, + operationDefinitionMap: Record + ) => boolean; /** * Transfering a column to another index pattern. This can be used to * adjust operation specific settings such as reacting to aggregation restrictions @@ -220,7 +236,8 @@ interface BaseOperationDefinitionProps { getErrorMessage?: ( layer: IndexPatternLayer, columnId: string, - indexPattern: IndexPattern + indexPattern: IndexPattern, + operationDefinitionMap?: Record ) => string[] | undefined; /* @@ -230,9 +247,18 @@ interface BaseOperationDefinitionProps { * If set to optional, time scaling won't be enabled by default and can be removed. */ timeScalingMode?: TimeScalingMode; + /** + * Filterable operations can have a KQL or Lucene query added at the dimension level. + * This flag is used by the formula to assign the kql= and lucene= named arguments and set up + * autocomplete. + */ filterable?: boolean; getHelpMessage?: (props: HelpProps) => React.ReactNode; + /* + * Operations can be used as middleware for other operations, hence not shown in the panel UI + */ + hidden?: boolean; } interface BaseBuildColumnArgs { @@ -240,15 +266,28 @@ interface BaseBuildColumnArgs { indexPattern: IndexPattern; } +interface OperationParam { + name: string; + type: string; + required?: boolean; +} + interface FieldlessOperationDefinition { input: 'none'; + + /** + * The specification of the arguments used by the operations used for both validation, + * and use from external managed operations + */ + operationParams?: OperationParam[]; /** * Builds the column object for the given parameters. Should include default p */ buildColumn: ( arg: BaseBuildColumnArgs & { previousColumn?: IndexPatternColumn; - } + }, + columnParams?: (IndexPatternColumn & C)['params'] ) => C; /** * Returns the meta data of the operation if applied. Undefined @@ -270,6 +309,12 @@ interface FieldlessOperationDefinition { interface FieldBasedOperationDefinition { input: 'field'; + + /** + * The specification of the arguments used by the operations used for both validation, + * and use from external managed operations + */ + operationParams?: OperationParam[]; /** * Returns the meta data of the operation if applied to the given field. Undefined * if the field is not applicable to the operation. @@ -282,7 +327,8 @@ interface FieldBasedOperationDefinition { arg: BaseBuildColumnArgs & { field: IndexPatternField; previousColumn?: IndexPatternColumn; - } + }, + columnParams?: (IndexPatternColumn & C)['params'] & { kql?: string; lucene?: string } ) => C; /** * This method will be called if the user changes the field of an operation. @@ -320,7 +366,8 @@ interface FieldBasedOperationDefinition { getErrorMessage: ( layer: IndexPatternLayer, columnId: string, - indexPattern: IndexPattern + indexPattern: IndexPattern, + operationDefinitionMap?: Record ) => string[] | undefined; } @@ -333,6 +380,7 @@ export interface RequiredReference { // operation types. The main use case is Cumulative Sum, where we need to only take the // sum of Count or sum of Sum. specificOperations?: OperationType[]; + multi?: boolean; } // Full reference uses one or more reference operations which are visible to the user @@ -345,12 +393,19 @@ interface FullReferenceOperationDefinition { */ requiredReferences: RequiredReference[]; + /** + * The specification of the arguments used by the operations used for both validation, + * and use from external managed operations + */ + operationParams?: OperationParam[]; + /** * The type of UI that is shown in the editor for this function: * - full: List of sub-functions and fields * - field: List of fields, selects first operation per field + * - hidden: Do not allow to use operation directly */ - selectionStyle: 'full' | 'field'; + selectionStyle: 'full' | 'field' | 'hidden'; /** * Builds the column object for the given parameters. Should include default p @@ -359,6 +414,10 @@ interface FullReferenceOperationDefinition { arg: BaseBuildColumnArgs & { referenceIds: string[]; previousColumn?: IndexPatternColumn; + }, + columnParams?: (ReferenceBasedIndexPatternColumn & C)['params'] & { + kql?: string; + lucene?: string; } ) => ReferenceBasedIndexPatternColumn & C; /** @@ -376,10 +435,49 @@ interface FullReferenceOperationDefinition { ) => ExpressionAstFunction[]; } +interface ManagedReferenceOperationDefinition { + input: 'managedReference'; + /** + * Builds the column object for the given parameters. Should include default p + */ + buildColumn: ( + arg: BaseBuildColumnArgs & { + previousColumn?: IndexPatternColumn | ReferenceBasedIndexPatternColumn; + }, + columnParams?: (ReferenceBasedIndexPatternColumn & C)['params'], + operationDefinitionMap?: Record + ) => ReferenceBasedIndexPatternColumn & C; + /** + * Returns the meta data of the operation if applied. Undefined + * if the operation can't be added with these fields. + */ + getPossibleOperation: () => OperationMetadata | undefined; + /** + * A chain of expression functions which will transform the table + */ + toExpression: ( + layer: IndexPatternLayer, + columnId: string, + indexPattern: IndexPattern + ) => ExpressionAstFunction[]; + /** + * Managed references control the IDs of their inner columns, so we need to be able to copy from the + * root level + */ + createCopy: ( + layer: IndexPatternLayer, + sourceColumnId: string, + targetColumnId: string, + indexPattern: IndexPattern, + operationDefinitionMap: Record + ) => IndexPatternLayer; +} + interface OperationDefinitionMap { field: FieldBasedOperationDefinition; none: FieldlessOperationDefinition; fullReference: FullReferenceOperationDefinition; + managedReference: ManagedReferenceOperationDefinition; } /** @@ -405,7 +503,8 @@ export type OperationType = typeof internalOperationDefinitions[number]['type']; export type GenericOperationDefinition = | OperationDefinition | OperationDefinition - | OperationDefinition; + | OperationDefinition + | OperationDefinition; /** * List of all available operation definitions diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index b2244e0cc769f..280cfe9471c9d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -29,6 +29,7 @@ const defaultProps = { ...createMockedIndexPattern(), hasRestrictions: false, } as IndexPattern, + operationDefinitionMap: {}, }; describe('last_value', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx index 4f5c897fb5378..4632d262c441d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx @@ -15,7 +15,12 @@ import { FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternField, IndexPattern } from '../../types'; import { updateColumnParam } from '../layer_helpers'; import { DataType } from '../../../types'; -import { getFormatFromPreviousColumn, getInvalidFieldMessage, getSafeName } from './helpers'; +import { + getFormatFromPreviousColumn, + getInvalidFieldMessage, + getSafeName, + getFilter, +} from './helpers'; function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.lastValueOf', { @@ -141,7 +146,7 @@ export const lastValueOperation: OperationDefinition f.type === 'date')?.name; @@ -161,7 +166,7 @@ export const lastValueOperation: OperationDefinition>({ : (layer.columns[thisColumnId] as T), getDefaultLabel: (column, indexPattern, columns) => labelLookup(getSafeName(column.sourceField, indexPattern), column), - buildColumn: ({ field, previousColumn }) => - ({ + buildColumn: ({ field, previousColumn }, columnParams) => { + return { label: labelLookup(field.displayName, previousColumn), dataType: 'number', operationType: type, @@ -98,9 +103,10 @@ function buildMetricOperation>({ isBucketed: false, scale: 'ratio', timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, - filter: previousColumn?.filter, + filter: getFilter(previousColumn, columnParams), params: getFormatFromPreviousColumn(previousColumn), - } as T), + } as T; + }, onFieldChange: (oldColumn, field) => { return { ...oldColumn, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index c14ff9f86f602..59da0f6f7bcde 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -31,6 +31,7 @@ const defaultProps = { ...createMockedIndexPattern(), hasRestrictions: false, } as IndexPattern, + operationDefinitionMap: {}, }; describe('percentile', () => { @@ -178,6 +179,41 @@ describe('percentile', () => { expect(percentileColumn.params.percentile).toEqual(95); expect(percentileColumn.label).toEqual('95th percentile of test'); }); + + it('should create a percentile from formula', () => { + const indexPattern = createMockedIndexPattern(); + const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!; + bytesField.displayName = 'test'; + const percentileColumn = percentileOperation.buildColumn( + { + indexPattern, + field: bytesField, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }, + { percentile: 75 } + ); + expect(percentileColumn.dataType).toEqual('number'); + expect(percentileColumn.params.percentile).toEqual(75); + expect(percentileColumn.label).toEqual('75th percentile of test'); + }); + + it('should create a percentile from formula with filter', () => { + const indexPattern = createMockedIndexPattern(); + const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!; + bytesField.displayName = 'test'; + const percentileColumn = percentileOperation.buildColumn( + { + indexPattern, + field: bytesField, + layer: { columns: {}, columnOrder: [], indexPatternId: '' }, + }, + { percentile: 75, kql: 'bytes > 100' } + ); + expect(percentileColumn.dataType).toEqual('number'); + expect(percentileColumn.params.percentile).toEqual(75); + expect(percentileColumn.filter).toEqual({ language: 'kuery', query: 'bytes > 100' }); + expect(percentileColumn.label).toEqual('75th percentile of test'); + }); }); describe('isTransferable', () => { @@ -202,7 +238,8 @@ describe('percentile', () => { percentile: 95, }, }, - indexPattern + indexPattern, + {} ) ).toBeTruthy(); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index dd0f3b978da5f..705a1f7172fff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -17,6 +17,7 @@ import { getSafeName, isValidNumber, useDebounceWithOptions, + getFilter, } from './helpers'; import { FieldBasedIndexPatternColumn } from './column_types'; @@ -51,6 +52,7 @@ export const percentileOperation: OperationDefinition { if (supportedFieldTypes.includes(fieldType) && aggregatable && !aggregationRestrictions) { @@ -73,13 +75,14 @@ export const percentileOperation: OperationDefinition ofName(getSafeName(column.sourceField, indexPattern), column.params.percentile), - buildColumn: ({ field, previousColumn, indexPattern }) => { + buildColumn: ({ field, previousColumn, indexPattern }, columnParams) => { const existingPercentileParam = previousColumn?.operationType === 'percentile' && previousColumn.params && 'percentile' in previousColumn.params && previousColumn.params.percentile; - const newPercentileParam = existingPercentileParam || DEFAULT_PERCENTILE_VALUE; + const newPercentileParam = + columnParams?.percentile ?? (existingPercentileParam || DEFAULT_PERCENTILE_VALUE); return { label: ofName(getSafeName(field.name, indexPattern), newPercentileParam), dataType: 'number', @@ -87,7 +90,7 @@ export const percentileOperation: OperationDefinition { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 2e7307f6a2ec4..b094d3f0ff5cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -28,6 +28,7 @@ const defaultProps = { data: dataPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), + operationDefinitionMap: {}, }; describe('terms', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index c506e800d6d01..4dd56d2de1144 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -7,6 +7,7 @@ import type { OperationMetadata } from '../../types'; import { + copyColumn, insertNewColumn, replaceColumn, updateColumnParam, @@ -23,7 +24,7 @@ import type { IndexPattern, IndexPatternLayer } from '../types'; import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; -import { createMockedReferenceOperation } from './mocks'; +import { createMockedFullReference } from './mocks'; jest.mock('../operations'); jest.mock('../../id_generator'); @@ -89,13 +90,126 @@ describe('state_helpers', () => { (generateId as jest.Mock).mockImplementation(() => `id${++count}`); // @ts-expect-error we are inserting an invalid type - operationDefinitionMap.testReference = createMockedReferenceOperation(); + operationDefinitionMap.testReference = createMockedFullReference(); }); afterEach(() => { delete operationDefinitionMap.testReference; }); + describe('copyColumn', () => { + it('should recursively modify a formula and update the math ast', () => { + const source = { + dataType: 'number' as const, + isBucketed: false, + label: 'Formula', + operationType: 'formula' as const, + params: { + formula: 'moving_average(sum(bytes), window=5)', + isFormulaBroken: false, + }, + references: ['formulaX3'], + }; + const math = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'math', + operationType: 'math' as const, + params: { tinymathAst: 'formulaX2' }, + references: ['formulaX2'], + }; + const sum = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX0', + operationType: 'sum' as const, + scale: 'ratio' as const, + sourceField: 'bytes', + }; + const movingAvg = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX2', + operationType: 'moving_average' as const, + params: { window: 5 }, + references: ['formulaX1'], + }; + expect( + copyColumn({ + layer: { + indexPatternId: '', + columnOrder: [], + columns: { + source, + formulaX0: sum, + formulaX1: math, + formulaX2: movingAvg, + formulaX3: { + ...math, + label: 'formulaX3', + references: ['formulaX2'], + params: { tinymathAst: 'formulaX2' }, + }, + }, + }, + targetId: 'copy', + sourceColumn: source, + shouldDeleteSource: false, + indexPattern, + sourceColumnId: 'source', + }) + ).toEqual({ + indexPatternId: '', + columnOrder: [ + 'source', + 'formulaX0', + 'formulaX1', + 'formulaX2', + 'formulaX3', + 'copyX0', + 'copyX1', + 'copyX2', + 'copyX3', + 'copy', + ], + columns: { + source, + formulaX0: sum, + formulaX1: math, + formulaX2: movingAvg, + formulaX3: { + ...math, + label: 'formulaX3', + references: ['formulaX2'], + params: { tinymathAst: 'formulaX2' }, + }, + copy: expect.objectContaining({ ...source, references: ['copyX3'] }), + copyX0: expect.objectContaining({ ...sum, label: 'copyX0' }), + copyX1: expect.objectContaining({ + ...math, + label: 'copyX1', + references: ['copyX0'], + params: { tinymathAst: 'copyX0' }, + }), + copyX2: expect.objectContaining({ + ...movingAvg, + label: 'copyX2', + references: ['copyX1'], + }), + copyX3: expect.objectContaining({ + ...math, + label: 'copyX3', + references: ['copyX2'], + params: { tinymathAst: 'copyX2' }, + }), + }, + }); + }); + }); + describe('insertNewColumn', () => { it('should throw for invalid operations', () => { expect(() => { @@ -195,7 +309,7 @@ describe('state_helpers', () => { ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); - it('should insert a metric after buckets, but before references', () => { + it('should insert a metric after references', () => { const layer: IndexPatternLayer = { indexPatternId: '1', columnOrder: ['col1'], @@ -231,7 +345,7 @@ describe('state_helpers', () => { field: documentField, visualizationGroups: [], }) - ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); + ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col3', 'col2'] })); }); it('should insert new buckets at the end of previous buckets', () => { @@ -1074,7 +1188,7 @@ describe('state_helpers', () => { referenceIds: ['id1'], }) ); - expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columnOrder).toEqual(['col1', 'id1']); expect(result.columns).toEqual( expect.objectContaining({ id1: expectedColumn, @@ -1196,7 +1310,7 @@ describe('state_helpers', () => { op: 'testReference', }); - expect(result.columnOrder).toEqual(['id1', 'col1']); + expect(result.columnOrder).toEqual(['col1', 'id1']); expect(result.columns).toEqual({ id1: expect.objectContaining({ operationType: 'average', @@ -1426,6 +1540,83 @@ describe('state_helpers', () => { ); }); + it('should transition from managedReference to fullReference by deleting the managedReference', () => { + const math = { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'math', + operationType: 'math' as const, + }; + const layer: IndexPatternLayer = { + indexPatternId: '', + columnOrder: [], + columns: { + source: { + dataType: 'number' as const, + isBucketed: false, + label: 'Formula', + operationType: 'formula' as const, + params: { + formula: 'moving_average(sum(bytes), window=5)', + isFormulaBroken: false, + }, + references: ['formulaX3'], + }, + formulaX0: { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX0', + operationType: 'sum' as const, + scale: 'ratio' as const, + sourceField: 'bytes', + }, + formulaX1: { + ...math, + label: 'formulaX1', + references: ['formulaX0'], + params: { tinymathAst: 'formulaX0' }, + }, + formulaX2: { + customLabel: true, + dataType: 'number' as const, + isBucketed: false, + label: 'formulaX2', + operationType: 'moving_average' as const, + params: { window: 5 }, + references: ['formulaX1'], + }, + formulaX3: { + ...math, + label: 'formulaX3', + references: ['formulaX2'], + params: { tinymathAst: 'formulaX2' }, + }, + }, + }; + + expect( + replaceColumn({ + layer, + indexPattern, + columnId: 'source', + // @ts-expect-error not statically available + op: 'secondTest', + }) + ).toEqual( + expect.objectContaining({ + columnOrder: ['source'], + columns: { + source: expect.objectContaining({ + operationType: 'secondTest', + references: ['id1'], + }), + }, + }) + ); + }); + it('should transition by using the field from the previous reference if nothing else works (case new5)', () => { const layer: IndexPatternLayer = { indexPatternId: '1', @@ -1459,7 +1650,7 @@ describe('state_helpers', () => { }) ).toEqual( expect.objectContaining({ - columnOrder: ['id1', 'output'], + columnOrder: ['output', 'id1'], columns: { id1: expect.objectContaining({ sourceField: 'timestamp', @@ -2051,58 +2242,78 @@ describe('state_helpers', () => { ).toEqual(['col1', 'col3', 'col2']); }); - it('should correctly sort references to other references', () => { + it('does not topologically sort formulas, but keeps the relative order', () => { expect( getColumnOrder({ - columnOrder: [], indexPatternId: '', + columnOrder: [], columns: { - bucket: { - label: 'Top values of category', - dataType: 'string', + count: { + label: 'count', + dataType: 'number', + operationType: 'count', + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + customLabel: true, + }, + date: { + label: 'timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'timestamp', isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'category', + scale: 'interval', params: { - size: 5, - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'asc', + interval: 'auto', }, }, - metric: { - label: 'Average of bytes', + formula: { + label: 'Formula', dataType: 'number', + operationType: 'formula', isBucketed: false, - - // Private - operationType: 'average', - sourceField: 'bytes', + scale: 'ratio', + params: { + formula: 'count() + count()', + isFormulaBroken: false, + }, + references: ['math'], }, - ref2: { - label: 'Ref2', + countX0: { + label: 'countX0', dataType: 'number', + operationType: 'count', isBucketed: false, - - // @ts-expect-error only for testing - operationType: 'testReference', - references: ['ref1'], + scale: 'ratio', + sourceField: 'Records', + customLabel: true, }, - ref1: { - label: 'Ref', + math: { + label: 'math', dataType: 'number', + operationType: 'math', isBucketed: false, - - // @ts-expect-error only for testing - operationType: 'testReference', - references: ['bucket'], + scale: 'ratio', + params: { + tinymathAst: { + type: 'function', + name: 'add', + // @ts-expect-error String args are not valid tinymath, but signals something unique to Lens + args: ['countX0', 'count'], + location: { + min: 0, + max: 17, + }, + text: 'count() + count()', + }, + }, + references: ['countX0', 'count'], + customLabel: true, }, }, }) - ).toEqual(['bucket', 'metric', 'ref1', 'ref2']); + ).toEqual(['date', 'count', 'formula', 'countX0', 'math']); }); }); @@ -2459,7 +2670,8 @@ describe('state_helpers', () => { }, }, 'col1', - indexPattern + indexPattern, + operationDefinitionMap ); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index beebb72fff676..bc4a61eda3969 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -6,6 +6,7 @@ */ import _, { partition } from 'lodash'; +import { getSortScoreByPriority } from './operations'; import type { OperationMetadata, VisualizationDimensionGroupConfig } from '../../types'; import { operationDefinitionMap, @@ -15,9 +16,9 @@ import { RequiredReference, } from './definitions'; import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types'; -import { getSortScoreByPriority } from './operations'; import { generateId } from '../../id_generator'; import { ReferenceBasedIndexPatternColumn } from './definitions/column_types'; +import { FormulaIndexPatternColumn, regenerateLayerFromAst } from './definitions/formula'; interface ColumnChange { op: OperationType; @@ -32,7 +33,7 @@ interface ColumnChange { interface ColumnCopy { layer: IndexPatternLayer; - columnId: string; + targetId: string; sourceColumn: IndexPatternColumn; sourceColumnId: string; indexPattern: IndexPattern; @@ -41,16 +42,19 @@ interface ColumnCopy { export function copyColumn({ layer, - columnId, + targetId, sourceColumn, shouldDeleteSource, indexPattern, sourceColumnId, }: ColumnCopy): IndexPatternLayer { - let modifiedLayer = { - ...layer, - columns: copyReferencesRecursively(layer.columns, sourceColumn, columnId), - }; + let modifiedLayer = copyReferencesRecursively( + layer, + sourceColumn, + sourceColumnId, + targetId, + indexPattern + ); if (shouldDeleteSource) { modifiedLayer = deleteColumn({ @@ -64,16 +68,25 @@ export function copyColumn({ } function copyReferencesRecursively( - columns: Record, + layer: IndexPatternLayer, sourceColumn: IndexPatternColumn, - columnId: string -) { + sourceId: string, + targetId: string, + indexPattern: IndexPattern +): IndexPatternLayer { + let columns = { ...layer.columns }; if ('references' in sourceColumn) { - if (columns[columnId]) { - return columns; + if (columns[targetId]) { + return layer; + } + + const def = operationDefinitionMap[sourceColumn.operationType]; + if ('createCopy' in def) { + // Allow managed references to recursively insert new columns + return def.createCopy(layer, sourceId, targetId, indexPattern, operationDefinitionMap); } + sourceColumn?.references.forEach((ref, index) => { - // TODO: Add an option to assign IDs without generating the new one const newId = generateId(); const refColumn = { ...columns[ref] }; @@ -82,10 +95,10 @@ function copyReferencesRecursively( // and visible columns shouldn't be copied const refColumnWithInnerRefs = 'references' in refColumn - ? copyReferencesRecursively(columns, refColumn, newId) // if a column has references, copy them too + ? copyReferencesRecursively(layer, refColumn, sourceId, newId, indexPattern).columns // if a column has references, copy them too : { [newId]: refColumn }; - const newColumn = columns[columnId]; + const newColumn = columns[targetId]; let references = [newId]; if (newColumn && 'references' in newColumn) { references = newColumn.references; @@ -95,7 +108,7 @@ function copyReferencesRecursively( columns = { ...columns, ...refColumnWithInnerRefs, - [columnId]: { + [targetId]: { ...sourceColumn, references, }, @@ -104,10 +117,11 @@ function copyReferencesRecursively( } else { columns = { ...columns, - [columnId]: sourceColumn, + [targetId]: sourceColumn, }; } - return columns; + + return { ...layer, columns, columnOrder: getColumnOrder({ ...layer, columns }) }; } export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { @@ -141,12 +155,12 @@ export function insertNewColumn({ const baseOptions = { indexPattern, previousColumn: layer.columns[columnId] }; - if (operationDefinition.input === 'none') { + if (operationDefinition.input === 'none' || operationDefinition.input === 'managedReference') { if (field) { throw new Error(`Can't create operation ${op} with the provided field ${field.name}`); } const possibleOperation = operationDefinition.getPossibleOperation(); - const isBucketed = Boolean(possibleOperation.isBucketed); + const isBucketed = Boolean(possibleOperation?.isBucketed); const addOperationFn = isBucketed ? addBucket : addMetric; return updateDefaultLabels( addOperationFn( @@ -333,6 +347,19 @@ export function replaceColumn({ tempLayer = resetIncomplete(tempLayer, columnId); + if (previousDefinition.input === 'managedReference') { + // Every transition away from a managedReference resets it, we don't have a way to keep the state + tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); + return insertNewColumn({ + layer: tempLayer, + columnId, + indexPattern, + op, + field, + visualizationGroups, + }); + } + if (operationDefinition.input === 'fullReference') { return applyReferenceTransition({ layer: tempLayer, @@ -395,6 +422,54 @@ export function replaceColumn({ } } + // TODO: Refactor all this to be more generic and know less about Formula + // if managed it has to look at the full picture to have a seamless transition + if (operationDefinition.input === 'managedReference') { + const newColumn = copyCustomLabel( + operationDefinition.buildColumn( + { ...baseOptions, layer: tempLayer }, + previousColumn.params, + operationDefinitionMap + ), + previousColumn + ) as FormulaIndexPatternColumn; + + // now remove the previous references + if (previousDefinition.input === 'fullReference') { + (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { + tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + }); + } + + const basicLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; + // rebuild the references again for the specific AST generated + let newLayer; + + try { + newLayer = newColumn.params.formula + ? regenerateLayerFromAst( + newColumn.params.formula, + basicLayer, + columnId, + newColumn, + indexPattern, + operationDefinitionMap + ).newLayer + : basicLayer; + } catch (e) { + newLayer = basicLayer; + } + + return updateDefaultLabels( + { + ...tempLayer, + columnOrder: getColumnOrder(newLayer), + columns: adjustColumnReferencesForChangedColumn(newLayer, columnId), + }, + indexPattern + ); + } + // This logic comes after the transitions because they need to look at previous columns if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { @@ -976,8 +1051,12 @@ export function deleteColumn({ ); } -// Derives column order from column object, respects existing columnOrder -// when possible, but also allows new columns to be added to the order +// Column order mostly affects the visual order in the UI. It is derived +// from the columns objects, respecting any existing columnOrder relationships, +// but allowing new columns to be inserted +// +// This does NOT topologically sort references, as this would cause the order in the UI +// to change. Reference order is determined before creating the pipeline in to_expression export function getColumnOrder(layer: IndexPatternLayer): string[] { const entries = Object.entries(layer.columns); entries.sort(([idA], [idB]) => { @@ -992,16 +1071,6 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { } }); - // If a reference has another reference as input, put it last in sort order - entries.sort(([idA, a], [idB, b]) => { - if ('references' in a && a.references.includes(idB)) { - return 1; - } - if ('references' in b && b.references.includes(idA)) { - return -1; - } - return 0; - }); const [aggregations, metrics] = _.partition(entries, ([, col]) => col.isBucketed); return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id)); @@ -1019,8 +1088,22 @@ export function getExistingColumnGroups(layer: IndexPatternLayer): [string[], st /** * Returns true if the given column can be applied to the given index pattern */ -export function isColumnTransferable(column: IndexPatternColumn, newIndexPattern: IndexPattern) { - return operationDefinitionMap[column.operationType].isTransferable(column, newIndexPattern); +export function isColumnTransferable( + column: IndexPatternColumn, + newIndexPattern: IndexPattern, + layer: IndexPatternLayer +): boolean { + return ( + operationDefinitionMap[column.operationType].isTransferable( + column, + newIndexPattern, + operationDefinitionMap + ) && + (!('references' in column) || + column.references.every((columnId) => + isColumnTransferable(layer.columns[columnId], newIndexPattern, layer) + )) + ); } export function updateLayerIndexPattern( @@ -1028,15 +1111,7 @@ export function updateLayerIndexPattern( newIndexPattern: IndexPattern ): IndexPatternLayer { const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => { - if ('references' in column) { - return ( - isColumnTransferable(column, newIndexPattern) && - column.references.every((columnId) => - isColumnTransferable(layer.columns[columnId], newIndexPattern) - ) - ); - } - return isColumnTransferable(column, newIndexPattern); + return isColumnTransferable(column, newIndexPattern, layer); }); const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => { const operationDefinition = operationDefinitionMap[column.operationType]; @@ -1069,7 +1144,7 @@ export function getErrorMessages( .flatMap(([columnId, column]) => { const def = operationDefinitionMap[column.operationType]; if (def.getErrorMessage) { - return def.getErrorMessage(layer, columnId, indexPattern); + return def.getErrorMessage(layer, columnId, indexPattern, operationDefinitionMap); } }) // remove the undefined values @@ -1147,6 +1222,23 @@ export function resetIncomplete(layer: IndexPatternLayer, columnId: string): Ind return { ...layer, incompleteColumns }; } +// managedReferences have a relaxed policy about operation allowed, so let them pass +function maybeValidateOperations({ + column, + validation, +}: { + column: IndexPatternColumn; + validation: RequiredReference; +}) { + if (!validation.specificOperations) { + return true; + } + if (operationDefinitionMap[column.operationType].input === 'managedReference') { + return true; + } + return validation.specificOperations.includes(column.operationType); +} + export function isColumnValidAsReference({ column, validation, @@ -1159,7 +1251,29 @@ export function isColumnValidAsReference({ const operationDefinition = operationDefinitionMap[operationType]; return ( validation.input.includes(operationDefinition.input) && - (!validation.specificOperations || validation.specificOperations.includes(operationType)) && + maybeValidateOperations({ + column, + validation, + }) && validation.validateMetadata(column) ); } + +export function getManagedColumnsFrom( + columnId: string, + columns: Record +): Array<[string, IndexPatternColumn]> { + const allNodes: Record = {}; + Object.entries(columns).forEach(([id, col]) => { + allNodes[id] = 'references' in col ? [...col.references] : []; + }); + const queue: string[] = allNodes[columnId]; + const store: Array<[string, IndexPatternColumn]> = []; + + while (queue.length > 0) { + const nextId = queue.shift()!; + store.push([nextId, columns[nextId]]); + queue.push(...allNodes[nextId]); + } + return store.filter(([, column]) => column); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts index 429d881341e79..4a2e065269063 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/mocks.ts @@ -8,7 +8,7 @@ import type { OperationMetadata } from '../../types'; import type { OperationType } from './definitions'; -export const createMockedReferenceOperation = () => { +export const createMockedFullReference = () => { return { input: 'fullReference', displayName: 'Reference test', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 4c54b777b66f3..7df096c27d9a0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -354,6 +354,14 @@ describe('getOperationTypesForField', () => { "operationType": "last_value", "type": "field", }, + Object { + "operationType": "math", + "type": "managedReference", + }, + Object { + "operationType": "formula", + "type": "managedReference", + }, ], }, Object { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts index a45650f9323f9..437d2af005961 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.ts @@ -106,6 +106,10 @@ type OperationFieldTuple = | { type: 'fullReference'; operationType: OperationType; + } + | { + type: 'managedReference'; + operationType: OperationType; }; /** @@ -138,7 +142,11 @@ type OperationFieldTuple = * ] * ``` */ -export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { +export function getAvailableOperationsByMetadata( + indexPattern: IndexPattern, + // For consistency in testing + customOperationDefinitionMap?: Record +) { const operationByMetadata: Record< string, { operationMetaData: OperationMetadata; operations: OperationFieldTuple[] } @@ -161,36 +169,49 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { } }; - operationDefinitions.sort(getSortScoreByPriority).forEach((operationDefinition) => { - if (operationDefinition.input === 'field') { - indexPattern.fields.forEach((field) => { + (customOperationDefinitionMap + ? Object.values(customOperationDefinitionMap) + : operationDefinitions + ) + .sort(getSortScoreByPriority) + .forEach((operationDefinition) => { + if (operationDefinition.input === 'field') { + indexPattern.fields.forEach((field) => { + addToMap( + { + type: 'field', + operationType: operationDefinition.type, + field: field.name, + }, + operationDefinition.getPossibleOperationForField(field) + ); + }); + } else if (operationDefinition.input === 'none') { addToMap( { - type: 'field', + type: 'none', operationType: operationDefinition.type, - field: field.name, }, - operationDefinition.getPossibleOperationForField(field) - ); - }); - } else if (operationDefinition.input === 'none') { - addToMap( - { - type: 'none', - operationType: operationDefinition.type, - }, - operationDefinition.getPossibleOperation() - ); - } else if (operationDefinition.input === 'fullReference') { - const validOperation = operationDefinition.getPossibleOperation(indexPattern); - if (validOperation) { - addToMap( - { type: 'fullReference', operationType: operationDefinition.type }, - validOperation + operationDefinition.getPossibleOperation() ); + } else if (operationDefinition.input === 'fullReference') { + const validOperation = operationDefinition.getPossibleOperation(indexPattern); + if (validOperation) { + addToMap( + { type: 'fullReference', operationType: operationDefinition.type }, + validOperation + ); + } + } else if (operationDefinition.input === 'managedReference') { + const validOperation = operationDefinition.getPossibleOperation(); + if (validOperation) { + addToMap( + { type: 'managedReference', operationType: operationDefinition.type }, + validOperation + ); + } } - } - }); + }); return Object.values(operationByMetadata); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 4f596aa282510..4905bd75d6498 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -60,22 +60,26 @@ function getExpressionForLayer( const [referenceEntries, esAggEntries] = partition( columnEntries, - ([, col]) => operationDefinitionMap[col.operationType]?.input === 'fullReference' + ([, col]) => + operationDefinitionMap[col.operationType]?.input === 'fullReference' || + operationDefinitionMap[col.operationType]?.input === 'managedReference' ); if (referenceEntries.length || esAggEntries.length) { const aggs: ExpressionAstExpressionBuilder[] = []; const expressions: ExpressionAstFunction[] = []; - referenceEntries.forEach(([colId, col]) => { + + sortedReferences(referenceEntries).forEach((colId) => { + const col = columns[colId]; const def = operationDefinitionMap[col.operationType]; - if (def.input === 'fullReference') { + if (def.input === 'fullReference' || def.input === 'managedReference') { expressions.push(...def.toExpression(layer, colId, indexPattern)); } }); esAggEntries.forEach(([colId, col]) => { const def = operationDefinitionMap[col.operationType]; - if (def.input !== 'fullReference') { + if (def.input !== 'fullReference' && def.input !== 'managedReference') { const wrapInFilter = Boolean(def.filterable && col.filter); let aggAst = def.toEsAggsFn( col, @@ -112,6 +116,10 @@ function getExpressionForLayer( } }); + if (esAggEntries.length === 0) { + // Return early if there are no aggs, for example if the user has an empty formula + return null; + } const idMap = esAggEntries.reduce((currentIdMap, [colId, column], index) => { const esAggsId = `col-${index}-${colId}`; return { @@ -245,6 +253,33 @@ function getExpressionForLayer( return null; } +// Topologically sorts references so that we can execute them in sequence +function sortedReferences(columns: Array) { + const allNodes: Record = {}; + columns.forEach(([id, col]) => { + allNodes[id] = 'references' in col ? col.references : []; + }); + // remove real metric references + columns.forEach(([id]) => { + allNodes[id] = allNodes[id].filter((refId) => !!allNodes[refId]); + }); + const ordered: string[] = []; + + while (ordered.length < columns.length) { + Object.keys(allNodes).forEach((id) => { + if (allNodes[id].length === 0) { + ordered.push(id); + delete allNodes[id]; + Object.keys(allNodes).forEach((k) => { + allNodes[k] = allNodes[k].filter((i) => i !== id); + }); + } + }); + } + + return ordered; +} + export function toExpression( state: IndexPatternPrivateState, layerId: string, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 19c37da5bf2a9..23c7adb86d34f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -62,7 +62,12 @@ export function isColumnInvalid( Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length); return ( - !!operationDefinition.getErrorMessage?.(layer, columnId, indexPattern) || referencesHaveErrors + !!operationDefinition.getErrorMessage?.( + layer, + columnId, + indexPattern, + operationDefinitionMap + ) || referencesHaveErrors ); } @@ -74,7 +79,12 @@ function getReferencesErrors( return column.references?.map((referenceId: string) => { const referencedOperation = layer.columns[referenceId]?.operationType; const referencedDefinition = operationDefinitionMap[referencedOperation]; - return referencedDefinition?.getErrorMessage?.(layer, referenceId, indexPattern); + return referencedDefinition?.getErrorMessage?.( + layer, + referenceId, + indexPattern, + operationDefinitionMap + ); }); } From e29f4c56dc555f14db38bc6faae7c5c23d31dd0b Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 14 May 2021 17:14:18 -0400 Subject: [PATCH 068/186] [Uptime] Improve accessibility labeling for `FilterPopover` component (#99714) * Improve accessibility labeling for `FilterPopover` component. * Simplify test revisions. * Simplify unit test. * Refactor test to use text formatter helper functions. --- .../filter_group/filter_popover.test.tsx | 23 +++++++--- .../overview/filter_group/filter_popover.tsx | 42 ++++++++++++------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx index bccebb21718bf..9094b280fc8ef 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.test.tsx @@ -7,7 +7,12 @@ import React from 'react'; import { fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; -import { FilterPopoverProps, FilterPopover } from './filter_popover'; +import { + FilterPopoverProps, + FilterPopover, + removeFilterForItemLabel, + filterByItemLabel, +} from './filter_popover'; import { render } from '../../../lib/helper/rtl_helpers'; describe('FilterPopover component', () => { @@ -77,17 +82,25 @@ describe('FilterPopover component', () => { fireEvent.click(uptimeFilterButton); - const generateLabelText = (item: string) => `Filter by ${props.title} ${item}.`; + selectedPropsItems.forEach((item) => { + expect(getByLabelText(removeFilterForItemLabel(item, props.title))); + }); itemsToClick.forEach((item) => { - const optionButtonLabelText = generateLabelText(item); - const optionButton = getByLabelText(optionButtonLabelText); + let optionButton: HTMLElement; + if (selectedPropsItems.some((i) => i === item)) { + optionButton = getByLabelText(removeFilterForItemLabel(item, props.title)); + } else { + optionButton = getByLabelText(filterByItemLabel(item, props.title)); + } fireEvent.click(optionButton); }); fireEvent.click(uptimeFilterButton); - await waitForElementToBeRemoved(() => queryByLabelText(generateLabelText(itemsToClick[0]))); + await waitForElementToBeRemoved(() => + queryByLabelText(`by ${props.title} ${itemsToClick[0]}`, { exact: false }) + ); expect(props.onFilterFieldChange).toHaveBeenCalledTimes(1); expect(props.onFilterFieldChange).toHaveBeenCalledWith(props.fieldName, expectedSelections); diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx index 23e17802a6835..79d39e6d2dd44 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx @@ -29,6 +29,18 @@ export interface FilterPopoverProps { const isItemSelected = (selectedItems: string[], item: string): 'on' | undefined => selectedItems.find((selected) => selected === item) ? 'on' : undefined; +export const filterByItemLabel = (item: string, title: string) => + i18n.translate('xpack.uptime.filterPopover.filterItem.label', { + defaultMessage: 'Filter by {title} {item}.', + values: { item, title }, + }); + +export const removeFilterForItemLabel = (item: string, title: string) => + i18n.translate('xpack.uptime.filterPopover.removeFilterItem.label', { + defaultMessage: 'Remove filter by {title} {item}.', + values: { item, title }, + }); + export const FilterPopover = ({ fieldName, id, @@ -126,20 +138,22 @@ export const FilterPopover = ({ /> {!loading && - itemsToDisplay.map((item) => ( - toggleSelectedItems(item, tempSelectedItems, setTempSelectedItems)} - > - {item} - - ))} + itemsToDisplay.map((item) => { + const checked = isItemSelected(tempSelectedItems, item); + return ( + toggleSelectedItems(item, tempSelectedItems, setTempSelectedItems)} + > + {item} + + ); + })} {id === 'location' && items.length === 0 && } ); From 7ab55f35f5cd1cdf8dd1d299bcaef95d3230a214 Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 14 May 2021 15:13:26 -0700 Subject: [PATCH 069/186] [App Search] Meta engines schema view (#100087) * Set up TruncatedEnginesList component - Used for listing source engines - New in Kibana: now links to source engine schema pages for easier schema fixes! * Add meta engines schema active fields table * Render meta engine schema conflicts table & warning callout * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx Co-authored-by: Jason Stoltzfus Co-authored-by: Jason Stoltzfus --- .../components/schema/components/index.ts | 3 + .../meta_engines_conflicts_table.test.tsx | 69 ++++++++++++++++ .../meta_engines_conflicts_table.tsx | 69 ++++++++++++++++ .../meta_engines_schema_table.test.tsx | 63 +++++++++++++++ .../components/meta_engines_schema_table.tsx | 78 +++++++++++++++++++ .../truncated_engines_list.test.tsx | 41 ++++++++++ .../components/truncated_engines_list.tsx | 60 ++++++++++++++ .../schema/views/meta_engine_schema.test.tsx | 15 +++- .../schema/views/meta_engine_schema.tsx | 76 +++++++++++++++++- 9 files changed, 469 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts index 7da44849b5bc0..6e17547a93980 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/index.ts @@ -8,3 +8,6 @@ export { SchemaCallouts } from './schema_callouts'; export { SchemaTable } from './schema_table'; export { EmptyState } from './empty_state'; +export { MetaEnginesSchemaTable } from './meta_engines_schema_table'; +export { MetaEnginesConflictsTable } from './meta_engines_conflicts_table'; +export { TruncatedEnginesList } from './truncated_engines_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx new file mode 100644 index 0000000000000..eb40d70e13ff8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.test.tsx @@ -0,0 +1,69 @@ +/* + * 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 { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow } from '@elastic/eui'; + +import { MetaEnginesConflictsTable } from './'; + +describe('MetaEnginesConflictsTable', () => { + const values = { + conflictingFields: { + hello_field: { + text: ['engine1'], + number: ['engine2'], + date: ['engine3'], + }, + world_field: { + text: ['engine1'], + location: ['engine2', 'engine3', 'engine4'], + }, + }, + }; + + setMockValues(values); + const wrapper = mount(); + const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]'); + const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldTypes"]'); + const engines = wrapper.find('EuiTableRowCell[data-test-subj="enginesPerFieldType"]'); + + it('renders', () => { + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Field type conflicts'); + expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Engines'); + }); + + it('renders a rowspan on the initial field name column so that it stretches to all associated field conflict rows', () => { + expect(fieldNames).toHaveLength(2); + expect(fieldNames.at(0).prop('rowSpan')).toEqual(3); + expect(fieldNames.at(1).prop('rowSpan')).toEqual(2); + }); + + it('renders a row for each field type conflict and the engines that have that field type', () => { + expect(wrapper.find(EuiTableRow)).toHaveLength(5); + + expect(fieldNames.at(0).text()).toEqual('hello_field'); + expect(fieldTypes.at(0).text()).toEqual('text'); + expect(engines.at(0).text()).toEqual('engine1'); + expect(fieldTypes.at(1).text()).toEqual('number'); + expect(engines.at(1).text()).toEqual('engine2'); + expect(fieldTypes.at(2).text()).toEqual('date'); + expect(engines.at(2).text()).toEqual('engine3'); + + expect(fieldNames.at(1).text()).toEqual('world_field'); + expect(fieldTypes.at(3).text()).toEqual('text'); + expect(engines.at(3).text()).toEqual('engine1'); + expect(fieldTypes.at(4).text()).toEqual('location'); + expect(engines.at(4).text()).toEqual('engine2, engine3, +1'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx new file mode 100644 index 0000000000000..a37caafe69a59 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_conflicts_table.tsx @@ -0,0 +1,69 @@ +/* + * 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 React from 'react'; + +import { useValues } from 'kea'; + +import { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_NAME } from '../../../../shared/schema/constants'; +import { ENGINES_TITLE } from '../../engines'; + +import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; + +import { TruncatedEnginesList } from './'; + +export const MetaEnginesConflictsTable: React.FC = () => { + const { conflictingFields } = useValues(MetaEngineSchemaLogic); + + return ( + + + {FIELD_NAME} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.fieldTypeConflicts', + { defaultMessage: 'Field type conflicts' } + )} + + {ENGINES_TITLE} + + + {Object.entries(conflictingFields).map(([fieldName, conflicts]) => + Object.entries(conflicts).map(([fieldType, engines], i) => { + const isFirstRow = i === 0; + return ( + + {isFirstRow && ( + + {fieldName} + + )} + {fieldType} + + + + + ); + }) + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx new file mode 100644 index 0000000000000..7d377d5a92714 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.test.tsx @@ -0,0 +1,63 @@ +/* + * 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 { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiTable, EuiTableHeaderCell, EuiTableRow, EuiTableRowCell } from '@elastic/eui'; + +import { MetaEnginesSchemaTable } from './'; + +describe('MetaEnginesSchemaTable', () => { + const values = { + schema: { + some_text_field: 'text', + some_number_field: 'number', + }, + fields: { + some_text_field: { + text: ['engine1', 'engine2'], + }, + some_number_field: { + number: ['engine1', 'engine2', 'engine3', 'engine4'], + }, + }, + }; + + setMockValues(values); + const wrapper = mount(); + const fieldNames = wrapper.find('EuiTableRowCell[data-test-subj="fieldName"]'); + const engines = wrapper.find('EuiTableRowCell[data-test-subj="engines"]'); + const fieldTypes = wrapper.find('EuiTableRowCell[data-test-subj="fieldType"]'); + + it('renders', () => { + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(EuiTableHeaderCell).at(0).text()).toEqual('Field name'); + expect(wrapper.find(EuiTableHeaderCell).at(1).text()).toEqual('Engines'); + expect(wrapper.find(EuiTableHeaderCell).at(2).text()).toEqual('Field type'); + }); + + it('always renders an initial ID row', () => { + expect(wrapper.find('code').at(0).text()).toEqual('id'); + expect(wrapper.find(EuiTableRowCell).at(1).text()).toEqual('All'); + }); + + it('renders subsequent table rows for each schema field', () => { + expect(wrapper.find(EuiTableRow)).toHaveLength(3); + + expect(fieldNames.at(0).text()).toEqual('some_text_field'); + expect(engines.at(0).text()).toEqual('engine1, engine2'); + expect(fieldTypes.at(0).text()).toEqual('text'); + + expect(fieldNames.at(1).text()).toEqual('some_number_field'); + expect(engines.at(1).text()).toEqual('engine1, engine2, engine3, +1'); + expect(fieldTypes.at(1).text()).toEqual('number'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx new file mode 100644 index 0000000000000..2367ad4e0c53e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/meta_engines_schema_table.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { + EuiTable, + EuiTableHeader, + EuiTableHeaderCell, + EuiTableBody, + EuiTableRow, + EuiTableRowCell, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_NAME, FIELD_TYPE } from '../../../../shared/schema/constants'; +import { ENGINES_TITLE } from '../../engines'; + +import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; + +import { TruncatedEnginesList } from './'; + +export const MetaEnginesSchemaTable: React.FC = () => { + const { schema, fields } = useValues(MetaEngineSchemaLogic); + + return ( + + + {FIELD_NAME} + {ENGINES_TITLE} + {FIELD_TYPE} + + + + + + id + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.allEngines', + { defaultMessage: 'All' } + )} + + + + + {Object.keys(fields).map((fieldName) => { + const fieldType = schema[fieldName]; + const engines = fields[fieldName][fieldType]; + + return ( + + + {fieldName} + + + + + + {fieldType} + + + ); + })} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx new file mode 100644 index 0000000000000..193d727be00b5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { TruncatedEnginesList } from './'; + +describe('TruncatedEnginesList', () => { + it('renders a list of engines with links to their schema pages', () => { + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(3); + expect(wrapper.find('[data-test-subj="displayedEngine"]').first().prop('to')).toEqual( + '/engines/engine1/schema' + ); + }); + + it('renders a tooltip when the number of engines is greater than the cutoff', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="displayedEngine"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="hiddenEnginesTooltip"]').prop('content')).toEqual( + 'engine2, engine3' + ); + }); + + it('does not render if no engines are passed', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx new file mode 100644 index 0000000000000..a642eb99e3563 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/components/truncated_engines_list.tsx @@ -0,0 +1,60 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiText, EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { ENGINE_SCHEMA_PATH } from '../../../routes'; +import { generateEncodedPath } from '../../../utils/encode_path_params'; + +interface Props { + engines?: string[]; + cutoff?: number; +} + +export const TruncatedEnginesList: React.FC = ({ engines, cutoff = 3 }) => { + if (!engines?.length) return null; + + const displayedEngines = engines.slice(0, cutoff); + const hiddenEngines = engines.slice(cutoff); + const SEPARATOR = ', '; + + return ( + + {displayedEngines.map((engineName, i) => { + const isLast = i === displayedEngines.length - 1; + return ( + + + {engineName} + + {!isLast ? SEPARATOR : ''} + + ); + })} + {hiddenEngines.length > 0 && ( + <> + {SEPARATOR} + + + +{hiddenEngines.length} + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx index a6e9eef8efa70..b1322c148b577 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.test.tsx @@ -12,8 +12,12 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + import { Loading } from '../../../../shared/loading'; +import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; + import { MetaEngineSchema } from './'; describe('MetaEngineSchema', () => { @@ -33,8 +37,7 @@ describe('MetaEngineSchema', () => { it('renders', () => { const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); - // TODO: Check for schema components + expect(wrapper.find(MetaEnginesSchemaTable)).toHaveLength(1); }); it('calls loadSchema on mount', () => { @@ -49,4 +52,12 @@ describe('MetaEngineSchema', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); + + it('renders an inactive fields callout & table when source engines have schema conflicts', () => { + setMockValues({ ...values, hasConflicts: true, conflictingFieldsCount: 5 }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(MetaEnginesConflictsTable)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx index 234fcdb5a5a50..4c0235cf81129 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/views/meta_engine_schema.tsx @@ -9,17 +9,19 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; +import { EuiPageHeader, EuiPageContentBody, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; import { Loading } from '../../../../shared/loading'; +import { DataPanel } from '../../data_panel'; +import { MetaEnginesSchemaTable, MetaEnginesConflictsTable } from '../components'; import { MetaEngineSchemaLogic } from '../schema_meta_engine_logic'; export const MetaEngineSchema: React.FC = () => { const { loadSchema } = useActions(MetaEngineSchemaLogic); - const { dataLoading } = useValues(MetaEngineSchemaLogic); + const { dataLoading, hasConflicts, conflictingFieldsCount } = useValues(MetaEngineSchemaLogic); useEffect(() => { loadSchema(); @@ -40,7 +42,75 @@ export const MetaEngineSchema: React.FC = () => { )} /> - TODO + + {hasConflicts && ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.conflictsCalloutDescription', + { + defaultMessage: + 'The field(s) have an inconsistent field-type across the source engines that make up this meta engine. Apply a consistent field-type from the source engines to make these fields searchable.', + } + )} +

+
+ + + )} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsTitle', + { defaultMessage: 'Active fields' } + )} +

+ } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.activeFieldsDescription', + { defaultMessage: 'Fields which belong to one or more engine.' } + )} + > + + + + {hasConflicts && ( + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsTitle', + { defaultMessage: 'Inactive fields' } + )} + + } + subtitle={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.schema.metaEngine.inactiveFieldsDescription', + { + defaultMessage: + 'These fields have type conflicts. To activate these fields, change types in the source engines to match.', + } + )} + > + + + )} + ); }; From 13e5a18c286c91f23ff1ba1ba919c1f90a71644e Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Sat, 15 May 2021 00:17:10 +0200 Subject: [PATCH 070/186] [status_page test] use navigateToApp (#100146) --- .github/CODEOWNERS | 2 -- .../apps/status_page/status_page.ts | 7 +++---- .../functional/page_objects/status_page.ts | 20 +------------------ 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de323128afed1..39daa5780436f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -203,7 +203,6 @@ /packages/kbn-legacy-logging/ @elastic/kibana-core /packages/kbn-crypto/ @elastic/kibana-core /packages/kbn-http-tools/ @elastic/kibana-core -/src/plugins/status_page/ @elastic/kibana-core /src/plugins/saved_objects_management/ @elastic/kibana-core /src/dev/run_check_published_api_changes.ts @elastic/kibana-core /src/plugins/home/public @elastic/kibana-core @@ -215,7 +214,6 @@ #CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core -#CC# /src/plugins/status_page/ @elastic/kibana-core #CC# /x-pack/plugins/cloud/ @elastic/kibana-core #CC# /x-pack/plugins/features/ @elastic/kibana-core #CC# /x-pack/plugins/global_search/ @elastic/kibana-core diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index 55a54245cf832..ecef6225632e9 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -12,17 +12,16 @@ export default function statusPageFunctonalTests({ getPageObjects, }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['security', 'statusPage', 'home']); + const PageObjects = getPageObjects(['security', 'statusPage', 'common']); - // FLAKY: https://github.com/elastic/kibana/issues/50448 - describe.skip('Status Page', function () { + describe('Status Page', function () { this.tags(['skipCloud', 'includeFirefox']); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); it('allows user to navigate without authentication', async () => { await PageObjects.security.forceLogout(); - await PageObjects.statusPage.navigateToPage(); + await PageObjects.common.navigateToApp('status_page', { shouldLoginIfPrompted: false }); await PageObjects.statusPage.expectStatusPage(); }); }); diff --git a/x-pack/test/functional/page_objects/status_page.ts b/x-pack/test/functional/page_objects/status_page.ts index 9edaf4dea53f8..ed90aef954770 100644 --- a/x-pack/test/functional/page_objects/status_page.ts +++ b/x-pack/test/functional/page_objects/status_page.ts @@ -5,36 +5,18 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function StatusPagePageProvider({ getService }: FtrProviderContext) { - const retry = getService('retry'); const log = getService('log'); - const browser = getService('browser'); const find = getService('find'); - const deployment = getService('deployment'); - class StatusPage { async initTests() { log.debug('StatusPage:initTests'); } - async navigateToPage() { - return await retry.try(async () => { - const url = deployment.getHostPort() + '/status'; - log.info(`StatusPage:navigateToPage(): ${url}`); - await browser.get(url); - }); - } - async expectStatusPage(): Promise { - return await retry.try(async () => { - log.debug(`expectStatusPage()`); - await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); - const url = await browser.getCurrentUrl(); - expect(url).to.contain(`/status`); - }); + await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); } } From a818f144415220d4904a2ad482bf6a963c425d6d Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 14 May 2021 16:56:08 -0600 Subject: [PATCH 071/186] [Security Solutions] Removes deprecation and more copied code between security solutions and lists plugin (#100150) ## Summary * Removes deprecations * Removes duplicated code ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/lists/common/shared_exports.ts | 39 --- .../autocomplete/field_value_match.tsx | 2 +- .../autocomplete/field_value_match_any.tsx | 2 +- .../components/autocomplete/helpers.ts | 3 +- .../use_field_value_autocomplete.test.ts | 2 +- .../hooks/use_field_value_autocomplete.ts | 2 +- .../components/autocomplete/operators.ts | 6 +- .../components/autocomplete/types.ts | 6 +- .../builder/entry_renderer.stories.tsx | 5 +- .../components/builder/entry_renderer.tsx | 8 +- .../builder/exception_item_renderer.tsx | 3 +- .../builder/exception_items_renderer.tsx | 16 +- .../components/builder/helpers.test.ts | 330 ++++++++++++++++- .../exceptions/components/builder/helpers.ts | 24 +- .../exceptions/components/builder/reducer.ts | 4 +- .../exceptions/components/builder/types.ts | 16 +- .../lists/public/exceptions/transforms.ts | 3 +- x-pack/plugins/lists/public/shared_exports.ts | 14 +- .../detection_engine/schemas/types/lists.ts | 2 +- .../common/detection_engine/utils.test.ts | 3 +- .../common/detection_engine/utils.ts | 9 +- .../common/shared_imports.ts | 33 -- .../autocomplete/field_value_match.tsx | 3 +- .../autocomplete/field_value_match_any.tsx | 3 +- .../common/components/autocomplete/helpers.ts | 3 +- .../use_field_value_autocomplete.test.ts | 2 +- .../hooks/use_field_value_autocomplete.ts | 2 +- .../components/autocomplete/operators.ts | 5 +- .../common/components/autocomplete/types.ts | 5 +- .../exceptions/add_exception_comments.tsx | 2 +- .../add_exception_modal/index.test.tsx | 5 +- .../exceptions/add_exception_modal/index.tsx | 3 +- .../exceptions/edit_exception_modal/index.tsx | 3 +- .../components/exceptions/helpers.test.tsx | 331 +----------------- .../common/components/exceptions/helpers.tsx | 194 +--------- .../common/components/exceptions/types.ts | 19 +- .../exceptions/use_add_exception.test.tsx | 9 +- ...tch_or_create_rule_exception_list.test.tsx | 5 +- .../exceptions_viewer_header.stories.tsx | 2 +- .../viewer/exceptions_viewer_header.test.tsx | 3 +- .../viewer/exceptions_viewer_header.tsx | 2 +- .../components/exceptions/viewer/helpers.tsx | 10 +- .../exceptions/viewer/index.test.tsx | 8 +- .../components/exceptions/viewer/index.tsx | 3 +- .../components/exceptions/viewer/reducer.ts | 2 +- .../timeline_actions/alert_context_menu.tsx | 2 +- .../value_lists_management_modal/form.tsx | 4 +- .../rules/all/exceptions/columns.tsx | 2 +- .../rules/all/exceptions/exceptions_table.tsx | 2 +- .../detection_engine/rules/create/helpers.ts | 3 +- .../detection_engine/rules/details/index.tsx | 4 +- .../pages/event_filters/constants.ts | 3 +- .../public/shared_imports.ts | 10 +- .../endpoint/routes/trusted_apps/mapping.ts | 12 +- .../create_field_and_set_tuples.test.ts | 2 +- .../filters/create_field_and_set_tuples.ts | 2 +- 56 files changed, 501 insertions(+), 701 deletions(-) diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index bc9d0ca8d7b94..f00afb7ac810d 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -5,45 +5,6 @@ * 2.0. */ -// TODO: We should remove these and instead directly import them in the security_solution project. This is to get my PR across the line without too many conflicts. -export { - CommentsArray, - Comment, - CreateComment, - CreateCommentsArray, - Entry, - EntryExists, - EntryMatch, - EntryMatchAny, - EntryMatchWildcard, - EntryNested, - EntryList, - EntriesArray, - NamespaceType, - NestedEntriesArray, - ListOperator as Operator, - ListOperatorEnum as OperatorEnum, - ListOperatorTypeEnum as OperatorTypeEnum, - listOperator as operator, - ExceptionListTypeEnum, - ExceptionListType, - comment, - exceptionListType, - entry, - entriesNested, - nestedEntryItem, - entriesMatch, - entriesMatchAny, - entriesMatchWildcard, - entriesExists, - entriesList, - namespaceType, - osType, - osTypeArray, - OsTypeArray, - Type, -} from '@kbn/securitysolution-io-ts-list-types'; - export { ListSchema, ExceptionListSchema, diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx index a0994871808d1..c1776280842c6 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx @@ -14,8 +14,8 @@ import { EuiSuperSelect, } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorTypeEnum } from '../../../../common'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx index 08958f6d99aab..82347f6212442 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx @@ -8,8 +8,8 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorTypeEnum } from '../../../../common'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts index 4f25bec3b38dc..b982193d1d349 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts @@ -7,8 +7,9 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; -import { ListSchema, Type } from '../../../../common'; +import type { ListSchema } from '../../../../common'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index 4e3fb2179d786..0335ffa55d2a2 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -6,10 +6,10 @@ */ import { act, renderHook } from '@testing-library/react-hooks'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { OperatorTypeEnum } from '../../../../../common'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts index 6c6198ac55a0f..674bb5e5537d9 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -7,10 +7,10 @@ import { useEffect, useRef, useState } from 'react'; import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { OperatorTypeEnum } from '../../../../../common'; interface FuncArgs { fieldSelected: IFieldType | undefined; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts index 551dfcb61e3ad..83a424d72ec5f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/operators.ts @@ -6,8 +6,10 @@ */ import { i18n } from '@kbn/i18n'; - -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; import { OperatorOption } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts index 8ea3e8d927d68..76d5b7758007b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/autocomplete/types.ts @@ -6,8 +6,10 @@ */ import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; +import type { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; export interface GetGenericComboBoxPropsReturn { comboOptions: EuiComboBoxOptionOption[]; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx index 5b3730a6deb93..dd67381c30934 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx @@ -9,8 +9,11 @@ import { Story } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { HttpStart } from 'kibana/public'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; -import { OperatorEnum, OperatorTypeEnum } from '../../../../common'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 0ece28d409bd5..09863660e98af 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -8,7 +8,11 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import styled from 'styled-components'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListType, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; @@ -21,7 +25,7 @@ import { AutocompleteFieldExistsComponent } from '../autocomplete/field_value_ex import { AutocompleteFieldMatchComponent } from '../autocomplete/field_value_match'; import { AutocompleteFieldMatchAnyComponent } from '../autocomplete/field_value_match_any'; import { AutocompleteFieldListsComponent } from '../autocomplete/field_value_lists'; -import { ExceptionListType, ListSchema, OperatorTypeEnum } from '../../../../common'; +import { ListSchema } from '../../../../common'; import { getEmptyValue } from '../../../common/empty_value'; import { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index 94c3bff8f4cf9..e10cd2934328f 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -10,9 +10,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { AutocompleteStart } from 'src/plugins/data/public'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListType, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionListType } from '../../../../common'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 4ec152e155e39..f771969a92025 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -10,19 +10,21 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from 'kibana/public'; import { addIdToItem } from '@kbn/securitysolution-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - CreateExceptionListItemSchema, - ExceptionListItemSchema, ExceptionListType, NamespaceType, - OperatorEnum, - OperatorTypeEnum, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, entriesNested, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, exceptionListItemSchema, } from '../../../../common'; +import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { AndOrBadge } from '../and_or_badge'; import { CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem } from './types'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts index 1e74193299e56..dbfeaa4a258ca 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts @@ -5,6 +5,18 @@ * 2.0. */ +import { + EntryExists, + EntryList, + EntryMatch, + EntryMatchAny, + EntryNested, + ExceptionListType, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common'; import { ENTRIES_WITH_IDS } from '../../../../common/constants.mock'; import { getEntryExistsMock } from '../../../../common/schemas/types/entry_exists.mock'; import { getExceptionListItemSchemaMock } from '../../../../common/schemas/response/exception_list_item_schema.mock'; @@ -23,25 +35,23 @@ import { doesNotExistOperator, existsOperator, isInListOperator, + isNotInListOperator, isNotOneOfOperator, isNotOperator, isOneOfOperator, isOperator, } from '../autocomplete/operators'; -import { - EntryExists, - EntryList, - EntryMatch, - EntryMatchAny, - EntryNested, - ExceptionListType, - OperatorEnum, - OperatorTypeEnum, -} from '../../../../common'; import { OperatorOption } from '../autocomplete/types'; +import { getEntryListMock } from '../../../../common/schemas/types/entry_list.mock'; -import { BuilderEntry, ExceptionsBuilderExceptionItem, FormattedBuilderEntry } from './types'; import { + BuilderEntry, + EmptyEntry, + ExceptionsBuilderExceptionItem, + FormattedBuilderEntry, +} from './types'; +import { + filterExceptionItems, getCorrespondingKeywordField, getEntryFromOperator, getEntryOnFieldChange, @@ -49,10 +59,14 @@ import { getEntryOnMatchAnyChange, getEntryOnMatchChange, getEntryOnOperatorChange, + getEntryValue, + getExceptionOperatorSelect, getFilteredIndexPatterns, getFormattedBuilderEntries, getFormattedBuilderEntry, + getNewExceptionItem, getOperatorOptions, + getOperatorType, getUpdatedEntriesOnDelete, isEntryNested, } from './helpers'; @@ -1426,4 +1440,298 @@ describe('Exception builder helpers', () => { expect(output).toEqual(undefined); }); }); + + describe('#getOperatorType', () => { + test('returns operator type "match" if entry.type is "match"', () => { + const payload = getEntryMatchMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.MATCH); + }); + + test('returns operator type "match_any" if entry.type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY); + }); + + test('returns operator type "list" if entry.type is "list"', () => { + const payload = getEntryListMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.LIST); + }); + + test('returns operator type "exists" if entry.type is "exists"', () => { + const payload = getEntryExistsMock(); + const operatorType = getOperatorType(payload); + + expect(operatorType).toEqual(OperatorTypeEnum.EXISTS); + }); + }); + + describe('#getExceptionOperatorSelect', () => { + test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => { + const payload = getEntryMatchMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isOperator); + }); + + test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => { + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotOperator); + }); + + test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isOneOfOperator); + }); + + test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => { + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotOneOfOperator); + }); + + test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => { + const payload = getEntryExistsMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(existsOperator); + }); + + test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => { + const payload = getEntryExistsMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(doesNotExistOperator); + }); + + test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => { + const payload = getEntryListMock(); + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isInListOperator); + }); + + test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => { + const payload = getEntryListMock(); + payload.operator = 'excluded'; + const result = getExceptionOperatorSelect(payload); + + expect(result).toEqual(isNotInListOperator); + }); + }); + + describe('#filterExceptionItems', () => { + // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes + // for context around the temporary `id` + test('it correctly validates entries that include a temporary `id`', () => { + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]); + }); + + test('it removes entry items with "value" of "undefined"', () => { + const { entries, ...rest } = getExceptionListItemSchemaMock(); + const mockEmptyException: EmptyEntry = { + field: 'host.name', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: undefined, + }; + const exceptions = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); + }); + + test('it removes "match" entry items with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: 'host.name', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: '', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH, + value: 'some value', + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "match_any" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EmptyEntry = { + field: '', + id: '123', + operator: OperatorEnum.INCLUDED, + type: OperatorTypeEnum.MATCH_ANY, + value: ['some value'], + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes "nested" entry items with "field" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [getEntryMatchMock()], + field: '', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes the "nested" entry entries with "value" of empty string', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], + field: 'host.name', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [ + ...getExceptionListItemSchemaMock().entries, + { ...mockEmptyException, entries: [getEntryMatchMock()] }, + ], + }, + ]); + }); + + test('it removes the "nested" entry item if all its entries are invalid', () => { + const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; + const mockEmptyException: EntryNested = { + entries: [{ ...getEntryMatchMock(), value: '' }], + field: 'host.name', + type: OperatorTypeEnum.NESTED, + }; + const output: Array< + ExceptionListItemSchema | CreateExceptionListItemSchema + > = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); + }); + + test('it removes `temporaryId` from items', () => { + const { meta, ...rest } = getNewExceptionItem({ + listId: '123', + namespaceType: 'single', + ruleName: 'rule name', + }); + const exceptions = filterExceptionItems([{ ...rest, meta }]); + + expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); + }); + }); + + describe('#getEntryValue', () => { + it('returns "match" entry value', () => { + const payload = getEntryMatchMock(); + const result = getEntryValue(payload); + const expected = 'some host name'; + expect(result).toEqual(expected); + }); + + it('returns "match any" entry values', () => { + const payload = getEntryMatchAnyMock(); + const result = getEntryValue(payload); + const expected = ['some host name']; + expect(result).toEqual(expected); + }); + + it('returns "exists" entry value', () => { + const payload = getEntryExistsMock(); + const result = getEntryValue(payload); + const expected = undefined; + expect(result).toEqual(expected); + }); + + it('returns "list" entry value', () => { + const payload = getEntryListMock(); + const result = getEntryValue(payload); + const expected = 'some-list-id'; + expect(result).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 18d607d6807fc..6cd9dec0dc7a1 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -8,27 +8,29 @@ import uuid from 'uuid'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; import { validate } from '@kbn/securitysolution-io-ts-utils'; -import { OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; - -import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { - CreateExceptionListItemSchema, EntriesArray, Entry, EntryNested, - ExceptionListItemSchema, ExceptionListType, - ListSchema, NamespaceType, - OperatorEnum, - OperatorTypeEnum, - createExceptionListItemSchema, + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, + OsTypeArray, entriesList, entriesNested, entry, - exceptionListItemSchema, nestedEntryItem, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + ListSchema, + createExceptionListItemSchema, + exceptionListItemSchema, } from '../../../../common'; +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { EXCEPTION_OPERATORS, EXCEPTION_OPERATORS_SANS_LISTS, @@ -96,7 +98,7 @@ export const filterExceptionItems = ( return [...acc, item]; } else if (createExceptionListItemSchema.is(item)) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { meta: _, ...rest } = item; + const { meta, ...rest } = item; const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; return [...acc, itemSansMetaId]; } else { diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts index 92df2fd3793de..0e8a5fadd3b1a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/reducer.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { ExceptionListItemSchema, OperatorTypeEnum } from '../../../../common'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionListItemSchema } from '../../../../common'; import { ExceptionsBuilderExceptionItem } from './types'; import { getDefaultEmptyEntry } from './helpers'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts index 800f1445217b9..5cf4238ab5e0c 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/types.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/types.ts @@ -5,20 +5,20 @@ * 2.0. */ -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { - CreateExceptionListItemSchema, +import type { Entry, EntryExists, EntryMatch, EntryMatchAny, EntryMatchWildcard, EntryNested, - ExceptionListItemSchema, - OperatorEnum, - OperatorTypeEnum, -} from '../../../../common'; + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import type { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../../../../common'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; export interface FormattedBuilderEntry { id: string; diff --git a/x-pack/plugins/lists/public/exceptions/transforms.ts b/x-pack/plugins/lists/public/exceptions/transforms.ts index 50ce1b6e33a4b..564ba1a699f98 100644 --- a/x-pack/plugins/lists/public/exceptions/transforms.ts +++ b/x-pack/plugins/lists/public/exceptions/transforms.ts @@ -7,11 +7,10 @@ import { flow } from 'fp-ts/lib/function'; import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; +import type { EntriesArray, Entry } from '@kbn/securitysolution-io-ts-list-types'; import type { CreateExceptionListItemSchema, - EntriesArray, - Entry, ExceptionListItemSchema, UpdateExceptionListItemSchema, } from '../../common'; diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index 2032a44a8fd33..6d14c6b541904 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -7,11 +7,8 @@ // Exports to be shared with plugins export { withOptionalSignal } from './common/with_optional_signal'; -export { useIsMounted } from './common/hooks/use_is_mounted'; export { useAsync } from './common/hooks/use_async'; export { useApi } from './exceptions/hooks/use_api'; -export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; -export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionListItems } from './exceptions/hooks/use_exception_list_items'; export { useExceptionLists } from './exceptions/hooks/use_exception_lists'; export { useFindLists } from './lists/hooks/use_find_lists'; @@ -24,13 +21,18 @@ export { useReadListIndex } from './lists/hooks/use_read_list_index'; export { useCreateListIndex } from './lists/hooks/use_create_list_index'; export { useReadListPrivileges } from './lists/hooks/use_read_list_privileges'; export { - addExceptionListItem, - updateExceptionListItem, + getEntryValue, + getExceptionOperatorSelect, + getOperatorType, + getNewExceptionItem, + addIdToEntries, +} from './exceptions/components/builder/helpers'; +export { fetchExceptionListById, addExceptionList, addEndpointExceptionList, } from './exceptions/api'; -export { +export type { ExceptionList, ExceptionListFilter, ExceptionListIdentifiers, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts index 79fd264808138..e2c3ee88f6a65 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; -import { exceptionListType, namespaceType } from '../../../shared_imports'; +import { exceptionListType, namespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { NonEmptyString } from './non_empty_string'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index c477036a07d85..1e0f7e087a5b3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -13,7 +13,8 @@ import { normalizeMachineLearningJobIds, normalizeThresholdField, } from './utils'; -import { EntriesArray } from '../shared_imports'; + +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; describe('#hasLargeValueList', () => { test('it returns false if empty array', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index a8e0ffcccef82..611d23fd1ce22 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -7,11 +7,10 @@ import { isEmpty } from 'lodash'; -import { - CreateExceptionListItemSchema, - EntriesArray, - ExceptionListItemSchema, -} from '../shared_imports'; +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; + +import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../shared_imports'; + import { Type, JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; export const hasLargeValueItem = ( diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index e987775a8e768..a6bad0347e641 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -7,44 +7,14 @@ export { ListSchema, - CommentsArray, - CreateCommentsArray, - Comment, - CreateComment, ExceptionListSchema, ExceptionListItemSchema, CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, - Entry, - EntryExists, - EntryMatch, - EntryMatchAny, - EntryMatchWildcard, - EntryNested, - EntryList, - EntriesArray, - NamespaceType, - Operator, - OperatorEnum, - OperatorTypeEnum, - ExceptionListTypeEnum, exceptionListItemSchema, - exceptionListType, - comment, createExceptionListItemSchema, listSchema, - entry, - entriesNested, - nestedEntryItem, - entriesMatch, - entriesMatchAny, - entriesMatchWildcard, - entriesExists, - entriesList, - namespaceType, - ExceptionListType, - Type, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, EXCEPTION_LIST_URL, @@ -52,8 +22,5 @@ export { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, - osType, - osTypeArray, - OsTypeArray, buildExceptionFilter, } from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index c1efb4d7c4565..9cb219e7a8d45 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -15,10 +15,11 @@ import { } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; import { paramIsValid, getGenericComboBoxProps } from './helpers'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; + import { GetGenericComboBoxPropsReturn } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx index e77bf570adc63..dbfdaf9749b6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -9,11 +9,12 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { uniq } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; import { getGenericComboBoxProps, paramIsValid } from './helpers'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; import { GetGenericComboBoxPropsReturn } from './types'; + import * as i18n from './translations'; interface AutocompleteFieldMatchAnyProps { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index b89f9525024c7..bd79bb0fcc8e8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -8,6 +8,8 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; +import type { ListSchema } from '../../../lists_plugin_deps'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { @@ -19,7 +21,6 @@ import { } from './operators'; import { GetGenericComboBoxPropsReturn, OperatorOption } from './types'; import * as i18n from './translations'; -import { ListSchema, Type } from '../../../lists_plugin_deps'; /** * Returns the appropriate operators given a field type diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts index 36e050c84f0b3..e0bdbf2603dc3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -15,7 +15,7 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts index b8440205e7d32..0f369fa01d01e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts @@ -8,9 +8,9 @@ import { useEffect, useState, useRef } from 'react'; import { debounce } from 'lodash'; +import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { useKibana } from '../../../../common/lib/kibana'; -import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; interface FuncArgs { fieldSelected: IFieldType | undefined; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts index 93eab41264bf7..53e2ddf84b3d3 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts @@ -6,8 +6,11 @@ */ import { i18n } from '@kbn/i18n'; +import { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; import { OperatorOption } from './types'; -import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; export const isOperator: OperatorOption = { message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts index 903edc403ea25..1d8e3e9aee28e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts @@ -7,7 +7,10 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; +import type { + ListOperatorEnum as OperatorEnum, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; export interface GetGenericComboBoxPropsReturn { comboOptions: EuiComboBoxOptionOption[]; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx index c627363fc29ef..c13a1b011ccbd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx @@ -17,7 +17,7 @@ import { EuiCommentProps, EuiText, } from '@elastic/eui'; -import { Comment } from '../../../shared_imports'; +import type { Comment } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from './translations'; import { useCurrentUser } from '../../lib/kibana'; import { getFormattedComments } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 5ec8999d20518..5fb527a821bac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -49,7 +49,10 @@ jest.mock('../../../containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); -jest.mock('../../../../shared_imports'); +jest.mock('../../../../shared_imports', () => ({ + ...jest.requireActual('../../../../shared_imports'), + useAsync: jest.fn(), +})); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); describe('When the add exception modal is opened', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 120c4ad8efc1b..6efbbcf64406b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -25,6 +25,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, } from '@elastic/eui'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -34,9 +35,9 @@ import { Status } from '../../../../../common/detection_engine/schemas/common/sc import { ExceptionListItemSchema, CreateExceptionListItemSchema, - ExceptionListType, ExceptionBuilder, } from '../../../../../public/shared_imports'; + import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 5fb52994fb0f5..6c68dcf934b71 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -22,6 +22,7 @@ import { EuiCallOut, } from '@elastic/eui'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -33,9 +34,9 @@ import { useRuleAsync } from '../../../../detections/containers/detection_engine import { ExceptionListItemSchema, CreateExceptionListItemSchema, - ExceptionListType, ExceptionBuilder, } from '../../../../../public/shared_imports'; + import * as i18n from './translations'; import * as sharedI18n from '../translations'; import { useKibana } from '../../../lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 907b30fcaa879..98c2b4db5676e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -10,13 +10,8 @@ import { mount } from 'enzyme'; import moment from 'moment-timezone'; import { - getOperatorType, - getExceptionOperatorSelect, getFormattedComments, - filterExceptionItems, - getNewExceptionItem, formatOperatingSystems, - getEntryValue, formatExceptionItemForUpdate, enrichNewExceptionItemsWithComments, enrichExistingExceptionItemWithComments, @@ -32,35 +27,19 @@ import { retrieveAlertOsTypes, filterIndexPatterns, } from './helpers'; -import { AlertData, EmptyEntry } from './types'; +import { AlertData } from './types'; import { - isOperator, - isNotOperator, - isOneOfOperator, - isNotOneOfOperator, - isInListOperator, - isNotInListOperator, - existsOperator, - doesNotExistOperator, -} from '../autocomplete/operators'; -import { OperatorTypeEnum, OperatorEnum, EntryNested } from '../../../shared_imports'; + ListOperatorTypeEnum as OperatorTypeEnum, + EntriesArray, + OsTypeArray, +} from '@kbn/securitysolution-io-ts-list-types'; + import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryMatchMock } from '../../../../../lists/common/schemas/types/entry_match.mock'; -import { getEntryMatchAnyMock } from '../../../../../lists/common/schemas/types/entry_match_any.mock'; -import { getEntryExistsMock } from '../../../../../lists/common/schemas/types/entry_exists.mock'; -import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comment.mock'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { - ENTRIES, - ENTRIES_WITH_IDS, - OLD_DATE_RELATIVE_TO_DATE_NOW, -} from '../../../../../lists/common/constants.mock'; -import { EntriesArray, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; -import { - CreateExceptionListItemSchema, - ExceptionListItemSchema, -} from '../../../../../lists/common/schemas'; +import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { IFieldType, IIndexPattern } from 'src/plugins/data/common'; jest.mock('uuid', () => ({ @@ -162,128 +141,6 @@ describe('Exception helpers', () => { }); }); - describe('#getOperatorType', () => { - test('returns operator type "match" if entry.type is "match"', () => { - const payload = getEntryMatchMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.MATCH); - }); - - test('returns operator type "match_any" if entry.type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.MATCH_ANY); - }); - - test('returns operator type "list" if entry.type is "list"', () => { - const payload = getEntryListMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.LIST); - }); - - test('returns operator type "exists" if entry.type is "exists"', () => { - const payload = getEntryExistsMock(); - const operatorType = getOperatorType(payload); - - expect(operatorType).toEqual(OperatorTypeEnum.EXISTS); - }); - }); - - describe('#getExceptionOperatorSelect', () => { - test('it returns "isOperator" when "operator" is "included" and operator type is "match"', () => { - const payload = getEntryMatchMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isOperator); - }); - - test('it returns "isNotOperator" when "operator" is "excluded" and operator type is "match"', () => { - const payload = getEntryMatchMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotOperator); - }); - - test('it returns "isOneOfOperator" when "operator" is "included" and operator type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isOneOfOperator); - }); - - test('it returns "isNotOneOfOperator" when "operator" is "excluded" and operator type is "match_any"', () => { - const payload = getEntryMatchAnyMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotOneOfOperator); - }); - - test('it returns "existsOperator" when "operator" is "included" and no operator type is provided', () => { - const payload = getEntryExistsMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(existsOperator); - }); - - test('it returns "doesNotExistsOperator" when "operator" is "excluded" and no operator type is provided', () => { - const payload = getEntryExistsMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(doesNotExistOperator); - }); - - test('it returns "isInList" when "operator" is "included" and operator type is "list"', () => { - const payload = getEntryListMock(); - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isInListOperator); - }); - - test('it returns "isNotInList" when "operator" is "excluded" and operator type is "list"', () => { - const payload = getEntryListMock(); - payload.operator = 'excluded'; - const result = getExceptionOperatorSelect(payload); - - expect(result).toEqual(isNotInListOperator); - }); - }); - - describe('#getEntryValue', () => { - it('returns "match" entry value', () => { - const payload = getEntryMatchMock(); - const result = getEntryValue(payload); - const expected = 'some host name'; - expect(result).toEqual(expected); - }); - - it('returns "match any" entry values', () => { - const payload = getEntryMatchAnyMock(); - const result = getEntryValue(payload); - const expected = ['some host name']; - expect(result).toEqual(expected); - }); - - it('returns "exists" entry value', () => { - const payload = getEntryExistsMock(); - const result = getEntryValue(payload); - const expected = undefined; - expect(result).toEqual(expected); - }); - - it('returns "list" entry value', () => { - const payload = getEntryListMock(); - const result = getEntryValue(payload); - const expected = 'some-list-id'; - expect(result).toEqual(expected); - }); - }); - describe('#formatOperatingSystems', () => { test('it returns null if no operating system tag specified', () => { const result = formatOperatingSystems(['some os', 'some other os']); @@ -324,178 +181,6 @@ describe('Exception helpers', () => { }); }); - describe('#filterExceptionItems', () => { - // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes - // for context around the temporary `id` - test('it correctly validates entries that include a temporary `id`', () => { - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }]); - }); - - test('it removes entry items with "value" of "undefined"', () => { - const { entries, ...rest } = getExceptionListItemSchemaMock(); - const mockEmptyException: EmptyEntry = { - id: '123', - field: 'host.name', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: undefined, - }; - const exceptions = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); - }); - - test('it removes "match" entry items with "value" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: 'host.name', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: '', - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "match" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: '', - type: OperatorTypeEnum.MATCH, - operator: OperatorEnum.INCLUDED, - value: 'some value', - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "match_any" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EmptyEntry = { - id: '123', - field: '', - type: OperatorTypeEnum.MATCH_ANY, - operator: OperatorEnum.INCLUDED, - value: ['some value'], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes "nested" entry items with "field" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: '', - type: OperatorTypeEnum.NESTED, - entries: [getEntryMatchMock()], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes the "nested" entry entries with "value" of empty string', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: 'host.name', - type: OperatorTypeEnum.NESTED, - entries: [getEntryMatchMock(), { ...getEntryMatchMock(), value: '' }], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([ - { - ...getExceptionListItemSchemaMock(), - entries: [ - ...getExceptionListItemSchemaMock().entries, - { ...mockEmptyException, entries: [getEntryMatchMock()] }, - ], - }, - ]); - }); - - test('it removes the "nested" entry item if all its entries are invalid', () => { - const { entries, ...rest } = { ...getExceptionListItemSchemaMock() }; - const mockEmptyException: EntryNested = { - field: 'host.name', - type: OperatorTypeEnum.NESTED, - entries: [{ ...getEntryMatchMock(), value: '' }], - }; - const output: Array< - ExceptionListItemSchema | CreateExceptionListItemSchema - > = filterExceptionItems([ - { - ...rest, - entries: [...entries, mockEmptyException], - }, - ]); - - expect(output).toEqual([{ ...getExceptionListItemSchemaMock() }]); - }); - - test('it removes `temporaryId` from items', () => { - const { meta, ...rest } = getNewExceptionItem({ - listId: '123', - namespaceType: 'single', - ruleName: 'rule name', - }); - const exceptions = filterExceptionItems([{ ...rest, meta }]); - - expect(exceptions).toEqual([{ ...rest, entries: [], meta: undefined }]); - }); - }); - describe('#formatExceptionItemForUpdate', () => { test('it should return correct update fields', () => { const payload = getExceptionListItemSchemaMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index ce76114309e2e..437e93bb26fef 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -9,46 +9,36 @@ import React from 'react'; import { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui'; import { capitalize } from 'lodash'; import moment from 'moment'; -import uuid from 'uuid'; -import * as i18n from './translations'; -import { - AlertData, - BuilderEntry, - CreateExceptionListItemBuilderSchema, - ExceptionsBuilderExceptionItem, - Flattened, -} from './types'; -import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; -import { OperatorOption } from '../autocomplete/types'; import { + comment, + osType, CommentsArray, Comment, CreateComment, Entry, - ExceptionListItemSchema, NamespaceType, - OperatorTypeEnum, - CreateExceptionListItemSchema, - comment, - entry, - entriesNested, - nestedEntryItem, - createExceptionListItemSchema, - exceptionListItemSchema, - UpdateExceptionListItemSchema, EntryNested, OsTypeArray, - EntriesArray, - osType, ExceptionListType, + ListOperatorTypeEnum as OperatorTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import * as i18n from './translations'; +import { AlertData, ExceptionsBuilderExceptionItem, Flattened } from './types'; +import { + ExceptionListItemSchema, + CreateExceptionListItemSchema, + UpdateExceptionListItemSchema, + getOperatorType, + getNewExceptionItem, + addIdToEntries, } from '../../../shared_imports'; + import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; -import { validate } from '../../../../common/validate'; import { Ecs } from '../../../../common/ecs'; import { CodeSignature } from '../../../../common/ecs/file'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; -import { addIdToItem, removeIdFromItem } from '../../../../common'; import exceptionableLinuxFields from './exceptionable_linux_fields.json'; import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json'; import exceptionableEndpointFields from './exceptionable_endpoint_fields.json'; @@ -84,75 +74,6 @@ export const filterIndexPatterns = ( } }; -export const addIdToEntries = (entries: EntriesArray): EntriesArray => { - return entries.map((singleEntry) => { - if (singleEntry.type === 'nested') { - return addIdToItem({ - ...singleEntry, - entries: singleEntry.entries.map((nestedEntry) => addIdToItem(nestedEntry)), - }); - } else { - return addIdToItem(singleEntry); - } - }); -}; - -/** - * Returns the operator type, may not need this if using io-ts types - * - * @param item a single ExceptionItem entry - */ -export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => { - switch (item.type) { - case 'match': - return OperatorTypeEnum.MATCH; - case 'match_any': - return OperatorTypeEnum.MATCH_ANY; - case 'list': - return OperatorTypeEnum.LIST; - default: - return OperatorTypeEnum.EXISTS; - } -}; - -/** - * Determines operator selection (is/is not/is one of, etc.) - * Default operator is "is" - * - * @param item a single ExceptionItem entry - */ -export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption => { - if (item.type === 'nested') { - return isOperator; - } else { - const operatorType = getOperatorType(item); - const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => { - return item.operator === operatorOption.operator && operatorType === operatorOption.type; - }); - - return foundOperator ?? isOperator; - } -}; - -/** - * Returns the fields corresponding value for an entry - * - * @param item a single ExceptionItem entry - */ -export const getEntryValue = (item: BuilderEntry): string | string[] | undefined => { - switch (item.type) { - case OperatorTypeEnum.MATCH: - case OperatorTypeEnum.MATCH_ANY: - return item.value; - case OperatorTypeEnum.EXISTS: - return undefined; - case OperatorTypeEnum.LIST: - return item.list.id; - default: - return undefined; - } -}; - /** * Formats os value array to a displayable string */ @@ -189,91 +110,6 @@ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] ), })); -export const getNewExceptionItem = ({ - listId, - namespaceType, - ruleName, -}: { - listId: string; - namespaceType: NamespaceType; - ruleName: string; -}): CreateExceptionListItemBuilderSchema => { - return { - comments: [], - description: `${ruleName} - exception list item`, - entries: addIdToEntries([ - { - field: '', - operator: 'included', - type: 'match', - value: '', - }, - ]), - item_id: undefined, - list_id: listId, - meta: { - temporaryUuid: uuid.v4(), - }, - name: `${ruleName} - exception list item`, - namespace_type: namespaceType, - tags: [], - type: 'simple', - }; -}; - -export const filterExceptionItems = ( - exceptions: ExceptionsBuilderExceptionItem[] -): Array => { - return exceptions.reduce>( - (acc, exception) => { - const entries = exception.entries.reduce((nestedAcc, singleEntry) => { - const strippedSingleEntry = removeIdFromItem(singleEntry); - - if (entriesNested.is(strippedSingleEntry)) { - const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => { - const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry); - const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem); - return validatedNestedEntry != null; - }); - const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) => - removeIdFromItem(singleNestedEntry) - ); - - const [validatedNestedEntry] = validate( - { ...strippedSingleEntry, entries: noIdNestedEntries }, - entriesNested - ); - - if (validatedNestedEntry != null) { - return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }]; - } - return nestedAcc; - } else { - const [validatedEntry] = validate(strippedSingleEntry, entry); - - if (validatedEntry != null) { - return [...nestedAcc, singleEntry]; - } - return nestedAcc; - } - }, []); - - const item = { ...exception, entries }; - - if (exceptionListItemSchema.is(item)) { - return [...acc, item]; - } else if (createExceptionListItemSchema.is(item)) { - const { meta, ...rest } = item; - const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; - return [...acc, itemSansMetaId]; - } else { - return acc; - } - }, - [] - ); -}; - export const formatExceptionItemForUpdate = ( exceptionItem: ExceptionListItemSchema ): UpdateExceptionListItemSchema => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 92a3cb2cfac93..49cdd7103c48b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -6,23 +6,22 @@ */ import { ReactNode } from 'react'; -import { Ecs } from '../../../../common/ecs'; -import { CodeSignature } from '../../../../common/ecs/file'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { +import type { EntryNested, Entry, EntryMatch, EntryMatchAny, EntryMatchWildcard, EntryExists, - ExceptionListItemSchema, - CreateExceptionListItemSchema, NamespaceType, - OperatorTypeEnum, - OperatorEnum, -} from '../../../lists_plugin_deps'; + ListOperatorTypeEnum as OperatorTypeEnum, + ListOperatorEnum as OperatorEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import { Ecs } from '../../../../common/ecs'; +import { CodeSignature } from '../../../../common/ecs/file'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; +import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../lists_plugin_deps'; export interface FormattedEntry { fieldName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index 0f6dd19ea9b66..f609acf9c6c63 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -27,6 +27,7 @@ import { ReturnUseAddOrUpdateException, AddOrUpdateExceptionItemsFunc, } from './use_add_exception'; +import { UpdateDocumentByQueryResponse } from 'elasticsearch'; const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -36,11 +37,9 @@ const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('useAddOrUpdateException', () => { - let updateAlertStatus: jest.SpyInstance>; - let addExceptionListItem: jest.SpyInstance>; - let updateExceptionListItem: jest.SpyInstance< - ReturnType - >; + let updateAlertStatus: jest.SpyInstance>; + let addExceptionListItem: jest.SpyInstance>; + let updateExceptionListItem: jest.SpyInstance>; let getQueryFilter: jest.SpyInstance>; let buildAlertStatusFilter: jest.SpyInstance< ReturnType diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 877f545b69d65..17237f4f94c61 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -12,7 +12,7 @@ import * as rulesApi from '../../../detections/containers/detection_engine/rules import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; -import { ExceptionListType } from '../../../lists_plugin_deps'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { ListArray } from '../../../../common/detection_engine/schemas/types'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { @@ -20,6 +20,7 @@ import { UseFetchOrCreateRuleExceptionListProps, ReturnUseFetchOrCreateRuleExceptionList, } from './use_fetch_or_create_rule_exception_list'; +import { ExceptionListSchema } from '../../../shared_imports'; const mockKibanaHttpService = coreMock.createStart().http; jest.mock('../../../detections/containers/detection_engine/rules/api'); @@ -31,7 +32,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { let addEndpointExceptionList: jest.SpyInstance< ReturnType >; - let fetchExceptionListById: jest.SpyInstance>; + let fetchExceptionListById: jest.SpyInstance>; let render: ( listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType'] ) => RenderHookResult< diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx index 8ded1b902f302..4f78b49ea266c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; addDecorator((storyFn) => ( ({ eui: euiLightVars, darkMode: false })}>{storyFn()} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx index b82a472befdcf..7dcd59069b53c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; describe('ExceptionsViewerHeader', () => { it('it renders all disabled if "isInitLoading" is true', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx index eff4368ef6809..8fc28ad89156d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx @@ -18,9 +18,9 @@ import { } from '@elastic/eui'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; import { Filter } from '../types'; -import { ExceptionListTypeEnum } from '../../../../../public/lists_plugin_deps'; interface ExceptionsViewerHeaderProps { isInitLoading: boolean; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index 29764625075d6..abd45cf2945cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -7,8 +7,14 @@ import moment from 'moment'; -import { entriesNested, ExceptionListItemSchema } from '../../../../lists_plugin_deps'; -import { getEntryValue, getExceptionOperatorSelect, formatOperatingSystems } from '../helpers'; +import { entriesNested } from '@kbn/securitysolution-io-ts-list-types'; +import { + ExceptionListItemSchema, + getEntryValue, + getExceptionOperatorSelect, +} from '../../../../lists_plugin_deps'; + +import { formatOperatingSystems } from '../helpers'; import { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index 3fe6497105af1..971b3fda47191 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -11,11 +11,9 @@ import { ThemeProvider } from 'styled-components'; import { ExceptionsViewer } from './'; import { useKibana } from '../../../../common/lib/kibana'; -import { - ExceptionListTypeEnum, - useExceptionListItems, - useApi, -} from '../../../../../public/lists_plugin_deps'; +import { useExceptionListItems, useApi } from '../../../../../public/lists_plugin_deps'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 8c4569ed29b33..da7607f40ab72 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -9,6 +9,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiSpacer } from '@elastic/eui'; import uuid from 'uuid'; +import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; import { useStateToaster } from '../../toasters'; import { useKibana } from '../../../../common/lib/kibana'; @@ -20,11 +21,11 @@ import { allExceptionItemsReducer, State, ViewerModalName } from './reducer'; import { useExceptionListItems, ExceptionListIdentifiers, - ExceptionListTypeEnum, ExceptionListItemSchema, UseExceptionListItemsSuccess, useApi, } from '../../../../../public/lists_plugin_deps'; + import { ExceptionsViewerPagination } from './exceptions_pagination'; import { ExceptionsViewerUtility } from './exceptions_utility'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts index 46ac19f47503d..bf8e454e9971f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { FilterOptions, ExceptionsPagination, @@ -12,7 +13,6 @@ import { Filter, } from '../types'; import { - ExceptionListType, ExceptionListItemSchema, ExceptionListIdentifiers, Pagination, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 69e41a2c3d0a2..3152c08fab323 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -19,6 +19,7 @@ import styled from 'styled-components'; import { getOr } from 'lodash/fp'; import { indexOf } from 'lodash'; +import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; @@ -44,7 +45,6 @@ import { } from '../../../../common/components/toasters'; import { inputsModel } from '../../../../common/store'; import { useUserData } from '../../user_info'; -import { ExceptionListType } from '../../../../../common/shared_imports'; import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 94cb22592f4ed..ea903882c326d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -18,7 +18,9 @@ import { EuiSelectOption, } from '@elastic/eui'; -import { useImportList, ListSchema, Type } from '../../../shared_imports'; +import type { Type } from '@kbn/securitysolution-io-ts-list-types'; +import { useImportList, ListSchema } from '../../../shared_imports'; + import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index d11ceee7f5978..64cb936f160f1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -11,8 +11,8 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { Spacer } from '../../../../../../common/components/page'; -import { NamespaceType } from '../../../../../../../../lists/common'; import { FormatUrl } from '../../../../../../common/components/link_to'; import { LinkAnchor } from '../../../../../../common/components/links'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 146b7e8470718..50cf1b1830fec 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -15,9 +15,9 @@ import { } from '@elastic/eui'; import { History } from 'history'; +import { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; -import { NamespaceType } from '../../../../../../../../lists/common'; import { useKibana } from '../../../../../../common/lib/kibana'; import { ExceptionListFilter, useApi, useExceptionLists } from '../../../../../../shared_imports'; import { FormatUrl } from '../../../../../../common/components/link_to'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 64dfac5787f23..29b63721513d4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -9,11 +9,12 @@ import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; import deepmerge from 'deepmerge'; +import type { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { ENDPOINT_LIST_ID, ExceptionListType, NamespaceType } from '../../../../../shared_imports'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { Rule } from '../../../../containers/detection_engine/rules'; import { Threats, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 0fab428ef6d1b..9660132147a57 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -28,6 +28,7 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { useDeepEqualSelector, useShallowEqualSelector, @@ -83,7 +84,8 @@ import { ExceptionsViewer } from '../../../../../common/components/exceptions/vi import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; -import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; +import type { ExceptionListIdentifiers } from '../../../../../shared_imports'; + import { focusUtilityBarAction, onTimelineTabKeyPressed, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts index 5d600f471994b..e1fa1107fcb01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts @@ -5,9 +5,8 @@ * 2.0. */ +import { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { - ExceptionListType, - ExceptionListTypeEnum, EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL, ENDPOINT_EVENT_FILTERS_LIST_ID, diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index e77c4a0eec486..76ec761d41703 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -33,23 +33,23 @@ export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/he export { exportList, - useIsMounted, useCursor, useApi, useAsync, useExceptionListItems, useExceptionLists, - usePersistExceptionItem, - usePersistExceptionList, useFindLists, useDeleteList, useImportList, useCreateListIndex, useReadListIndex, useReadListPrivileges, - addExceptionListItem, - updateExceptionListItem, fetchExceptionListById, + addIdToEntries, + getOperatorType, + getNewExceptionItem, + getEntryValue, + getExceptionOperatorSelect, addExceptionList, ExceptionListFilter, ExceptionListIdentifiers, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 786a74e91b51a..e4704523a16c3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -7,17 +7,19 @@ import uuid from 'uuid'; -import { OsType } from '../../../../../lists/common/schemas'; -import { +import type { EntriesArray, EntryMatch, EntryMatchWildcard, EntryNested, - ExceptionListItemSchema, NestedEntriesArray, -} from '../../../../../lists/common'; +} from '@kbn/securitysolution-io-ts-list-types'; + +import type { ExceptionListItemSchema } from '../../../../../lists/common'; + +import type { OsType } from '../../../../../lists/common/schemas'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { +import type { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, } from '../../../../../lists/server'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts index 3fa5d1178b3ec..578c1aba64558 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts @@ -11,7 +11,7 @@ import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { listMock } from '../../../../../../lists/server/mocks'; import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; -import { EntryList } from '../../../../../../lists/common'; +import { EntryList } from '@kbn/securitysolution-io-ts-list-types'; import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; describe('filterEventsAgainstList', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts index b2002dbb5a7e2..40322029c1d98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntryList, entriesList } from '../../../../../../lists/common'; +import { EntryList, entriesList } from '@kbn/securitysolution-io-ts-list-types'; import { createSetToFilterAgainst } from './create_set_to_filter_against'; import { CreateFieldAndSetTuplesOptions, FieldSet } from './types'; From e4b4fd47316ff996f2a01d0a1275fc17906bcfa2 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Sat, 15 May 2021 01:10:53 -0400 Subject: [PATCH 072/186] [App Search] Allow user to manage source engines through Kibana UX (#98866) * New bulk create route for meta engine source engines * New delete route for meta engine source engines * Add removeSourceEngine and onSourceEngineRemove to SourceEnginesLogicActions * New SourceEnginesTable component * Use new SourceEnginesTable component in SourceEngines view * Added closeAddSourceEnginesModal and openAddSourceEnginesModal to SourceEnginesLogic * New AddSourceEnginesModal component * New AddSourceEnginesButton component * Add AddSourceEnginesButton and AddSourceEnginesModal to SourceEngines view * Allow user to select source engines to add * Add addSourceEngines and onSourceEnginesAdd to SourceEnginesLogic * Submit new source engines when user saves from inside AddSourceEnginesModal * Fix failing tests * fix i18n * Fix imports * Use body instead of query params for source engines bulk create endpoint * Tests for SouceEnginesLogic actions setIndexedEngines and fetchIndexedEngines * Re-enabling two skipped tests * Feedback: move source engine APIs to own file - We generally organize routes/logic etc. by view, and since this is its own view, it can get its own file * Misc UI polish Table: - Add EuiPageContent bordered panel (matches Curations & API logs which is a table in a panel) - Remove bolding on engine name (matches rest of Kibana UI) - Remove responsive false (we do want responsive tables in Kibana) Modal: - Remove EuiOverlayMask - per recent EUI changes, this now comes baked in with EuiModal - Change description text to subdued to match other modals (e.g. Curations queries) in Kibana * Misc i18n/copy tweaks Modal: - Add combobox placeholder text - i18n cancel/save buttons - inline i18n and change title casing to sentence casing * Table refactors - DRY out table columns shared with the main engines tables (title & formatting change slightly from the standalone UI, but this is fine / we should prefer Kibana standardization moving forward) - Actions column changes - Give it a name - axe will throw issues for table column missing headings - Do not make actions a conditional empty array - we should opt to remove the column totally if there is no content present, otherwise screen readers will read out blank cells unnecessarily - Switch to icons w/ description tooltips to match the other Kibana tables - Remove unnecessary sorting props (we don't have sorting enabled on any columns) Tests - Add describe block for organization - Add missing coverage for window confirm branch and canManageMetaEngineSourceEngines branch * Modal test fixes - Remove unnecessary type casting - Remove commented out line - Fix missing onChange function coverage * Modal: move unmemoized array iterations to Kea selectors - more performant: kea selectors are memoized - cleaner/less logic in views - easier to write unit tests for + rename setSelectedEngineNamesToAdd to onAddEnginesSelection + remove unused selectors test code * Modal: Add isLoading UX to submit button + value renames - isLoading prevents double clicks/dupe events, and also provides a responsive UX hint that something is happening - Var renames: there's only one modal on the page, being extra specific with the name isn't really necessary. If we ever add more than one to this view it would probably make sense to split up the logic files or do something else. Verbose modal names/states shouldn't necessarily be the answer * Source Engines view test fixes - Remove unused mock values/actions - Move constants to within main describe - Remove unhappy vs happy path describes - there aren't enough of either scenario to warrant the distinction - add page actions describe block and fix skipped/mounted test by shallow diving into EuiPageHeader * [Misc] Single components/index.ts export For easier group importing * Move all copy consts/strings to their own i18n constants file * Refactor recursive fetchEngines fn to shared util + update MetaEnginesTableLogic to use new helper/DRY out code + write unit tests for just that helper + simplify other previous logic checks to just check that the fn was called + add mock * Tests cleanup - Move consts into top of describe blocks to match rest of codebase - Remove logic comments for files that are only sourcing 1 logic file - Modal: - shallow is fairly cheap and it's easier / more consistent w/ other tests to start a new wrapper every test - Logic: - Remove unnecessarily EnginesLogic mocks - Remove mount() in beforeEach - it doesn't save us that many extra lines / better to be more consistent when starting tests that mount with values vs not - mock clearing in beforeEach to match rest of codebase - describe blocks: split up actions vs listeners, move selectors between the two - actions: fix tests that are in a describe() but not an it() (incorrect syntax) - Reducer/value checks: check against entire values obj to check for regressions or untested reducers & be consistent rest of codebase - listeners - DRY out beforeEach of success vs error paths, combine some tests that are a bit repetitive vs just having multiple assertions - Logic comments: - Remove unnecessary comments (if we're not setting a response, it seems clear we're not using it) - Add extra business logic context explanation as to why we call re-initialize the engine Co-authored-by: Constance Chen --- .../app_search/__mocks__/index.ts | 1 + .../recursively_fetch_engines.mock.ts | 21 + .../tables/meta_engines_table_logic.test.ts | 101 +---- .../tables/meta_engines_table_logic.ts | 41 +- .../add_source_engines_button.test.tsx | 35 ++ .../components/add_source_engines_button.tsx | 25 ++ .../add_source_engines_modal.test.tsx | 103 +++++ .../components/add_source_engines_modal.tsx | 68 +++ .../source_engines/components/index.ts | 10 + .../components/source_engines_table.test.tsx | 83 ++++ .../components/source_engines_table.tsx | 75 ++++ .../components/source_engines/i18n.ts | 67 +++ .../source_engines/source_engines.test.tsx | 80 +++- .../source_engines/source_engines.tsx | 32 +- .../source_engines_logic.test.ts | 423 ++++++++++++++---- .../source_engines/source_engines_logic.ts | 164 +++++-- .../recursively_fetch_engines/index.test.ts | 108 +++++ .../utils/recursively_fetch_engines/index.ts | 54 +++ .../server/routes/app_search/engines.test.ts | 43 -- .../server/routes/app_search/engines.ts | 17 - .../server/routes/app_search/index.ts | 2 + .../routes/app_search/source_engines.test.ts | 151 +++++++ .../routes/app_search/source_engines.ts | 65 +++ 23 files changed, 1430 insertions(+), 339 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts index 271a09849cba7..b444c1cc94383 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/index.ts @@ -6,3 +6,4 @@ */ export { mockEngineValues, mockEngineActions } from './engine_logic.mock'; +export { mockRecursivelyFetchEngines, mockSourceEngines } from './recursively_fetch_engines.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts new file mode 100644 index 0000000000000..dd4c86a2a6360 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/recursively_fetch_engines.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EngineDetails } from '../components/engine/types'; + +export const mockSourceEngines = [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, +] as EngineDetails[]; + +export const mockRecursivelyFetchEngines = jest.fn(({ onComplete }) => + onComplete(mockSourceEngines) +); + +jest.mock('../utils/recursively_fetch_engines', () => ({ + recursivelyFetchEngines: mockRecursivelyFetchEngines, +})); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts index b90207331ffd6..de1902c7cf748 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; - -import { nextTick } from '@kbn/test/jest'; +import { LogicMounter } from '../../../../../__mocks__'; +import { mockRecursivelyFetchEngines } from '../../../../__mocks__/recursively_fetch_engines.mock'; import { EngineDetails } from '../../../engine/types'; import { MetaEnginesTableLogic } from './meta_engines_table_logic'; describe('MetaEnginesTableLogic', () => { + const { mount } = new LogicMounter(MetaEnginesTableLogic); + const DEFAULT_VALUES = { expandedRows: {}, sourceEngines: {}, @@ -44,15 +45,11 @@ describe('MetaEnginesTableLogic', () => { metaEngines: [...SOURCE_ENGINES, ...META_ENGINES] as EngineDetails[], }; - const { http } = mockHttpValues; - const { mount } = new LogicMounter(MetaEnginesTableLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; - beforeEach(() => { jest.clearAllMocks(); }); - it('has expected default values', async () => { + it('has expected default values', () => { mount({}, DEFAULT_PROPS); expect(MetaEnginesTableLogic.values).toEqual(DEFAULT_VALUES); }); @@ -122,16 +119,6 @@ describe('MetaEnginesTableLogic', () => { }); it('calls fetchSourceEngines when it needs to fetch data for the itemId', () => { - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); mount(); jest.spyOn(MetaEnginesTableLogic.actions, 'fetchSourceEngines'); @@ -142,88 +129,22 @@ describe('MetaEnginesTableLogic', () => { }); describe('fetchSourceEngines', () => { - it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => { + it('calls addSourceEngines and displayRow when it has retrieved all pages', () => { mount(); - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); jest.spyOn(MetaEnginesTableLogic.actions, 'displayRow'); jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines'); MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith( - '/api/app_search/engines/test-engine-1/source_engines', - { - query: { - 'page[current]': 1, - 'page[size]': 25, - }, - } - ); - expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({ - 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }); - expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1'); - }); - - it('display a flash message on error', async () => { - http.get.mockReturnValueOnce(Promise.reject()); - mount(); - MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledTimes(1); - }); - - it('recursively fetches a number of pages', async () => { - mount(); - jest.spyOn(MetaEnginesTableLogic.actions, 'addSourceEngines'); - - // First page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-1' }], - }) - ); - - // Second and final page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-2' }], + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines/test-engine-1/source_engines', }) ); - - MetaEnginesTableLogic.actions.fetchSourceEngines('test-engine-1'); - await nextTick(); - expect(MetaEnginesTableLogic.actions.addSourceEngines).toHaveBeenCalledWith({ - 'test-engine-1': [ - // First page - { name: 'source-engine-1' }, - // Second and final page - { name: 'source-engine-2' }, - ], + 'test-engine-1': [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], }); + expect(MetaEnginesTableLogic.actions.displayRow).toHaveBeenCalledWith('test-engine-1'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts index 3a4c7d51c50a9..af4d0119a94af 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table_logic.ts @@ -7,10 +7,8 @@ import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../../../../shared/flash_messages'; -import { HttpLogic } from '../../../../../shared/http'; +import { recursivelyFetchEngines } from '../../../../utils/recursively_fetch_engines'; import { EngineDetails } from '../../../engine/types'; -import { EnginesAPIResponse } from '../../types'; interface MetaEnginesTableValues { expandedRows: { [id: string]: boolean }; @@ -85,36 +83,13 @@ export const MetaEnginesTableLogic = kea< } }, fetchSourceEngines: ({ engineName }) => { - const { http } = HttpLogic.values; - - let enginesAccumulator: EngineDetails[] = []; - - const recursiveFetchSourceEngines = async (page = 1) => { - try { - const { meta, results }: EnginesAPIResponse = await http.get( - `/api/app_search/engines/${engineName}/source_engines`, - { - query: { - 'page[current]': page, - 'page[size]': 25, - }, - } - ); - - enginesAccumulator = [...enginesAccumulator, ...results]; - - if (page >= meta.page.total_pages) { - actions.addSourceEngines({ [engineName]: enginesAccumulator }); - actions.displayRow(engineName); - } else { - recursiveFetchSourceEngines(page + 1); - } - } catch (e) { - flashAPIErrors(e); - } - }; - - recursiveFetchSourceEngines(); + recursivelyFetchEngines({ + endpoint: `/api/app_search/engines/${engineName}/source_engines`, + onComplete: (sourceEngines) => { + actions.addSourceEngines({ [engineName]: sourceEngines }); + actions.displayRow(engineName); + }, + }); }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx new file mode 100644 index 0000000000000..43a4682849c78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { AddSourceEnginesButton } from './add_source_engines_button'; + +describe('AddSourceEnginesButton', () => { + const MOCK_ACTIONS = { + openModal: jest.fn(), + }; + + it('opens the modal on click', () => { + setMockActions(MOCK_ACTIONS); + + const wrapper = shallow(); + const button = wrapper.find(EuiButton); + + expect(button).toHaveLength(1); + + button.simulate('click'); + + expect(MOCK_ACTIONS.openModal).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx new file mode 100644 index 0000000000000..004217d88987b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton } from '@elastic/eui'; + +import { ADD_SOURCE_ENGINES_BUTTON_LABEL } from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const AddSourceEnginesButton: React.FC = () => { + const { openModal } = useActions(SourceEnginesLogic); + + return ( + + {ADD_SOURCE_ENGINES_BUTTON_LABEL} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx new file mode 100644 index 0000000000000..19c2f72ed6f52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiButtonEmpty, EuiComboBox, EuiModal } from '@elastic/eui'; + +import { AddSourceEnginesModal } from './add_source_engines_modal'; + +describe('AddSourceEnginesModal', () => { + const MOCK_VALUES = { + selectableEngineNames: ['source-engine-1', 'source-engine-2', 'source-engine-3'], + selectedEngineNamesToAdd: ['source-engine-2'], + modalLoading: false, + }; + + const MOCK_ACTIONS = { + addSourceEngines: jest.fn(), + closeModal: jest.fn(), + onAddEnginesSelection: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(MOCK_VALUES); + setMockActions(MOCK_ACTIONS); + }); + + it('calls closeAddSourceEnginesModal when the modal is closed', () => { + const wrapper = shallow(); + wrapper.find(EuiModal).simulate('close'); + + expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled(); + }); + + describe('combo box', () => { + it('has the proper options and selected options', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiComboBox).prop('options')).toEqual([ + { label: 'source-engine-1' }, + { label: 'source-engine-2' }, + { label: 'source-engine-3' }, + ]); + expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([ + { label: 'source-engine-2' }, + ]); + }); + + it('calls setSelectedEngineNamesToAdd when changed', () => { + const wrapper = shallow(); + wrapper.find(EuiComboBox).simulate('change', [{ label: 'source-engine-3' }]); + + expect(MOCK_ACTIONS.onAddEnginesSelection).toHaveBeenCalledWith(['source-engine-3']); + }); + }); + + describe('cancel button', () => { + it('calls closeModal when clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonEmpty).simulate('click'); + + expect(MOCK_ACTIONS.closeModal).toHaveBeenCalled(); + }); + }); + + describe('save button', () => { + it('is disabled when user has selected no engines', () => { + setMockValues({ + ...MOCK_VALUES, + selectedEngineNamesToAdd: [], + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true); + }); + + it('passes modalLoading state', () => { + setMockValues({ + ...MOCK_VALUES, + modalLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiButton).prop('isLoading')).toEqual(true); + }); + + it('calls addSourceEngines when clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(MOCK_ACTIONS.addSourceEngines).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx new file mode 100644 index 0000000000000..24e27e03818ad --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_modal.tsx @@ -0,0 +1,68 @@ +/* + * 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 React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiComboBox, + EuiModalFooter, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../shared/constants'; + +import { + ADD_SOURCE_ENGINES_MODAL_TITLE, + ADD_SOURCE_ENGINES_MODAL_DESCRIPTION, + ADD_SOURCE_ENGINES_PLACEHOLDER, +} from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const AddSourceEnginesModal: React.FC = () => { + const { addSourceEngines, closeModal, onAddEnginesSelection } = useActions(SourceEnginesLogic); + const { selectableEngineNames, selectedEngineNamesToAdd, modalLoading } = useValues( + SourceEnginesLogic + ); + + return ( + + + {ADD_SOURCE_ENGINES_MODAL_TITLE} + + + {ADD_SOURCE_ENGINES_MODAL_DESCRIPTION} + + ({ label: engineName }))} + selectedOptions={selectedEngineNamesToAdd.map((engineName) => ({ label: engineName }))} + onChange={(options) => onAddEnginesSelection(options.map((option) => option.label))} + placeholder={ADD_SOURCE_ENGINES_PLACEHOLDER} + /> + + + {CANCEL_BUTTON_LABEL} + addSourceEngines(selectedEngineNamesToAdd)} + fill + > + {SAVE_BUTTON_LABEL} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts new file mode 100644 index 0000000000000..edec07a70a0bf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AddSourceEnginesButton } from './add_source_engines_button'; +export { AddSourceEnginesModal } from './add_source_engines_modal'; +export { SourceEnginesTable } from './source_engines_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx new file mode 100644 index 0000000000000..895c7ab35e86a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 { mountWithIntl, setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui'; + +import { SourceEnginesTable } from './source_engines_table'; + +describe('SourceEnginesTable', () => { + const MOCK_VALUES = { + // AppLogic + myRole: { + canManageMetaEngineSourceEngines: true, + }, + // SourceEnginesLogic + sourceEngines: [{ name: 'source-engine-1', document_count: 15, field_count: 26 }], + }; + + const MOCK_ACTIONS = { + removeSourceEngine: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + }); + + it('contains relevant informatiom from source engines', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiInMemoryTable).text()).toContain('source-engine-1'); + expect(wrapper.find(EuiInMemoryTable).text()).toContain('15'); + expect(wrapper.find(EuiInMemoryTable).text()).toContain('26'); + }); + + describe('actions column', () => { + it('clicking a remove engine link calls a confirm dialogue before remove the engine', () => { + const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + const wrapper = mountWithIntl(); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(confirmSpy).toHaveBeenCalled(); + expect(MOCK_ACTIONS.removeSourceEngine).toHaveBeenCalled(); + }); + + it('does not remove an engine if the user cancels the confirmation dialog', () => { + const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValueOnce(false); + const wrapper = mountWithIntl(); + + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(confirmSpy).toHaveBeenCalled(); + expect(MOCK_ACTIONS.removeSourceEngine).not.toHaveBeenCalled(); + }); + + it('does not render the actions column if the user does not have permission to manage the engine', () => { + setMockValues({ + ...MOCK_VALUES, + myRole: { canManageMetaEngineSourceEngines: false }, + }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx new file mode 100644 index 0000000000000..f8c3e3ca00c95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/source_engines_table.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import { ENGINE_PATH } from '../../../routes'; +import { generateEncodedPath } from '../../../utils/encode_path_params'; +import { EngineDetails } from '../../engine/types'; +import { + NAME_COLUMN, + DOCUMENT_COUNT_COLUMN, + FIELD_COUNT_COLUMN, + ACTIONS_COLUMN, +} from '../../engines/components/tables/shared_columns'; + +import { REMOVE_SOURCE_ENGINE_BUTTON_LABEL, REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE } from '../i18n'; +import { SourceEnginesLogic } from '../source_engines_logic'; + +export const SourceEnginesTable: React.FC = () => { + const { + myRole: { canManageMetaEngineSourceEngines }, + } = useValues(AppLogic); + + const { removeSourceEngine } = useActions(SourceEnginesLogic); + const { sourceEngines } = useValues(SourceEnginesLogic); + + const columns: Array> = [ + { + ...NAME_COLUMN, + render: (engineName: string) => ( + {engineName} + ), + }, + DOCUMENT_COUNT_COLUMN, + FIELD_COUNT_COLUMN, + ]; + if (canManageMetaEngineSourceEngines) { + columns.push({ + name: ACTIONS_COLUMN.name, + actions: [ + { + name: REMOVE_SOURCE_ENGINE_BUTTON_LABEL, + description: REMOVE_SOURCE_ENGINE_BUTTON_LABEL, + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (engine: EngineDetails) => { + if (confirm(REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE(engine.name))) { + removeSourceEngine(engine.name); + } + }, + }, + ], + }); + } + + return ( + 10} + search={{ box: { incremental: true } }} + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts new file mode 100644 index 0000000000000..4e3f4f81d5a9f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/i18n.ts @@ -0,0 +1,67 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const SOURCE_ENGINES_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title', + { defaultMessage: 'Manage engines' } +); + +export const ADD_SOURCE_ENGINES_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesButtonLabel', + { defaultMessage: 'Add engines' } +); + +export const ADD_SOURCE_ENGINES_MODAL_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.title', + { defaultMessage: 'Add engines' } +); + +export const ADD_SOURCE_ENGINES_MODAL_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesModal.description', + { defaultMessage: 'Add additional engines to this meta engine.' } +); + +export const ADD_SOURCE_ENGINES_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesPlaceholder', + { defaultMessage: 'Select engine(s)' } +); + +export const ADD_SOURCE_ENGINES_SUCCESS_MESSAGE = (sourceEngineNames: string[]) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.addSourceEnginesSuccessMessage', + { + defaultMessage: + '{sourceEnginesCount, plural, one {# engine has} other {# engines have}} been added to this meta engine.', + values: { sourceEnginesCount: sourceEngineNames.length }, + } + ); + +export const REMOVE_SOURCE_ENGINE_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineButton.label', + { defaultMessage: 'Remove from meta engine' } +); + +export const REMOVE_SOURCE_ENGINE_CONFIRM_DIALOGUE = (engineName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.sourceEngines.removeEngineConfirmDialogue.description', + { + defaultMessage: + 'This will remove the engine, {engineName}, from this meta engine. All existing settings will be lost. Are you sure?', + values: { engineName }, + } + ); + +export const REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE = (engineName: string) => + i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.souceEngines.removeSourceEngineSuccessMessage', + { + defaultMessage: 'Engine {engineName} has been removed from this meta engine.', + values: { engineName }, + } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx index 4bf62de408a2b..8cfcaeec97b87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx @@ -5,52 +5,88 @@ * 2.0. */ -import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../__mocks__'; +import '../../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiCodeBlock } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; +import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; + import { SourceEngines } from '.'; -const MOCK_ACTIONS = { - // SourceEnginesLogic - fetchSourceEngines: jest.fn(), -}; +describe('SourceEngines', () => { + const MOCK_ACTIONS = { + fetchIndexedEngines: jest.fn(), + fetchSourceEngines: jest.fn(), + }; -const MOCK_VALUES = { - dataLoading: false, - sourceEngines: [], -}; + const MOCK_VALUES = { + // AppLogic + myRole: { + canManageMetaEngineSourceEngines: true, + }, + // SourceEnginesLogic + dataLoading: false, + isModalOpen: false, + }; -describe('SourceEngines', () => { beforeEach(() => { jest.clearAllMocks(); + setMockValues(MOCK_VALUES); setMockActions(MOCK_ACTIONS); }); - describe('non-happy-path states', () => { - it('renders a loading component before data has loaded', () => { - setMockValues({ ...MOCK_VALUES, dataLoading: true }); - const wrapper = shallow(); + it('renders and calls a function to initialize data', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceEnginesTable)).toHaveLength(1); + expect(MOCK_ACTIONS.fetchIndexedEngines).toHaveBeenCalled(); + expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled(); + }); - expect(wrapper.find(Loading)).toHaveLength(1); + it('renders the add source engines modal', () => { + setMockValues({ + ...MOCK_VALUES, + isModalOpen: true, }); + const wrapper = shallow(); + + expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1); + }); + + it('renders a loading component before data has loaded', () => { + setMockValues({ ...MOCK_VALUES, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); }); - describe('happy-path states', () => { - it('renders and calls a function to initialize data', () => { - setMockValues(MOCK_VALUES); + describe('page actions', () => { + const getPageHeader = (wrapper: ShallowWrapper) => + wrapper.find(EuiPageHeader).dive().children().dive(); + + it('contains a button to add source engines', () => { + const wrapper = shallow(); + expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); + }); + + it('hides the add source engines button if the user does not have permissions', () => { + setMockValues({ + ...MOCK_VALUES, + myRole: { + canManageMetaEngineSourceEngines: false, + }, + }); const wrapper = shallow(); - expect(wrapper.find(EuiCodeBlock)).toHaveLength(1); - expect(MOCK_ACTIONS.fetchSourceEngines).toHaveBeenCalled(); + expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx index 0b68eb5fd2c2e..190c44c919020 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx @@ -9,29 +9,27 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiCodeBlock, EuiPageHeader } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; +import { EuiPageHeader, EuiPageContent } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Loading } from '../../../shared/loading'; +import { AppLogic } from '../../app_logic'; import { getEngineBreadcrumbs } from '../engine'; +import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; +import { SOURCE_ENGINES_TITLE } from './i18n'; import { SourceEnginesLogic } from './source_engines_logic'; -const SOURCE_ENGINES_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.souceEngines.title', - { - defaultMessage: 'Manage engines', - } -); - export const SourceEngines: React.FC = () => { - const { fetchSourceEngines } = useActions(SourceEnginesLogic); - const { dataLoading, sourceEngines } = useValues(SourceEnginesLogic); + const { + myRole: { canManageMetaEngineSourceEngines }, + } = useValues(AppLogic); + const { fetchIndexedEngines, fetchSourceEngines } = useActions(SourceEnginesLogic); + const { dataLoading, isModalOpen } = useValues(SourceEnginesLogic); useEffect(() => { + fetchIndexedEngines(); fetchSourceEngines(); }, []); @@ -40,9 +38,15 @@ export const SourceEngines: React.FC = () => { return ( <> - + ] : []} + /> - {JSON.stringify(sourceEngines, null, 2)} + + + {isModalOpen && } + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts index df1165620adc3..49886f1257a58 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts @@ -6,129 +6,372 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; +import { mockRecursivelyFetchEngines } from '../../__mocks__'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { EngineLogic } from '../engine'; import { EngineDetails } from '../engine/types'; import { SourceEnginesLogic } from './source_engines_logic'; -const DEFAULT_VALUES = { - dataLoading: true, - sourceEngines: [], -}; - describe('SourceEnginesLogic', () => { const { http } = mockHttpValues; const { mount } = new LogicMounter(SourceEnginesLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; + const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + dataLoading: true, + modalLoading: false, + isModalOpen: false, + indexedEngines: [], + indexedEngineNames: [], + sourceEngines: [], + sourceEngineNames: [], + selectedEngineNamesToAdd: [], + selectableEngineNames: [], + }; beforeEach(() => { jest.clearAllMocks(); - mount(); }); it('initializes with default values', () => { + mount(); expect(SourceEnginesLogic.values).toEqual(DEFAULT_VALUES); }); - describe('setSourceEngines', () => { - beforeEach(() => { - SourceEnginesLogic.actions.onSourceEnginesFetch([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ] as EngineDetails[]); + describe('actions', () => { + describe('closeModal', () => { + it('sets isModalOpen and modalLoading to false', () => { + mount({ + isModalOpen: true, + modalLoading: true, + }); + + SourceEnginesLogic.actions.closeModal(); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + isModalOpen: false, + modalLoading: false, + }); + }); }); - it('sets the source engines', () => { - expect(SourceEnginesLogic.values.sourceEngines).toEqual([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ]); + describe('openModal', () => { + it('sets isModalOpen to true', () => { + mount({ + isModalOpen: false, + }); + + SourceEnginesLogic.actions.openModal(); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + isModalOpen: true, + }); + }); }); - it('sets dataLoading to false', () => { - expect(SourceEnginesLogic.values.dataLoading).toEqual(false); + describe('onAddEnginesSelection', () => { + it('sets selectedEngineNamesToAdd to the specified value', () => { + mount(); + + SourceEnginesLogic.actions.onAddEnginesSelection(['source-engine-1', 'source-engine-2']); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + selectedEngineNamesToAdd: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('setIndexedEngines', () => { + it('sets indexedEngines to the specified value', () => { + mount(); + + SourceEnginesLogic.actions.setIndexedEngines([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + indexedEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + // Selectors + indexedEngineNames: ['source-engine-1', 'source-engine-2'], + selectableEngineNames: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('onSourceEnginesFetch', () => { + it('sets sourceEngines to the specified value and dataLoading to false', () => { + mount(); + + SourceEnginesLogic.actions.onSourceEnginesFetch([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + // Selectors + sourceEngineNames: ['source-engine-1', 'source-engine-2'], + }); + }); + }); + + describe('onSourceEnginesAdd', () => { + it('adds to the existing sourceEngines', () => { + mount({ + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ] as EngineDetails[], + }); + + SourceEnginesLogic.actions.onSourceEnginesAdd([ + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ] as EngineDetails[]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ], + // Selectors + sourceEngineNames: [ + 'source-engine-1', + 'source-engine-2', + 'source-engine-3', + 'source-engine-4', + ], + }); + }); + }); + + describe('onSourceEngineRemove', () => { + it('removes an item from the existing sourceEngines', () => { + mount({ + sourceEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + ] as EngineDetails[], + }); + + SourceEnginesLogic.actions.onSourceEngineRemove('source-engine-2'); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + sourceEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-3' }], + // Selectors + sourceEngineNames: ['source-engine-1', 'source-engine-3'], + }); + }); }); }); - describe('fetchSourceEngines', () => { - it('calls addSourceEngines and displayRow when it has retrieved all pages', async () => { - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 1, - }, - }, - results: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - }) - ); - jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); - - expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', { - query: { - 'page[current]': 1, - 'page[size]': 25, - }, - }); - expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ]); + describe('selectors', () => { + describe('indexedEngineNames', () => { + it('returns a flat array of `indexedEngine.name`s', () => { + mount({ + indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + }); + + expect(SourceEnginesLogic.values.indexedEngineNames).toEqual(['a', 'b', 'c']); + }); }); - it('display a flash message on error', async () => { - http.get.mockReturnValueOnce(Promise.reject()); - mount(); + describe('sourceEngineNames', () => { + it('returns a flat array of `sourceEngine.name`s', () => { + mount({ + sourceEngines: [{ name: 'd' }, { name: 'e' }], + }); + + expect(SourceEnginesLogic.values.sourceEngineNames).toEqual(['d', 'e']); + }); + }); - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); + describe('selectableEngineNames', () => { + it('returns a flat list of indexedEngineNames that are not already in sourceEngineNames', () => { + mount({ + indexedEngines: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + sourceEngines: [{ name: 'a' }, { name: 'b' }], + }); - expect(flashAPIErrors).toHaveBeenCalledTimes(1); + expect(SourceEnginesLogic.values.selectableEngineNames).toEqual(['c']); + }); }); + }); + + describe('listeners', () => { + describe('fetchSourceEngines', () => { + it('calls onSourceEnginesFetch with all recursively fetched engines', () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - it('recursively fetches a number of pages', async () => { - mount(); - jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesFetch'); - - // First page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-1' }], - }) - ); - - // Second and final page - http.get.mockReturnValueOnce( - Promise.resolve({ - meta: { - page: { - total_pages: 2, - }, - }, - results: [{ name: 'source-engine-2' }], - }) - ); - - SourceEnginesLogic.actions.fetchSourceEngines(); - await nextTick(); - - expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ - // First page - { name: 'source-engine-1' }, - // Second and final page - { name: 'source-engine-2' }, - ]); + SourceEnginesLogic.actions.fetchSourceEngines(); + + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines/some-engine/source_engines', + }) + ); + expect(SourceEnginesLogic.actions.onSourceEnginesFetch).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ]); + }); + }); + + describe('fetchIndexedEngines', () => { + it('calls setIndexedEngines with all recursively fetched engines', () => { + jest.spyOn(SourceEnginesLogic.actions, 'setIndexedEngines'); + + SourceEnginesLogic.actions.fetchIndexedEngines(); + + expect(mockRecursivelyFetchEngines).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: '/api/app_search/engines', + query: { type: 'indexed' }, + }) + ); + expect(SourceEnginesLogic.actions.setIndexedEngines).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + ]); + }); + }); + + describe('addSourceEngines', () => { + it('sets modalLoading to true', () => { + mount({ modalLoading: false }); + + SourceEnginesLogic.actions.addSourceEngines([]); + + expect(SourceEnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + modalLoading: true, + }); + }); + + describe('on success', () => { + beforeEach(() => { + http.post.mockReturnValue(Promise.resolve()); + mount({ + indexedEngines: [{ name: 'source-engine-3' }, { name: 'source-engine-4' }], + }); + }); + + it('calls the bulk endpoint, adds source engines to state, and shows a success message', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEnginesAdd'); + + SourceEnginesLogic.actions.addSourceEngines(['source-engine-3', 'source-engine-4']); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/source_engines/bulk_create', + { + body: JSON.stringify({ source_engine_slugs: ['source-engine-3', 'source-engine-4'] }), + } + ); + expect(SourceEnginesLogic.actions.onSourceEnginesAdd).toHaveBeenCalledWith([ + { name: 'source-engine-3' }, + { name: 'source-engine-4' }, + ]); + expect(setSuccessMessage).toHaveBeenCalledWith( + '2 engines have been added to this meta engine.' + ); + }); + + it('re-initializes the engine and closes the modal', async () => { + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + jest.spyOn(SourceEnginesLogic.actions, 'closeModal'); + + SourceEnginesLogic.actions.addSourceEngines([]); + await nextTick(); + + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalled(); + expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled(); + }); + }); + + describe('on error', () => { + beforeEach(() => { + http.post.mockReturnValue(Promise.reject()); + mount(); + }); + + it('flashes errors and closes the modal', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'closeModal'); + + SourceEnginesLogic.actions.addSourceEngines([]); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + expect(SourceEnginesLogic.actions.closeModal).toHaveBeenCalled(); + }); + }); + }); + + describe('removeSourceEngine', () => { + describe('on success', () => { + beforeEach(() => { + http.delete.mockReturnValue(Promise.resolve()); + mount(); + }); + + it('calls the delete endpoint and removes source engines from state', async () => { + jest.spyOn(SourceEnginesLogic.actions, 'onSourceEngineRemove'); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(http.delete).toHaveBeenCalledWith( + '/api/app_search/engines/some-engine/source_engines/source-engine-2' + ); + expect(SourceEnginesLogic.actions.onSourceEngineRemove).toHaveBeenCalledWith( + 'source-engine-2' + ); + }); + + it('shows a success message', async () => { + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(setSuccessMessage).toHaveBeenCalledWith( + 'Engine source-engine-2 has been removed from this meta engine.' + ); + }); + + it('re-initializes the engine', async () => { + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalledWith(); + }); + }); + + it('displays a flash message on error', async () => { + http.delete.mockReturnValueOnce(Promise.reject()); + mount(); + + SourceEnginesLogic.actions.removeSourceEngine('source-engine-2'); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts index b8a5c7c359518..c10f11a7de327 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts @@ -4,24 +4,47 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { kea, MakeLogicType } from 'kea'; -import { flashAPIErrors } from '../../../shared/flash_messages'; +import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { recursivelyFetchEngines } from '../../utils/recursively_fetch_engines'; import { EngineLogic } from '../engine'; import { EngineDetails } from '../engine/types'; -import { EnginesAPIResponse } from '../engines/types'; -interface SourceEnginesLogicValues { +import { ADD_SOURCE_ENGINES_SUCCESS_MESSAGE, REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE } from './i18n'; + +export interface SourceEnginesLogicValues { dataLoading: boolean; + modalLoading: boolean; + isModalOpen: boolean; + indexedEngines: EngineDetails[]; + indexedEngineNames: string[]; sourceEngines: EngineDetails[]; + sourceEngineNames: string[]; + selectableEngineNames: string[]; + selectedEngineNamesToAdd: string[]; } interface SourceEnginesLogicActions { + addSourceEngines: (sourceEngineNames: string[]) => { sourceEngineNames: string[] }; + fetchIndexedEngines: () => void; fetchSourceEngines: () => void; + onSourceEngineRemove: (sourceEngineNameToRemove: string) => { sourceEngineNameToRemove: string }; + onSourceEnginesAdd: ( + sourceEnginesToAdd: EngineDetails[] + ) => { sourceEnginesToAdd: EngineDetails[] }; onSourceEnginesFetch: ( sourceEngines: SourceEnginesLogicValues['sourceEngines'] ) => { sourceEngines: SourceEnginesLogicValues['sourceEngines'] }; + removeSourceEngine: (sourceEngineName: string) => { sourceEngineName: string }; + setIndexedEngines: (indexedEngines: EngineDetails[]) => { indexedEngines: EngineDetails[] }; + openModal: () => void; + closeModal: () => void; + onAddEnginesSelection: ( + selectedEngineNamesToAdd: string[] + ) => { selectedEngineNamesToAdd: string[] }; } export const SourceEnginesLogic = kea< @@ -29,8 +52,17 @@ export const SourceEnginesLogic = kea< >({ path: ['enterprise_search', 'app_search', 'source_engines_logic'], actions: () => ({ + addSourceEngines: (sourceEngineNames) => ({ sourceEngineNames }), + fetchIndexedEngines: true, fetchSourceEngines: true, + onSourceEngineRemove: (sourceEngineNameToRemove) => ({ sourceEngineNameToRemove }), + onSourceEnginesAdd: (sourceEnginesToAdd) => ({ sourceEnginesToAdd }), onSourceEnginesFetch: (sourceEngines) => ({ sourceEngines }), + removeSourceEngine: (sourceEngineName) => ({ sourceEngineName }), + setIndexedEngines: (indexedEngines) => ({ indexedEngines }), + openModal: true, + closeModal: true, + onAddEnginesSelection: (selectedEngineNamesToAdd) => ({ selectedEngineNamesToAdd }), }), reducers: () => ({ dataLoading: [ @@ -39,47 +71,119 @@ export const SourceEnginesLogic = kea< onSourceEnginesFetch: () => false, }, ], + modalLoading: [ + false, + { + addSourceEngines: () => true, + closeModal: () => false, + }, + ], + isModalOpen: [ + false, + { + openModal: () => true, + closeModal: () => false, + }, + ], + indexedEngines: [ + [], + { + setIndexedEngines: (_, { indexedEngines }) => indexedEngines, + }, + ], + selectedEngineNamesToAdd: [ + [], + { + closeModal: () => [], + onAddEnginesSelection: (_, { selectedEngineNamesToAdd }) => selectedEngineNamesToAdd, + }, + ], sourceEngines: [ [], { + onSourceEnginesAdd: (sourceEngines, { sourceEnginesToAdd }) => [ + ...sourceEngines, + ...sourceEnginesToAdd, + ], onSourceEnginesFetch: (_, { sourceEngines }) => sourceEngines, + onSourceEngineRemove: (sourceEngines, { sourceEngineNameToRemove }) => + sourceEngines.filter((sourceEngine) => sourceEngine.name !== sourceEngineNameToRemove), }, ], }), - listeners: ({ actions }) => ({ - fetchSourceEngines: () => { + selectors: { + indexedEngineNames: [ + (selectors) => [selectors.indexedEngines], + (indexedEngines) => indexedEngines.map((engine: EngineDetails) => engine.name), + ], + sourceEngineNames: [ + (selectors) => [selectors.sourceEngines], + (sourceEngines) => sourceEngines.map((engine: EngineDetails) => engine.name), + ], + selectableEngineNames: [ + (selectors) => [selectors.indexedEngineNames, selectors.sourceEngineNames], + (indexedEngineNames, sourceEngineNames) => + indexedEngineNames.filter((engineName: string) => !sourceEngineNames.includes(engineName)), + ], + }, + listeners: ({ actions, values }) => ({ + addSourceEngines: async ({ sourceEngineNames }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; - let enginesAccumulator: EngineDetails[] = []; + try { + await http.post(`/api/app_search/engines/${engineName}/source_engines/bulk_create`, { + body: JSON.stringify({ + source_engine_slugs: sourceEngineNames, + }), + }); + + const sourceEnginesToAdd = values.indexedEngines.filter(({ name }) => + sourceEngineNames.includes(name) + ); + + actions.onSourceEnginesAdd(sourceEnginesToAdd); + setSuccessMessage(ADD_SOURCE_ENGINES_SUCCESS_MESSAGE(sourceEngineNames)); + EngineLogic.actions.initializeEngine(); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.closeModal(); + } + }, + fetchSourceEngines: () => { + const { engineName } = EngineLogic.values; - // We need to recursively fetch all source engines because we put the data - // into an EuiInMemoryTable to enable searching - const recursiveFetchSourceEngines = async (page = 1) => { - try { - const { meta, results }: EnginesAPIResponse = await http.get( - `/api/app_search/engines/${engineName}/source_engines`, - { - query: { - 'page[current]': page, - 'page[size]': 25, - }, - } - ); + recursivelyFetchEngines({ + endpoint: `/api/app_search/engines/${engineName}/source_engines`, + onComplete: (engines) => actions.onSourceEnginesFetch(engines), + }); + }, + fetchIndexedEngines: () => { + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines', + onComplete: (engines) => actions.setIndexedEngines(engines), + query: { type: 'indexed' }, + }); + }, + removeSourceEngine: async ({ sourceEngineName }) => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; - enginesAccumulator = [...enginesAccumulator, ...results]; + try { + await http.delete( + `/api/app_search/engines/${engineName}/source_engines/${sourceEngineName}` + ); - if (page >= meta.page.total_pages) { - actions.onSourceEnginesFetch(enginesAccumulator); - } else { - recursiveFetchSourceEngines(page + 1); - } - } catch (e) { - flashAPIErrors(e); - } - }; + actions.onSourceEngineRemove(sourceEngineName); + setSuccessMessage(REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE(sourceEngineName)); - recursiveFetchSourceEngines(); + // Changing source engines can change schema conflicts and invalid boosts, + // so we re-initialize the engine to re-fetch that data + EngineLogic.actions.initializeEngine(); // + } catch (e) { + flashAPIErrors(e); + } }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts new file mode 100644 index 0000000000000..104f98e45a5f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; + +import { nextTick } from '@kbn/test/jest'; + +import { recursivelyFetchEngines } from './'; + +describe('recursivelyFetchEngines', () => { + const { http } = mockHttpValues; + const { flashAPIErrors } = mockFlashMessageHelpers; + + const MOCK_PAGE_1 = { + meta: { + page: { current: 1, total_pages: 3 }, + }, + results: [{ name: 'source-engine-1' }], + }; + const MOCK_PAGE_2 = { + meta: { + page: { current: 2, total_pages: 3 }, + }, + results: [{ name: 'source-engine-2' }], + }; + const MOCK_PAGE_3 = { + meta: { + page: { current: 3, total_pages: 3 }, + }, + results: [{ name: 'source-engine-3' }], + }; + const MOCK_CALLBACK = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('recursively calls the passed API endpoint and returns all engines to the onComplete callback', async () => { + http.get + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_1)) + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_2)) + .mockReturnValueOnce(Promise.resolve(MOCK_PAGE_3)); + + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines/some-engine/source_engines', + onComplete: MOCK_CALLBACK, + }); + await nextTick(); + + expect(http.get).toHaveBeenCalledTimes(3); // Called once for each page + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/source_engines', { + query: { + 'page[current]': 1, + 'page[size]': 25, + }, + }); + + expect(MOCK_CALLBACK).toHaveBeenCalledWith([ + { name: 'source-engine-1' }, + { name: 'source-engine-2' }, + { name: 'source-engine-3' }, + ]); + }); + + it('passes optional query params', () => { + recursivelyFetchEngines({ + endpoint: '/api/app_search/engines/some-engine/engines', + onComplete: MOCK_CALLBACK, + query: { type: 'indexed' }, + }); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/engines', { + query: { + 'page[current]': 1, + 'page[size]': 25, + type: 'indexed', + }, + }); + }); + + it('passes optional custom page sizes', () => { + recursivelyFetchEngines({ + endpoint: '/over_9000', + onComplete: MOCK_CALLBACK, + pageSize: 9001, + }); + + expect(http.get).toHaveBeenCalledWith('/over_9000', { + query: { + 'page[current]': 1, + 'page[size]': 9001, + }, + }); + }); + + it('handles errors', async () => { + http.get.mockReturnValueOnce(Promise.reject('error')); + + recursivelyFetchEngines({ endpoint: '/error', onComplete: MOCK_CALLBACK }); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts new file mode 100644 index 0000000000000..797e89bd68b69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; + +import { EngineDetails } from '../../components/engine/types'; +import { EnginesAPIResponse } from '../../components/engines/types'; + +interface Params { + endpoint: string; + onComplete: (engines: EngineDetails[]) => void; + query?: object; + pageSize?: number; +} + +export const recursivelyFetchEngines = ({ + endpoint, + onComplete, + query = {}, + pageSize = 25, +}: Params) => { + const { http } = HttpLogic.values; + + let enginesAccumulator: EngineDetails[] = []; + + const fetchEngines = async (page = 1) => { + try { + const { meta, results }: EnginesAPIResponse = await http.get(endpoint, { + query: { + 'page[current]': page, + 'page[size]': pageSize, + ...query, + }, + }); + + enginesAccumulator = [...enginesAccumulator, ...results]; + + if (page >= meta.page.total_pages) { + onComplete(enginesAccumulator); + } else { + fetchEngines(page + 1); + } + } catch (e) { + flashAPIErrors(e); + } + }; + + fetchEngines(); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index bc4259fa37889..c653cad5c1c0d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -259,47 +259,4 @@ describe('engine routes', () => { }); }); }); - - describe('GET /api/app_search/engines/{name}/source_engines', () => { - let mockRouter: MockRouter; - - beforeEach(() => { - jest.clearAllMocks(); - mockRouter = new MockRouter({ - method: 'get', - path: '/api/app_search/engines/{name}/source_engines', - }); - - registerEnginesRoutes({ - ...mockDependencies, - router: mockRouter.router, - }); - }); - - it('validates correctly with name', () => { - const request = { params: { name: 'test-engine' } }; - mockRouter.shouldValidate(request); - }); - - it('fails validation without name', () => { - const request = { params: {} }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with a non-string name', () => { - const request = { params: { name: 1 } }; - mockRouter.shouldThrow(request); - }); - - it('fails validation with missing query params', () => { - const request = { query: {} }; - mockRouter.shouldThrow(request); - }); - - it('creates a request to enterprise search', () => { - expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/as/engines/:name/source_engines', - }); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index f6e9d30dd0ade..77b055add7d79 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -95,21 +95,4 @@ export function registerEnginesRoutes({ path: '/as/engines/:name/overview_metrics', }) ); - router.get( - { - path: '/api/app_search/engines/{name}/source_engines', - validate: { - params: schema.object({ - name: schema.string(), - }), - query: schema.object({ - 'page[current]': schema.number(), - 'page[size]': schema.number(), - }), - }, - }, - enterpriseSearchRequestHandler.createRequest({ - path: '/as/engines/:name/source_engines', - }) - ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 99aaaeeec38b3..18de4580318a2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -20,6 +20,7 @@ import { registerSchemaRoutes } from './schema'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSearchUIRoutes } from './search_ui'; import { registerSettingsRoutes } from './settings'; +import { registerSourceEnginesRoutes } from './source_engines'; import { registerSynonymsRoutes } from './synonyms'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { @@ -30,6 +31,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); registerSchemaRoutes(dependencies); + registerSourceEnginesRoutes(dependencies); registerCurationsRoutes(dependencies); registerSynonymsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts new file mode 100644 index 0000000000000..5b51048067c00 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts @@ -0,0 +1,151 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSourceEnginesRoutes } from './source_engines'; + +describe('source engine routes', () => { + describe('GET /api/app_search/engines/{name}/source_engines', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/source_engines', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name', () => { + const request = { params: { name: 'test-engine' } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines', + }); + }); + }); + + describe('POST /api/app_search/engines/{name}/source_engines/bulk_create', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{name}/source_engines/bulk_create', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name', () => { + const request = { params: { name: 'test-engine' }, body: { source_engine_slugs: [] } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: {}, body: { source_engine_slugs: [] } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1 }, body: { source_engine_slugs: [] } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { params: { name: 'test-engine' }, body: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines/bulk_create', + }); + }); + }); + + describe('DELETE /api/app_search/engines/{name}/source_engines/{source_engine_name}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}', + }); + + registerSourceEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('validates correctly with name and source_engine_name', () => { + const request = { params: { name: 'test-engine', source_engine_name: 'source-engine' } }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without name', () => { + const request = { params: { source_engine_name: 'source-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string name', () => { + const request = { params: { name: 1, source_engine_name: 'source-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without source_engine_name', () => { + const request = { params: { name: 'test-engine' } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with a non-string source_engine_name', () => { + const request = { params: { name: 'test-engine', source_engine_name: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('fails validation with missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:name/source_engines/:source_engine_name', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts new file mode 100644 index 0000000000000..8e55b0e6f1ac6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerSourceEnginesRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{name}/source_engines', + validate: { + params: schema.object({ + name: schema.string(), + }), + query: schema.object({ + 'page[current]': schema.number(), + 'page[size]': schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines', + }) + ); + + router.post( + { + path: '/api/app_search/engines/{name}/source_engines/bulk_create', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + source_engine_slugs: schema.arrayOf(schema.string()), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines/bulk_create', + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{name}/source_engines/{source_engine_name}', + validate: { + params: schema.object({ + name: schema.string(), + source_engine_name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:name/source_engines/:source_engine_name', + }) + ); +} From 92ac9b4301c15ddd7a7f29c6a137efeb91b9c217 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Sun, 16 May 2021 18:09:12 -0400 Subject: [PATCH 073/186] [Uptime] [Synthetics Integration] update tls passphrase and http password field to use EuiFieldPassword (#100162) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/fleet_package/http_advanced_fields.tsx | 3 ++- .../uptime/public/components/fleet_package/tls_fields.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx index 5cc1dd12ef961..7ab6c81fbf162 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http_advanced_fields.tsx @@ -17,6 +17,7 @@ import { EuiDescribedFormGroup, EuiCheckbox, EuiSpacer, + EuiFieldPassword, } from '@elastic/eui'; import { useHTTPAdvancedFieldsContext } from './contexts'; @@ -110,7 +111,7 @@ export const HTTPAdvancedFields = memo(({ validate }) => { /> } > - handleInputChange({ diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx index e01d3d59175a4..de8879ec3a819 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx @@ -13,12 +13,12 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, - EuiFieldText, EuiTextArea, EuiFormFieldset, EuiSelect, EuiScreenReaderOnly, EuiSpacer, + EuiFieldPassword, } from '@elastic/eui'; import { useTLSFieldsContext } from './contexts'; @@ -333,7 +333,7 @@ export const TLSFields: React.FunctionComponent<{ } labelAppend={} > - { const value = event.target.value; From a12d5ff59e80678d4c86dff6bcddae451cf35eee Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 17 May 2021 11:46:57 +0200 Subject: [PATCH 074/186] Improve migration perf (#99773) * Do not clone state, use TypeCheck it's not mutated * do not recreate context for every migration * use more optional semver check * update SavedObjectMigrationContext type * add a test model returns new state object * update docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...text.converttomultinamespacetypeversion.md | 2 +- ...-server.savedobjectmigrationcontext.log.md | 2 +- ...objectmigrationcontext.migrationversion.md | 2 +- .../migrations/core/document_migrator.ts | 11 ++++---- .../server/saved_objects/migrations/types.ts | 6 ++--- .../saved_objects/migrationsv2/model.test.ts | 25 +++++++++++++++++++ .../saved_objects/migrationsv2/model.ts | 4 +-- .../saved_objects/migrationsv2/types.ts | 5 ++-- src/core/server/server.api.md | 6 ++--- .../common/saved_dashboard_references.ts | 5 ++-- 10 files changed, 47 insertions(+), 21 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md index 2a30693f4da84..9fe43a2f3f477 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.converttomultinamespacetypeversion.md @@ -9,5 +9,5 @@ The version in which this object type is being converted to a multi-namespace ty Signature: ```typescript -convertToMultiNamespaceTypeVersion?: string; +readonly convertToMultiNamespaceTypeVersion?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md index a1b3378afc53b..20a0e99275a39 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.log.md @@ -9,5 +9,5 @@ logger instance to be used by the migration handler Signature: ```typescript -log: SavedObjectsMigrationLogger; +readonly log: SavedObjectsMigrationLogger; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md index 7b20ae41048f6..a1c2717e6e4a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationcontext.migrationversion.md @@ -9,5 +9,5 @@ The migration version that this migration function is defined for Signature: ```typescript -migrationVersion: string; +readonly migrationVersion: string; ``` diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index f30cfc53018db..c96de6ebbfcdd 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -661,13 +661,14 @@ function wrapWithTry( migrationFn: SavedObjectMigrationFn, log: Logger ) { + const context = Object.freeze({ + log: new MigrationLogger(log), + migrationVersion: version, + convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, + }); + return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) { try { - const context = { - log: new MigrationLogger(log), - migrationVersion: version, - convertToMultiNamespaceTypeVersion: type.convertToMultiNamespaceTypeVersion, - }; const result = migrationFn(doc, context); // A basic sanity check to help migration authors detect basic errors diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 619a7f85a327b..570315e780ebe 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -56,15 +56,15 @@ export interface SavedObjectMigrationContext { /** * logger instance to be used by the migration handler */ - log: SavedObjectsMigrationLogger; + readonly log: SavedObjectsMigrationLogger; /** * The migration version that this migration function is defined for */ - migrationVersion: string; + readonly migrationVersion: string; /** * The version in which this object type is being converted to a multi-namespace type */ - convertToMultiNamespaceTypeVersion?: string; + readonly convertToMultiNamespaceTypeVersion?: string; } /** diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index adeb78e568af3..7a47e58f1947c 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -198,6 +198,31 @@ describe('migrations v2 model', () => { }); describe('model transitions from', () => { + it('transition returns new state', () => { + const initState: State = { + ...baseState, + controlState: 'INIT', + currentAlias: '.kibana', + versionAlias: '.kibana_7.11.0', + versionIndex: '.kibana_7.11.0_001', + }; + + const res: ResponseType<'INIT'> = Either.right({ + '.kibana_7.11.0_001': { + aliases: { + '.kibana': {}, + '.kibana_7.11.0': {}, + }, + mappings: { + properties: {}, + }, + settings: {}, + }, + }); + const newState = model(initState, res); + expect(newState).not.toBe(initState); + }); + describe('INIT', () => { const initState: State = { ...baseState, diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 3ef3cb4f83b6f..f4185225ae073 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -9,7 +9,7 @@ import { gt, valid } from 'semver'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; -import { cloneDeep } from 'lodash'; + import { AliasAction, FetchIndexResponse, isLeftTypeof, RetryableEsClientError } from './actions'; import { AllActionStates, InitState, State } from './types'; import { IndexMapping } from '../mappings'; @@ -187,7 +187,7 @@ export const model = (currentState: State, resW: ResponseType): // control state using: // `const res = resW as ResponseType;` - let stateP: State = cloneDeep(currentState); + let stateP: State = currentState; // Handle retryable_es_client_errors. Other left values need to be handled // by the control state specific code below. diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index e3e52212d56cb..adcd2ad32fd24 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -381,7 +381,7 @@ export interface LegacyDeleteState extends LegacyBaseState { readonly controlState: 'LEGACY_DELETE'; } -export type State = +export type State = Readonly< | FatalState | InitState | DoneState @@ -411,7 +411,8 @@ export type State = | LegacySetWriteBlockState | LegacyReindexState | LegacyReindexWaitForTaskState - | LegacyDeleteState; + | LegacyDeleteState +>; export type AllControlStates = State['controlState']; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 972e220baae3e..3e6a69d159192 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2152,9 +2152,9 @@ export interface SavedObjectExportBaseOptions { // @public export interface SavedObjectMigrationContext { - convertToMultiNamespaceTypeVersion?: string; - log: SavedObjectsMigrationLogger; - migrationVersion: string; + readonly convertToMultiNamespaceTypeVersion?: string; + readonly log: SavedObjectsMigrationLogger; + readonly migrationVersion: string; } // @public diff --git a/src/plugins/dashboard/common/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts index 16ab470ce7d6f..9f0858759d0d9 100644 --- a/src/plugins/dashboard/common/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import semverSatisfies from 'semver/functions/satisfies'; +import Semver from 'semver'; import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; import { DashboardContainerStateWithType, DashboardPanelState } from './types'; import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; @@ -24,7 +23,7 @@ export interface SavedObjectAttributesAndReferences { } const isPre730Panel = (panel: Record): boolean => { - return 'version' in panel ? semverSatisfies(panel.version, '<7.3') : true; + return 'version' in panel ? Semver.gt('7.3.0', panel.version) : true; }; function dashboardAttributesToState( From 3e54c468bb99b832e7fb67df5f7101501d79fe64 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 17 May 2021 13:45:29 +0200 Subject: [PATCH 075/186] [Reporting] Added appropriate table caption for table listing generated reports (#100118) * added appropriate table caption for table listing generated reports * updated jest snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/__snapshots__/report_listing.test.tsx.snap | 1 + x-pack/plugins/reporting/public/components/report_listing.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap index ddba7842f1199..670d16ec11548 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -52,6 +52,7 @@ Array [ "onSelectionChange": [Function], } } + tableCaption="Reports generated in Kibana applications" tableLayout="fixed" >
{ return ( Date: Mon, 17 May 2021 14:22:37 +0200 Subject: [PATCH 076/186] [Ingest pipelines] add support for registered_domain processor (#99643) The Ingest Node Pipelines UI added support to configure a registered domain processor. This processor extracts the registered domain, sub-domain and top-level domain from a fully qualified domain name. --- .../processors/registered_domain.test.tsx | 130 ++++++++++++++++++ .../processor_form/processors/index.ts | 1 + .../processors/registered_domain.tsx | 35 +++++ .../shared/map_processor_type_to_form.tsx | 23 ++++ .../ingest_pipelines/public/shared_imports.ts | 1 + 5 files changed, 190 insertions(+) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/registered_domain.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx new file mode 100644 index 0000000000000..962e099f5b667 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/registered_domain.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the registered domain processor when saved +const defaultRegisteredDomainParameters = { + description: undefined, + if: undefined, + ignore_missing: undefined, + ignore_failure: undefined, +}; + +const REGISTERED_DOMAIN_TYPE = 'registered_domain'; + +describe('Processor: Registered Domain', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(REGISTERED_DOMAIN_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the type defined + await saveNewProcessor(); + + // Expect form error as "field" is required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('saves with default parameter values', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, REGISTERED_DOMAIN_TYPE); + expect(processors[0][REGISTERED_DOMAIN_TYPE]).toEqual({ + field: 'field_1', + ...defaultRegisteredDomainParameters, + }); + }); + + test('should still send ignore_missing:false when the toggle is disabled', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + + // Disable ignore missing toggle + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, REGISTERED_DOMAIN_TYPE); + expect(processors[0][REGISTERED_DOMAIN_TYPE]).toEqual({ + ...defaultRegisteredDomainParameters, + field: 'field_1', + ignore_missing: false, + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, REGISTERED_DOMAIN_TYPE); + expect(processors[0][REGISTERED_DOMAIN_TYPE]).toEqual({ + field: 'field_1', + target_field: 'target_field', + ...defaultRegisteredDomainParameters, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts index 9dec9d3f0384f..4fb4365c477b5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts @@ -28,6 +28,7 @@ export { Json } from './json'; export { Kv } from './kv'; export { Lowercase } from './lowercase'; export { Pipeline } from './pipeline'; +export { RegisteredDomain } from './registered_domain'; export { Remove } from './remove'; export { Rename } from './rename'; export { Script } from './script'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/registered_domain.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/registered_domain.tsx new file mode 100644 index 0000000000000..4118a125914b2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/registered_domain.tsx @@ -0,0 +1,35 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { from } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { SerializerFunc } from '../../../../../../shared_imports'; + +export const RegisteredDomain: FunctionComponent = () => { + return ( + <> + + + + + } + /> + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index 5ab2d68aa193f..b5e42ea56bdf8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -34,6 +34,7 @@ import { Kv, Lowercase, Pipeline, + RegisteredDomain, Remove, Rename, Script, @@ -518,6 +519,28 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }, }), }, + registered_domain: { + FieldsComponent: RegisteredDomain, + docLinkPath: '/registered-domain-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.registeredDomain', { + defaultMessage: 'Registered domain', + }), + typeDescription: i18n.translate( + 'xpack.ingestPipelines.processors.description.registeredDomain', + { + defaultMessage: + 'Extracts the registered domain (effective top-level domain), sub-domain, and top-level domain from a fully qualified domain name.', + } + ), + getDefaultDescription: ({ field }) => + i18n.translate('xpack.ingestPipelines.processors.defaultDescription.registeredDomain', { + defaultMessage: + 'Extracts the registered domain, sub-domain, and top-level domain from "{field}"', + values: { + field, + }, + }), + }, remove: { FieldsComponent: Remove, docLinkPath: '/remove-processor.html', diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 4afd434b89372..8ed57221a1395 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -52,6 +52,7 @@ export { ValidationConfig, useFormData, FormOptions, + SerializerFunc, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { From 438b52a34fc26b68b6100aae09dba52bc2bff56a Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 17 May 2021 10:17:53 -0400 Subject: [PATCH 077/186] [Fleet] Improve fleet server upgrade modal (#99796) --- .../components/fleet_server_upgrade_modal.tsx | 82 +++++++++++++++---- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx index 4d6ac864ee8b5..ede997f185c7e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/fleet_server_upgrade_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiButton, EuiCheckbox, @@ -24,7 +24,15 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { sendPutSettings, useLink, useStartServices } from '../../../hooks'; +import { + sendGetAgents, + sendGetOneAgentPolicy, + sendPutSettings, + useLink, + useStartServices, +} from '../../../hooks'; +import type { PackagePolicy } from '../../../types'; +import { FLEET_SERVER_PACKAGE } from '../../../constants'; interface Props { onClose: () => void; @@ -37,6 +45,56 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos const isCloud = !!cloud?.cloudId; const [checked, setChecked] = useState(false); + const [isAgentsAndPoliciesLoaded, setAgentsAndPoliciesLoaded] = useState(false); + + // Check if an other agent than the fleet server is already enrolled + useEffect(() => { + async function check() { + try { + const agentPoliciesAlreadyChecked: { [k: string]: boolean } = {}; + + const res = await sendGetAgents({ + page: 1, + perPage: 10, + showInactive: false, + }); + + if (res.error) { + throw res.error; + } + + for (const agent of res.data?.list ?? []) { + if (!agent.policy_id || agentPoliciesAlreadyChecked[agent.policy_id]) { + continue; + } + + agentPoliciesAlreadyChecked[agent.policy_id] = true; + const policyRes = await sendGetOneAgentPolicy(agent.policy_id); + const hasFleetServer = + (policyRes.data?.item.package_policies as PackagePolicy[]).some((p: PackagePolicy) => { + return p.package?.name === FLEET_SERVER_PACKAGE; + }) ?? false; + if (!hasFleetServer) { + await sendPutSettings({ + has_seen_fleet_migration_notice: true, + }); + onClose(); + return; + } + } + setAgentsAndPoliciesLoaded(true); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.fleetServerUpgradeModal.errorLoadingAgents', { + defaultMessage: `Error loading agents`, + }), + }); + } + } + + check(); + }, [notifications.toasts, onClose]); + const onChange = useCallback(async () => { try { setChecked(!checked); @@ -52,18 +110,23 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos } }, [checked, setChecked, notifications]); + if (!isAgentsAndPoliciesLoaded) { + return null; + } + return ( = ({ onClos {isCloud ? ( @@ -84,17 +147,6 @@ export const FleetServerUpgradeModal: React.FunctionComponent = ({ onClos /> ), - link: ( - - - - ), }} /> ) : ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ae61f24201ce5..c46b59bce2f8e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9514,7 +9514,6 @@ "xpack.fleet.fleetServerUpgradeModal.breakingChangeMessage": "これは大きい変更であるため、ベータリリースにしています。ご不便をおかけしていることをお詫び申し上げます。ご質問がある場合や、サポートが必要な場合は、{link}を共有してください。", "xpack.fleet.fleetServerUpgradeModal.checkboxLabel": "次回以降このメッセージを表示しない", "xpack.fleet.fleetServerUpgradeModal.closeButton": "閉じて開始する", - "xpack.fleet.fleetServerUpgradeModal.cloudDescriptionMessage": "Fleetサーバーを使用できます。スケーラビリティとセキュリティが強化されました。すでにElastic CloudでAPMを使用している場合は、APM & Fleetにアップグレードされました。{existingAgentsMessage} Fleetを使用し続けるには、Fleetサーバーと新しいバージョンのElasticエージェントを各ホストにインストールする必要があります。詳細については、{link}をご覧ください。", "xpack.fleet.fleetServerUpgradeModal.existingAgentText": "既存のElasticエージェントは自動的に登録解除され、データの送信を停止しました。", "xpack.fleet.fleetServerUpgradeModal.failedUpdateTitle": "設定の保存エラー", "xpack.fleet.fleetServerUpgradeModal.fleetFeedbackLink": "フィードバック", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ecd6c0d68a94f..90aa517c68232 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9603,7 +9603,6 @@ "xpack.fleet.fleetServerUpgradeModal.breakingChangeMessage": "这是一项重大更改,所以我们在公测版中进行该更改。非常抱歉带来不便。如果您有疑问或需要帮助,请共享 {link}。", "xpack.fleet.fleetServerUpgradeModal.checkboxLabel": "不再显示此消息", "xpack.fleet.fleetServerUpgradeModal.closeButton": "关闭并开始使用", - "xpack.fleet.fleetServerUpgradeModal.cloudDescriptionMessage": "Fleet 服务器现在可用并提供改善的可扩展性和安全性。如果在 Elastic Cloud 上已有 APM,则我们已将其升级到 APM & Fleet。{existingAgentsMessage}要继续使用 Fleet,必须在各个主机上安装 Fleet 服务器和新版 Elastic 代理。详细了解我们的 {link}。", "xpack.fleet.fleetServerUpgradeModal.existingAgentText": "您现有的 Elastic 代理已被自动销注且已停止发送数据。", "xpack.fleet.fleetServerUpgradeModal.failedUpdateTitle": "保存设置时出错", "xpack.fleet.fleetServerUpgradeModal.fleetFeedbackLink": "反馈", From b44c2a7c12edfe95fad805ef2a7eab0ad2da9bd4 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 17 May 2021 10:21:47 -0400 Subject: [PATCH 078/186] [Fleet] Fix migration 7.12 to 7.13 migrate settings (#100054) --- .../server/services/fleet_server/saved_object_migrations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index 82fd937092477..5c05ed7532ac6 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -28,6 +28,7 @@ import { invalidateAPIKeys } from '../api_keys'; import { settingsService } from '..'; export async function runFleetServerMigration() { + await settingsService.settingsSetup(getInternalUserSOClient()); await Promise.all([migrateEnrollmentApiKeys(), migrateAgentPolicies(), migrateAgents()]); } From 8758d8e4613638c26e3fc0adc7fc36e082c30115 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 17 May 2021 16:04:57 +0100 Subject: [PATCH 079/186] skip flaky suite #(95899) --- .../plugins/canvas/shareable_runtime/components/app.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx index acf71cad3f3ba..b68642d184542 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/app.test.tsx @@ -59,7 +59,8 @@ const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { return mount(); }; -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/95899 +describe.skip('', () => { test('App renders properly', () => { expect(getWrapper().html()).toMatchSnapshot(); }); From 6c31fd06dec5c2f65e0ae6d7a9ef6fdc61aced43 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 17 May 2021 16:12:08 +0100 Subject: [PATCH 080/186] skip flaky suite (#100012) --- x-pack/test/functional/apps/spaces/enter_space.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 4e4d4974ced09..e8a67f8899a36 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,7 +14,8 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - describe('Enter Space', function () { + // FLAKY: https://github.com/elastic/kibana/issues/100012 + describe.skip('Enter Space', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('spaces/enter_space'); From 0e674a53219b58de37a175a33b77c75e62274c72 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 17 May 2021 11:16:50 -0400 Subject: [PATCH 081/186] [Security Solution][Endpoint] Refactor Host Isolation component used in Isolate use case (#100159) * EndpointHostIsolateForm component * Refactor Detections Host isolation flyout to use isolateform --- .../endpoint/host_isolation/index.ts | 9 ++ .../endpoint/host_isolation/isolate_form.tsx | 88 +++++++++++ .../host_isolation/isolate_success.tsx | 41 ++++++ .../endpoint/host_isolation/translations.ts | 31 ++++ .../components/host_isolation/index.tsx | 138 +++++++----------- .../components/host_isolation/translations.ts | 17 --- 6 files changed, 218 insertions(+), 106 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts new file mode 100644 index 0000000000000..de51df283251d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './isolate_success'; +export * from './isolate_form'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx new file mode 100644 index 0000000000000..dd26f676c1fe3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx @@ -0,0 +1,88 @@ +/* + * 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 React, { ChangeEventHandler, memo, ReactNode, useCallback } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM } from './translations'; + +export interface EndpointIsolatedFormProps { + hostName: string; + onCancel: () => void; + onConfirm: () => void; + onChange: (changes: { comment: string }) => void; + comment?: string; + /** Any additional message to be appended to the default one */ + messageAppend?: ReactNode; + /** If true, then `Confirm` and `Cancel` buttons will be disabled, and `Confirm` button will loading loading style */ + isLoading?: boolean; +} + +export const EndpointIsolateForm = memo( + ({ hostName, onCancel, onConfirm, onChange, comment = '', messageAppend, isLoading = false }) => { + const handleCommentChange: ChangeEventHandler = useCallback( + (event) => { + onChange({ comment: event.target.value }); + }, + [onChange] + ); + + return ( + <> + +

+ {hostName} }} + />{' '} + {messageAppend} +

+ + + + + +

{COMMENT}

+
+ + + + + + + + {CANCEL} + + + + + {CONFIRM} + + + + + ); + } +); + +EndpointIsolateForm.displayName = 'EndpointIsolateForm'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx new file mode 100644 index 0000000000000..ee70a4526f5ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx @@ -0,0 +1,41 @@ +/* + * 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 React, { memo, ReactNode } from 'react'; +import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { GET_SUCCESS_MESSAGE } from './translations'; + +export interface EndpointIsolateSuccessProps { + hostName: string; + completeButtonLabel: string; + onComplete: () => void; + additionalInfo?: ReactNode; +} + +export const EndpointIsolateSuccess = memo( + ({ hostName, onComplete, completeButtonLabel, additionalInfo }) => { + return ( + <> + + {additionalInfo} + + + + + + +

{completeButtonLabel}

+
+
+
+
+ + ); + } +); + +EndpointIsolateSuccess.displayName = 'EndpointIsolateSuccess'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts new file mode 100644 index 0000000000000..baeced2a7a69f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts @@ -0,0 +1,31 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const CANCEL = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.cancel', { + defaultMessage: 'Cancel', +}); + +export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.confirm', { + defaultMessage: 'Confirm', +}); + +export const COMMENT = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.comment', { + defaultMessage: 'Comment', +}); + +export const COMMENT_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.endpoint.hostIsolation.comment.placeholder', + { defaultMessage: 'You may leave an optional note here.' } +); + +export const GET_SUCCESS_MESSAGE = (hostName: string) => + i18n.translate('xpack.securitySolution.endpoint.hostIsolation.successfulMessage', { + defaultMessage: 'Host Isolation on {hostName} successfully submitted', + values: { hostName }, + }); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx index 3897458e8459c..e6fd3a8459f76 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx @@ -7,32 +7,19 @@ import React, { useMemo, useState, useCallback } from 'react'; import { find } from 'lodash/fp'; -import { - EuiCallOut, - EuiTitle, - EuiText, - EuiTextArea, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation'; -import { - CANCEL, - CASES_ASSOCIATED_WITH_ALERT, - COMMENT, - COMMENT_PLACEHOLDER, - CONFIRM, - RETURN_TO_ALERT_DETAILS, -} from './translations'; +import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations'; import { Maybe } from '../../../../../observability/common/typings'; import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts'; import { CaseDetailsLink } from '../../../common/components/links'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { + EndpointIsolatedFormProps, + EndpointIsolateForm, + EndpointIsolateSuccess, +} from '../../../common/components/endpoint/host_isolation'; export const HostIsolationPanel = React.memo( ({ @@ -76,6 +63,11 @@ export const HostIsolationPanel = React.memo( const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); + const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback( + ({ comment: newComment }) => setComment(newComment), + [] + ); + const casesList = useMemo( () => caseIds.map((id, index) => { @@ -100,43 +92,29 @@ export const HostIsolationPanel = React.memo( return ( <> - - {caseCount > 0 && ( - <> - -

- -

-
- -
    {casesList}
-
- - )} -
- - - - -

{RETURN_TO_ALERT_DETAILS}

-
-
-
-
+ 0 && ( + <> + +

+ +

+
+ +
    {casesList}
+
+ + ) + } + /> ); }, [backToAlertDetails, hostName, caseCount, casesList]); @@ -145,13 +123,18 @@ export const HostIsolationPanel = React.memo( return ( <> - -

+ {hostName}, cases: ( {caseCount} @@ -161,42 +144,19 @@ export const HostIsolationPanel = React.memo( ), }} /> -

-
- - -

{COMMENT}

-
- ) => - setComment(event.target.value) } /> - - - - {CANCEL} - - - - {CONFIRM} - - - ); }, [ - alertRule, + hostName, backToAlertDetails, - comment, confirmHostIsolation, - hostName, + handleIsolateFormChange, + comment, loading, caseCount, + alertRule, ]); return isIsolated ? hostIsolated : hostNotIsolated; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts index 8d6334f6c340d..027a97cc3846e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts @@ -14,23 +14,6 @@ export const ISOLATE_HOST = i18n.translate( } ); -export const COMMENT = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.comment', { - defaultMessage: 'Comment', -}); - -export const COMMENT_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.endpoint.hostIsolation.comment.placeholder', - { defaultMessage: 'You may leave an optional note here.' } -); - -export const CANCEL = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.cancel', { - defaultMessage: 'Cancel', -}); - -export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.confirm', { - defaultMessage: 'Confirm', -}); - export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string => i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert', From 68c7227760be4f1577a47fb524403d9a2c519436 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 17 May 2021 17:18:20 +0200 Subject: [PATCH 082/186] [QA] fix dashboard lens by value test (#100196) * [functional test] remove redundant navigation, wait for lens to be loaded * fix navigation to new viz * update test title Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/visualize/_visualize_listing.ts | 1 - test/functional/page_objects/visualize_page.ts | 4 ++-- .../functional/apps/dashboard/dashboard_lens_by_value.ts | 9 ++++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/test/functional/apps/visualize/_visualize_listing.ts b/test/functional/apps/visualize/_visualize_listing.ts index 90e7da1696702..78474693e939a 100644 --- a/test/functional/apps/visualize/_visualize_listing.ts +++ b/test/functional/apps/visualize/_visualize_listing.ts @@ -45,7 +45,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('search', function () { before(async function () { // create one new viz - await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt('HELLO'); diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 9a4c01f0f2767..78a963867b8c2 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -129,14 +129,14 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide } public async navigateToNewVisualization() { - await common.navigateToApp('visualize'); + await this.gotoVisualizationLandingPage(); await header.waitUntilLoadingHasFinished(); await this.clickNewVisualization(); await this.waitForGroupsSelectPage(); } public async navigateToNewAggBasedVisualization() { - await common.navigateToApp('visualize'); + await this.gotoVisualizationLandingPage(); await header.waitUntilLoadingHasFinished(); await this.clickNewVisualization(); await this.clickAggBasedVisualizations(); diff --git a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts index 87ecfe0dcada9..865f4c64f0f1a 100644 --- a/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/dashboard_lens_by_value.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'header']); + const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'timePicker']); const find = getService('find'); const esArchiver = getService('esArchiver'); @@ -69,11 +69,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(titles.indexOf(newTitle)).to.not.be(-1); }); - it('is no longer linked to a dashboard after visiting the visuali1ze listing page', async () => { - await PageObjects.visualize.gotoVisualizationLandingPage(); + it('is no longer linked to a dashboard after visiting the visualize listing page', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickLensWidget(); - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', operation: 'date_histogram', @@ -84,8 +83,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { operation: 'average', field: 'bytes', }); + await PageObjects.lens.waitForVisualization(); await PageObjects.lens.notLinkedToOriginatingApp(); - await PageObjects.header.waitUntilLoadingHasFinished(); // return to origin should not be present in save modal await testSubjects.click('lnsApp_saveButton'); From 2c03e6b0f9e225551911b8a59a66f73f06b6ac68 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 17 May 2021 16:19:56 +0100 Subject: [PATCH 083/186] skip failing es promotion suite (#99915) --- .../security_and_spaces/tests/get_signals_migration_status.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts index 793dec9eaae4b..869d1672150cc 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts @@ -19,7 +19,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('Signals migration status', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 + describe.skip('Signals migration status', () => { let legacySignalsIndexName: string; beforeEach(async () => { await createSignalsIndex(supertest); From e1304fb57b39cbc0181dd3d5ce3cf8fb268982ef Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 17 May 2021 16:21:46 +0100 Subject: [PATCH 084/186] skip failing es promotion suite (#99915) --- .../reporting_and_security/download_csv_dashboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts b/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts index 7f642f171b9fc..626cd217bde5f 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts @@ -31,7 +31,8 @@ export default function ({ getService }: FtrProviderContext) { }, }; - describe('CSV Generation from SearchSource', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 + describe.skip('CSV Generation from SearchSource', () => { before(async () => { await kibanaServer.uiSettings.update({ 'csv:quoteValues': false, From 6a64220e3e99915718c129c065a1710ac61a462c Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 17 May 2021 17:26:13 +0200 Subject: [PATCH 085/186] remove non-valid code (#100144) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/common_page.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 6d9641a1a920e..bc60b8ce5f19c 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -257,7 +257,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo return currentUrl; }); - await retry.try(async () => { + await retry.tryForTime(defaultFindTimeout, async () => { await this.sleep(501); const currentUrl = await browser.getCurrentUrl(); log.debug('in navigateTo url = ' + currentUrl); @@ -266,10 +266,6 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo throw new Error('URL changed, waiting for it to settle'); } }); - if (appName === 'status_page') return; - if (await testSubjects.exists('statusPageContainer')) { - throw new Error('Navigation ended up at the status page.'); - } }); } From 28fd9188eb58964c0606158c8db0392fa615b9ea Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 17 May 2021 09:45:48 -0700 Subject: [PATCH 086/186] [Canvas] Fix column object shape in datatable created by CSV function (#98561) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/{csv.test.js => csv.test.ts} | 54 ++++++++++--------- .../canvas_plugin_src/functions/common/csv.ts | 6 ++- 2 files changed, 35 insertions(+), 25 deletions(-) rename x-pack/plugins/canvas/canvas_plugin_src/functions/common/{csv.test.js => csv.test.ts} (72%) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts similarity index 72% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts index 500224a7a3bbe..93cf07a9dd5dd 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts @@ -5,19 +5,21 @@ * 2.0. */ +// @ts-expect-error untyped lib import { functionWrapper } from '../../../test_helpers/function_wrapper'; import { getFunctionErrors } from '../../../i18n'; import { csv } from './csv'; +import { Datatable } from 'src/plugins/expressions'; const errors = getFunctionErrors().csv; describe('csv', () => { const fn = functionWrapper(csv); - const expected = { + const expected: Datatable = { type: 'datatable', columns: [ - { name: 'name', type: 'string' }, - { name: 'number', type: 'string' }, + { id: 'name', name: 'name', meta: { type: 'string' } }, + { id: 'number', name: 'number', meta: { type: 'string' } }, ], rows: [ { name: 'one', number: '1' }, @@ -69,43 +71,47 @@ fourty two%SPLIT%42`, }); it('should trim column names', () => { - expect( - fn(null, { - data: `foo," bar ", baz, " buz " -1,2,3,4`, - }) - ).toEqual({ + const expectedResult: Datatable = { type: 'datatable', columns: [ - { name: 'foo', type: 'string' }, - { name: 'bar', type: 'string' }, - { name: 'baz', type: 'string' }, - { name: 'buz', type: 'string' }, + { id: 'foo', name: 'foo', meta: { type: 'string' } }, + { id: 'bar', name: 'bar', meta: { type: 'string' } }, + { id: 'baz', name: 'baz', meta: { type: 'string' } }, + { id: 'buz', name: 'buz', meta: { type: 'string' } }, ], rows: [{ foo: '1', bar: '2', baz: '3', buz: '4' }], - }); - }); + }; - it('should handle odd spaces correctly', () => { expect( fn(null, { data: `foo," bar ", baz, " buz " -1," best ",3, " ok" -" good", bad, better , " worst " `, +1,2,3,4`, }) - ).toEqual({ + ).toEqual(expectedResult); + }); + + it('should handle odd spaces correctly', () => { + const expectedResult: Datatable = { type: 'datatable', columns: [ - { name: 'foo', type: 'string' }, - { name: 'bar', type: 'string' }, - { name: 'baz', type: 'string' }, - { name: 'buz', type: 'string' }, + { id: 'foo', name: 'foo', meta: { type: 'string' } }, + { id: 'bar', name: 'bar', meta: { type: 'string' } }, + { id: 'baz', name: 'baz', meta: { type: 'string' } }, + { id: 'buz', name: 'buz', meta: { type: 'string' } }, ], rows: [ { foo: '1', bar: ' best ', baz: '3', buz: ' ok' }, { foo: ' good', bar: ' bad', baz: ' better ', buz: ' worst ' }, ], - }); + }; + + expect( + fn(null, { + data: `foo," bar ", baz, " buz " +1," best ",3, " ok" +" good", bad, better , " worst " `, + }) + ).toEqual(expectedResult); }); it('throws when given invalid csv', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.ts index 1f810e3658c6a..078721fc08d05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.ts @@ -73,7 +73,11 @@ export function csv(): ExpressionFunctionDefinition<'csv', null, Arguments, Data if (i === 0) { // first row, assume header values row.forEach((colName: string) => - acc.columns.push({ name: colName.trim(), type: 'string' }) + acc.columns.push({ + id: colName.trim(), + name: colName.trim(), + meta: { type: 'string' }, + }) ); } else { // any other row is a data row From a641c8baa691c0555f0ab8b47537915720bbd8fb Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 17 May 2021 09:46:54 -0700 Subject: [PATCH 087/186] [Dashboard] Fixes dashboard_save functional test (#98830) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/dashboard/dashboard_save.ts | 9 ++++++--- test/functional/page_objects/dashboard_page.ts | 10 ++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/dashboard/dashboard_save.ts b/test/functional/apps/dashboard/dashboard_save.ts index 0a0a2fc1dd286..dce59744db305 100644 --- a/test/functional/apps/dashboard/dashboard_save.ts +++ b/test/functional/apps/dashboard/dashboard_save.ts @@ -9,12 +9,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['dashboard', 'header']); + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize']); const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); - // FLAKY: https://github.com/elastic/kibana/issues/89476 - describe.skip('dashboard save', function describeIndexTests() { + describe('dashboard save', function describeIndexTests() { this.tags('includeFirefox'); const dashboardName = 'Dashboard Save Test'; const dashboardNameEnterKey = 'Dashboard Save Test with Enter Key'; @@ -127,6 +127,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.expectExistsQuickSaveOption(); + await dashboardAddPanel.clickMarkdownQuickButton(); + await PageObjects.visualize.saveVisualizationAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.dashboard.clickQuickSave(); await testSubjects.existOrFail('saveDashboardSuccess'); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 576e7e516e251..ba75ab75cc6e8 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -277,6 +277,7 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide } public async clickQuickSave() { + await this.expectQuickSaveButtonEnabled(); log.debug('clickQuickSave'); await testSubjects.click('dashboardQuickSaveMenuItem'); } @@ -630,6 +631,15 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); } + public async expectQuickSaveButtonEnabled() { + log.debug('expectQuickSaveButtonEnabled'); + const quickSaveButton = await testSubjects.find('dashboardQuickSaveMenuItem'); + const isDisabled = await quickSaveButton.getAttribute('disabled'); + if (isDisabled) { + throw new Error('Quick save button disabled'); + } + } + public async getNotLoadedVisualizations(vizList: string[]) { const checkList = []; for (const name of vizList) { From 4180ad7f31e62601defa0ce5a1920c5db21adc65 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 17 May 2021 10:51:53 -0600 Subject: [PATCH 088/186] [kbn/test] move types/ftr into src (#99555) * [kbn/test] move types/ftr into src * Apply eslint updates * fix import of Lifecycle type Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/elastic-eslint-config-kibana/.eslintrc.js | 5 +++++ packages/kbn-test/package.json | 4 ++-- packages/kbn-test/src/functional_test_runner/index.ts | 1 + .../functional_test_runner/public_types.ts} | 10 +++------- packages/kbn-test/tsconfig.json | 4 +--- packages/kbn-test/types/README.md | 5 ----- test/accessibility/config.ts | 2 +- test/accessibility/ftr_provider_context.d.ts | 2 +- test/api_integration/ftr_provider_context.d.ts | 2 +- test/common/ftr_provider_context.d.ts | 2 +- .../functional/apps/discover/ftr_provider_context.d.ts | 2 +- test/functional/config.legacy.ts | 2 +- test/functional/ftr_provider_context.d.ts | 2 +- test/functional/services/common/browser.ts | 2 +- test/functional/services/common/test_subjects.ts | 2 +- test/functional/services/remote/webdriver.ts | 2 +- test/interpreter_functional/config.ts | 2 +- test/new_visualize_flow/config.ts | 2 +- test/plugin_functional/config.ts | 2 +- test/security_functional/config.ts | 2 +- test/server_integration/http/platform/config.ts | 2 +- test/server_integration/services/types.d.ts | 2 +- test/ui_capabilities/newsfeed_err/config.ts | 2 +- test/visual_regression/config.ts | 2 +- test/visual_regression/ftr_provider_context.d.ts | 2 +- .../services/visual_testing/visual_testing.ts | 2 +- x-pack/plugins/apm/ftr_e2e/config.ts | 2 +- x-pack/plugins/apm/ftr_e2e/cypress_open.ts | 2 +- x-pack/plugins/apm/ftr_e2e/cypress_run.ts | 2 +- x-pack/plugins/apm/ftr_e2e/ftr_provider_context.d.ts | 2 +- x-pack/test/accessibility/config.ts | 2 +- x-pack/test/accessibility/ftr_provider_context.d.ts | 2 +- x-pack/test/alerting_api_integration/common/config.ts | 2 +- .../common/ftr_provider_context.d.ts | 2 +- x-pack/test/api_integration/config.ts | 2 +- x-pack/test/api_integration/config_security_basic.ts | 2 +- x-pack/test/api_integration/config_security_trial.ts | 2 +- x-pack/test/api_integration/ftr_provider_context.d.ts | 2 +- x-pack/test/api_integration_basic/config.ts | 2 +- .../api_integration_basic/ftr_provider_context.d.ts | 2 +- x-pack/test/apm_api_integration/common/config.ts | 2 +- .../apm_api_integration/common/ftr_provider_context.ts | 2 +- x-pack/test/banners_functional/config.ts | 2 +- x-pack/test/banners_functional/ftr_provider_context.ts | 2 +- x-pack/test/case_api_integration/common/config.ts | 2 +- .../common/ftr_provider_context.d.ts | 2 +- x-pack/test/common/ftr_provider_context.ts | 2 +- .../detection_engine_api_integration/common/config.ts | 2 +- .../common/ftr_provider_context.d.ts | 2 +- .../encrypted_saved_objects_api_integration/config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- .../test/endpoint_api_integration_no_ingest/config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- x-pack/test/examples/config.ts | 2 +- x-pack/test/fleet_api_integration/config.ts | 2 +- x-pack/test/fleet_functional/config.ts | 2 +- x-pack/test/fleet_functional/ftr_provider_context.d.ts | 2 +- x-pack/test/functional/config_security_basic.ts | 2 +- x-pack/test/functional/ftr_provider_context.d.ts | 2 +- x-pack/test/functional/services/ml/api.ts | 2 +- x-pack/test/functional/services/ml/common_api.ts | 2 +- x-pack/test/functional/services/ml/common_ui.ts | 2 +- x-pack/test/functional/services/ml/custom_urls.ts | 2 +- .../services/ml/dashboard_job_selection_table.ts | 2 +- .../functional/services/ml/data_visualizer_table.ts | 2 +- x-pack/test/functional/services/ml/security_common.ts | 2 +- x-pack/test/functional/services/ml/test_resources.ts | 2 +- .../test/functional/services/transform/management.ts | 2 +- .../functional/services/transform/security_common.ts | 2 +- x-pack/test/functional_basic/config.ts | 2 +- x-pack/test/functional_basic/ftr_provider_context.d.ts | 2 +- x-pack/test/functional_cors/config.ts | 2 +- x-pack/test/functional_cors/ftr_provider_context.d.ts | 2 +- x-pack/test/functional_embedded/config.firefox.ts | 2 +- x-pack/test/functional_embedded/config.ts | 2 +- .../test/functional_embedded/ftr_provider_context.d.ts | 2 +- .../test/functional_enterprise_search/base_config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- .../with_host_configured.config.ts | 2 +- .../without_host_configured.config.ts | 2 +- x-pack/test/functional_vis_wizard/config.ts | 2 +- .../functional_vis_wizard/ftr_provider_context.d.ts | 2 +- x-pack/test/functional_with_es_ssl/config.ts | 2 +- .../functional_with_es_ssl/ftr_provider_context.d.ts | 2 +- x-pack/test/licensing_plugin/config.public.ts | 2 +- x-pack/test/licensing_plugin/config.ts | 2 +- x-pack/test/licensing_plugin/services.ts | 2 +- x-pack/test/lists_api_integration/common/config.ts | 2 +- .../common/ftr_provider_context.d.ts | 2 +- x-pack/test/load/config.ts | 2 +- .../observability_api_integration/common/config.ts | 2 +- x-pack/test/plugin_api_integration/config.ts | 2 +- .../plugin_api_integration/ftr_provider_context.d.ts | 2 +- x-pack/test/plugin_api_perf/ftr_provider_context.d.ts | 2 +- x-pack/test/plugin_functional/config.ts | 2 +- .../test/plugin_functional/ftr_provider_context.d.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- .../reporting_and_security.config.ts | 2 +- .../reporting_without_security.config.ts | 2 +- .../reporting_functional/ftr_provider_context.d.ts | 2 +- .../reporting_and_deprecated_security.config.ts | 2 +- .../reporting_and_security.config.ts | 2 +- .../reporting_without_security.config.ts | 2 +- .../test/saved_object_api_integration/common/config.ts | 2 +- .../common/ftr_provider_context.d.ts | 2 +- .../api_integration/security_and_spaces/config.ts | 2 +- .../api_integration/security_and_spaces/services.ts | 2 +- .../api_integration/tagging_api/config.ts | 2 +- .../api_integration/tagging_api/services.ts | 2 +- x-pack/test/saved_object_tagging/functional/config.ts | 2 +- .../functional/ftr_provider_context.ts | 2 +- x-pack/test/saved_objects_field_count/config.ts | 2 +- x-pack/test/search_sessions_integration/config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- .../test/security_api_integration/anonymous.config.ts | 2 +- .../anonymous_es_anonymous.config.ts | 2 +- x-pack/test/security_api_integration/audit.config.ts | 2 +- .../security_api_integration/ftr_provider_context.d.ts | 2 +- .../test/security_api_integration/kerberos.config.ts | 2 +- .../kerberos_anonymous_access.config.ts | 2 +- .../security_api_integration/login_selector.config.ts | 2 +- x-pack/test/security_api_integration/oidc.config.ts | 2 +- .../oidc_implicit_flow.config.ts | 2 +- x-pack/test/security_api_integration/pki.config.ts | 2 +- x-pack/test/security_api_integration/saml.config.ts | 2 +- .../security_api_integration/session_idle.config.ts | 2 +- .../session_invalidate.config.ts | 2 +- .../session_lifespan.config.ts | 2 +- x-pack/test/security_api_integration/token.config.ts | 2 +- .../test/security_functional/ftr_provider_context.d.ts | 2 +- .../test/security_functional/login_selector.config.ts | 2 +- x-pack/test/security_functional/oidc.config.ts | 2 +- x-pack/test/security_functional/saml.config.ts | 2 +- x-pack/test/security_solution_cypress/cli_config.ts | 2 +- .../test/security_solution_cypress/config.firefox.ts | 2 +- x-pack/test/security_solution_cypress/config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- x-pack/test/security_solution_cypress/visual_config.ts | 2 +- x-pack/test/security_solution_endpoint/config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- .../test/security_solution_endpoint_api_int/config.ts | 2 +- .../ftr_provider_context.d.ts | 2 +- x-pack/test/spaces_api_integration/common/config.ts | 2 +- .../common/ftr_provider_context.d.ts | 2 +- x-pack/test/ui_capabilities/common/config.ts | 2 +- .../ui_capabilities/common/ftr_provider_context.d.ts | 2 +- x-pack/test/upgrade/config.ts | 2 +- x-pack/test/upgrade/ftr_provider_context.d.ts | 2 +- x-pack/test/usage_collection/config.ts | 2 +- x-pack/test/usage_collection/ftr_provider_context.d.ts | 2 +- x-pack/test/visual_regression/config.ts | 2 +- .../test/visual_regression/ftr_provider_context.d.ts | 2 +- 152 files changed, 158 insertions(+), 163 deletions(-) rename packages/kbn-test/{types/ftr.d.ts => src/functional_test_runner/public_types.ts} (94%) delete mode 100644 packages/kbn-test/types/README.md diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 2e978c543cc69..a8c2e9546510e 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -70,6 +70,11 @@ module.exports = { to: '@kbn/tinymath', disallowedMessage: `Don't use 'tinymath', use '@kbn/tinymath'` }, + { + from: '@kbn/test/types/ftr', + to: '@kbn/test', + disallowedMessage: `import from the root of @kbn/test instead` + }, ], ], }, diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index e8e42de3114aa..275d9fac73c58 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/types/index.d.ts", + "main": "./target", + "types": "./target/types", "scripts": { "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --source-maps", diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts index 80d257f1cfb23..268c6b2bd9a67 100644 --- a/packages/kbn-test/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -10,3 +10,4 @@ export { FunctionalTestRunner } from './functional_test_runner'; export { readConfigFile, Config } from './lib'; export { runFtrCli } from './cli'; export * from './lib/docker_servers'; +export * from './public_types'; diff --git a/packages/kbn-test/types/ftr.d.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts similarity index 94% rename from packages/kbn-test/types/ftr.d.ts rename to packages/kbn-test/src/functional_test_runner/public_types.ts index 83f725d86857e..915cb34f6ffe5 100644 --- a/packages/kbn-test/types/ftr.d.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -7,13 +7,9 @@ */ import { ToolingLog } from '@kbn/dev-utils'; -import { - Config, - Lifecycle, - FailureMetadata, - DockerServersService, -} from '../src/functional_test_runner/lib'; -import { Test, Suite } from '../src/functional_test_runner/fake_mocha_types'; + +import { Config, Lifecycle, FailureMetadata, DockerServersService } from './lib'; +import { Test, Suite } from './fake_mocha_types'; export { Lifecycle, Config, FailureMetadata }; diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 8536ad7e0c12f..3cb68029d74cf 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -8,19 +8,17 @@ "declaration": true, "declarationMap": true, "sourceMap": true, - "sourceRoot": "../../../../../packages/kbn-test/src", + "sourceRoot": "../../../../../../packages/kbn-test/src", "types": [ "jest", "node" ], }, "include": [ - "types/**/*", "src/**/*", "index.d.ts" ], "exclude": [ - "types/ftr_globals/**/*", "**/__fixtures__/**/*" ] } diff --git a/packages/kbn-test/types/README.md b/packages/kbn-test/types/README.md deleted file mode 100644 index 1298d2a4afc0a..0000000000000 --- a/packages/kbn-test/types/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @kbn/test/types - -Shared types used by different parts of the tests - - - **`ftr.d.ts`**: These types are generic types for using the functional test runner. They are here because we plan to move the functional test runner into the `@kbn/test` package at some point and having them here makes them a lot easier to import from all over the place like we do. \ No newline at end of file diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index 7364aced7af2c..2643ed8ab2a78 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/test/accessibility/ftr_provider_context.d.ts b/test/accessibility/ftr_provider_context.d.ts index 3cf8ae3e9a1c5..4c827393e1ef3 100644 --- a/test/accessibility/ftr_provider_context.d.ts +++ b/test/accessibility/ftr_provider_context.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/test/api_integration/ftr_provider_context.d.ts b/test/api_integration/ftr_provider_context.d.ts index 56dd8965b8411..91d35a2dbc32a 100644 --- a/test/api_integration/ftr_provider_context.d.ts +++ b/test/api_integration/ftr_provider_context.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/test/common/ftr_provider_context.d.ts b/test/common/ftr_provider_context.d.ts index 56dd8965b8411..91d35a2dbc32a 100644 --- a/test/common/ftr_provider_context.d.ts +++ b/test/common/ftr_provider_context.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/test/functional/apps/discover/ftr_provider_context.d.ts b/test/functional/apps/discover/ftr_provider_context.d.ts index e0113899b0836..5bf34af1bf9f3 100644 --- a/test/functional/apps/discover/ftr_provider_context.d.ts +++ b/test/functional/apps/discover/ftr_provider_context.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from '../../services'; import { pageObjects } from '../../page_objects'; diff --git a/test/functional/config.legacy.ts b/test/functional/config.legacy.ts index 9c5064b3b376c..d38f30a32ef61 100644 --- a/test/functional/config.legacy.ts +++ b/test/functional/config.legacy.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/test/functional/ftr_provider_context.d.ts b/test/functional/ftr_provider_context.d.ts index 3cf8ae3e9a1c5..4c827393e1ef3 100644 --- a/test/functional/ftr_provider_context.d.ts +++ b/test/functional/ftr_provider_context.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index d9212e48f73fc..4dfd30c3b3b68 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -11,7 +11,7 @@ import { cloneDeepWith } from 'lodash'; import { Key, Origin } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { modifyUrl } from '@kbn/std'; import Jimp from 'jimp'; diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index 111206ec9eafe..d0050859cbb32 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -8,7 +8,7 @@ import testSubjSelector from '@kbn/test-subj-selector'; import { map as mapAsync } from 'bluebird'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { WebElementWrapper } from '../lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 21150e3d2ed08..7d629ee817252 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -11,7 +11,7 @@ import Fs from 'fs'; import * as Rx from 'rxjs'; import { mergeMap, map, takeUntil, catchError } from 'rxjs/operators'; -import { Lifecycle } from '@kbn/test/src/functional_test_runner/lib/lifecycle'; +import { Lifecycle } from '@kbn/test'; import { ToolingLog } from '@kbn/dev-utils'; import chromeDriver from 'chromedriver'; // @ts-ignore types not available diff --git a/test/interpreter_functional/config.ts b/test/interpreter_functional/config.ts index 2e52c52784afd..adcac520125ad 100644 --- a/test/interpreter_functional/config.ts +++ b/test/interpreter_functional/config.ts @@ -8,7 +8,7 @@ import path from 'path'; import fs from 'fs'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); diff --git a/test/new_visualize_flow/config.ts b/test/new_visualize_flow/config.ts index 612dcbbd2e1eb..a6bd97464e2d0 100644 --- a/test/new_visualize_flow/config.ts +++ b/test/new_visualize_flow/config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const commonConfig = await readConfigFile(require.resolve('../functional/config.js')); diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index d21a157975ac8..1ff55edc010da 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import path from 'path'; import fs from 'fs'; diff --git a/test/security_functional/config.ts b/test/security_functional/config.ts index fd87ff9bb2ffe..f61f3bbf240ec 100644 --- a/test/security_functional/config.ts +++ b/test/security_functional/config.ts @@ -7,7 +7,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); diff --git a/test/server_integration/http/platform/config.ts b/test/server_integration/http/platform/config.ts index 5b92d44c66485..f3cdf426e9cbb 100644 --- a/test/server_integration/http/platform/config.ts +++ b/test/server_integration/http/platform/config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; // eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/test/server_integration/services/types.d.ts b/test/server_integration/services/types.d.ts index 5689100c671bb..2df95f0297f90 100644 --- a/test/server_integration/services/types.d.ts +++ b/test/server_integration/services/types.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services as kibanaCommonServices } from '../../common/services'; import { services as kibanaApiIntegrationServices } from '../../api_integration/services'; diff --git a/test/ui_capabilities/newsfeed_err/config.ts b/test/ui_capabilities/newsfeed_err/config.ts index 81448bf641ebb..e9548b41b67a0 100644 --- a/test/ui_capabilities/newsfeed_err/config.ts +++ b/test/ui_capabilities/newsfeed_err/config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; // @ts-ignore untyped module import getFunctionalConfig from '../../functional/config'; diff --git a/test/visual_regression/config.ts b/test/visual_regression/config.ts index 0d38b067f35d9..3c11a4c2689ba 100644 --- a/test/visual_regression/config.ts +++ b/test/visual_regression/config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/test/visual_regression/ftr_provider_context.d.ts b/test/visual_regression/ftr_provider_context.d.ts index 2dd5767f2270c..ba3eb370048b8 100644 --- a/test/visual_regression/ftr_provider_context.d.ts +++ b/test/visual_regression/ftr_provider_context.d.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from './services'; diff --git a/test/visual_regression/services/visual_testing/visual_testing.ts b/test/visual_regression/services/visual_testing/visual_testing.ts index d0a714d6759b5..a0d9afa90f3fe 100644 --- a/test/visual_regression/services/visual_testing/visual_testing.ts +++ b/test/visual_regression/services/visual_testing/visual_testing.ts @@ -8,7 +8,7 @@ import { postSnapshot } from '@percy/agent/dist/utils/sdk-utils'; import testSubjSelector from '@kbn/test-subj-selector'; -import { Test } from '@kbn/test/types/ftr'; +import { Test } from '@kbn/test'; import { kibanaPackageJson as pkg } from '@kbn/utils'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/plugins/apm/ftr_e2e/config.ts b/x-pack/plugins/apm/ftr_e2e/config.ts index cbbac3a5eb6a3..cace655ad7c5b 100644 --- a/x-pack/plugins/apm/ftr_e2e/config.ts +++ b/x-pack/plugins/apm/ftr_e2e/config.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; async function config({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_open.ts b/x-pack/plugins/apm/ftr_e2e/cypress_open.ts index 77416518f113b..ec52f387a8b98 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_open.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_open.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { cypressOpenTests } from './cypress_start'; async function openE2ETests({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_run.ts b/x-pack/plugins/apm/ftr_e2e/cypress_run.ts index 19ea68b817ce5..0e9efb775fc7a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_run.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_run.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { cypressRunTests } from './cypress_start'; async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_provider_context.d.ts b/x-pack/plugins/apm/ftr_e2e/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/plugins/apm/ftr_e2e/ftr_provider_context.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 5b46e7de1efa4..81cfd70a23956 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/accessibility/ftr_provider_context.d.ts b/x-pack/test/accessibility/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/accessibility/ftr_provider_context.d.ts +++ b/x-pack/test/accessibility/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 7844eaf3920c6..8647c5951b7f3 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -9,7 +9,7 @@ import path from 'path'; import getPort from 'get-port'; import fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { getAllExternalServiceSimulatorPaths } from './fixtures/plugins/actions_simulators/server/plugin'; import { getTlsWebhookServerUrls } from './lib/get_tls_webhook_servers'; diff --git a/x-pack/test/alerting_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/alerting_api_integration/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/alerting_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/alerting_api_integration/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 9fd7afbfacc40..5c998a4322480 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/api_integration/config_security_basic.ts b/x-pack/test/api_integration/config_security_basic.ts index d4ff1574fa4ad..fc32e66c63e9e 100644 --- a/x-pack/test/api_integration/config_security_basic.ts +++ b/x-pack/test/api_integration/config_security_basic.ts @@ -7,7 +7,7 @@ /* eslint-disable import/no-default-export */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { default as createTestConfig } from './config'; export default async function (context: FtrConfigProviderContext) { diff --git a/x-pack/test/api_integration/config_security_trial.ts b/x-pack/test/api_integration/config_security_trial.ts index 441305e4ce7ed..93b1eefd350e9 100644 --- a/x-pack/test/api_integration/config_security_trial.ts +++ b/x-pack/test/api_integration/config_security_trial.ts @@ -7,7 +7,7 @@ /* eslint-disable import/no-default-export */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { default as createTestConfig } from './config'; export default async function (context: FtrConfigProviderContext) { diff --git a/x-pack/test/api_integration/ftr_provider_context.d.ts b/x-pack/test/api_integration/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/api_integration_basic/config.ts b/x-pack/test/api_integration_basic/config.ts index 3e24e49d620de..446f8e07679e3 100644 --- a/x-pack/test/api_integration_basic/config.ts +++ b/x-pack/test/api_integration_basic/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xpackApiIntegrationConfig = await readConfigFile( diff --git a/x-pack/test/api_integration_basic/ftr_provider_context.d.ts b/x-pack/test/api_integration_basic/ftr_provider_context.d.ts index db8621db0136e..63c97af0ed22a 100644 --- a/x-pack/test/api_integration_basic/ftr_provider_context.d.ts +++ b/x-pack/test/api_integration_basic/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from '../api_integration/services'; diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index 732f14d2a7bc8..68a0c6b4e9f1c 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import supertest from 'supertest'; import { format, UrlObject } from 'url'; import path from 'path'; diff --git a/x-pack/test/apm_api_integration/common/ftr_provider_context.ts b/x-pack/test/apm_api_integration/common/ftr_provider_context.ts index 7ca731854152b..cac4304696431 100644 --- a/x-pack/test/apm_api_integration/common/ftr_provider_context.ts +++ b/x-pack/test/apm_api_integration/common/ftr_provider_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { FtrProviderContext as InheritedFtrProviderContext } from '../../api_integration/ftr_provider_context'; import { ApmServices } from './config'; diff --git a/x-pack/test/banners_functional/config.ts b/x-pack/test/banners_functional/config.ts index 21cce31ca5d85..a4b2867b8d7ce 100644 --- a/x-pack/test/banners_functional/config.ts +++ b/x-pack/test/banners_functional/config.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services, pageObjects } from './ftr_provider_context'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/banners_functional/ftr_provider_context.ts b/x-pack/test/banners_functional/ftr_provider_context.ts index faac2954b00f6..c641b4efcc493 100644 --- a/x-pack/test/banners_functional/ftr_provider_context.ts +++ b/x-pack/test/banners_functional/ftr_provider_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from '../functional/services'; import { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 3c81407276453..8beabe450f66e 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -6,7 +6,7 @@ */ import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import path from 'path'; import fs from 'fs'; diff --git a/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/common/ftr_provider_context.ts b/x-pack/test/common/ftr_provider_context.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/common/ftr_provider_context.ts +++ b/x-pack/test/common/ftr_provider_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 355703ae35052..659c836eb9207 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -6,7 +6,7 @@ */ import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; interface CreateTestConfigOptions { diff --git a/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/detection_engine_api_integration/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts index 71903fe6c73a8..9305431711de6 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/config.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/endpoint_api_integration_no_ingest/config.ts b/x-pack/test/endpoint_api_integration_no_ingest/config.ts index 9670d809699b7..c6837256cf61b 100644 --- a/x-pack/test/endpoint_api_integration_no_ingest/config.ts +++ b/x-pack/test/endpoint_api_integration_no_ingest/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts b/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts index db8621db0136e..63c97af0ed22a 100644 --- a/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts +++ b/x-pack/test/endpoint_api_integration_no_ingest/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from '../api_integration/services'; diff --git a/x-pack/test/examples/config.ts b/x-pack/test/examples/config.ts index fe1b5ce299447..491c23a33a3ef 100644 --- a/x-pack/test/examples/config.ts +++ b/x-pack/test/examples/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; import fs from 'fs'; // @ts-expect-error https://github.com/elastic/kibana/issues/95679 diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index 2344bdc32904a..cd47da8ef5fc3 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -7,7 +7,7 @@ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { defineDockerServersConfig } from '@kbn/test'; // Docker image to use for Fleet API integration tests. diff --git a/x-pack/test/fleet_functional/config.ts b/x-pack/test/fleet_functional/config.ts index 386f39d7ec668..15d0c72ffc603 100644 --- a/x-pack/test/fleet_functional/config.ts +++ b/x-pack/test/fleet_functional/config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/fleet_functional/ftr_provider_context.d.ts b/x-pack/test/fleet_functional/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/fleet_functional/ftr_provider_context.d.ts +++ b/x-pack/test/fleet_functional/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/functional/config_security_basic.ts b/x-pack/test/functional/config_security_basic.ts index d8d008508785a..4deb598fe8769 100644 --- a/x-pack/test/functional/config_security_basic.ts +++ b/x-pack/test/functional/config_security_basic.ts @@ -9,7 +9,7 @@ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/functional/ftr_provider_context.d.ts b/x-pack/test/functional/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/functional/ftr_provider_context.d.ts +++ b/x-pack/test/functional/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index d341a27455a3c..4623b4f4e41ae 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -7,7 +7,7 @@ import { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { Calendar } from '../../../../plugins/ml/server/models/calendar/index'; import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/test/functional/services/ml/common_api.ts b/x-pack/test/functional/services/ml/common_api.ts index 22be24548892f..02cfecfc5208f 100644 --- a/x-pack/test/functional/services/ml/common_api.ts +++ b/x-pack/test/functional/services/ml/common_api.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index f42f54116c926..b7288d5927b4c 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/services/ml/custom_urls.ts b/x-pack/test/functional/services/ml/custom_urls.ts index 7c424c021fd6c..0b24c565b2fa8 100644 --- a/x-pack/test/functional/services/ml/custom_urls.ts +++ b/x-pack/test/functional/services/ml/custom_urls.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/services/ml/dashboard_job_selection_table.ts b/x-pack/test/functional/services/ml/dashboard_job_selection_table.ts index f372928d92a50..4fc90585f1408 100644 --- a/x-pack/test/functional/services/ml/dashboard_job_selection_table.ts +++ b/x-pack/test/functional/services/ml/dashboard_job_selection_table.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; export type MlDashboardJobSelectionTable = ProvidedType< diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index 3bd3b7e2e783a..cdd91da8ff9e6 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; import { ML_JOB_FIELD_TYPES } from '../../../../plugins/ml/common/constants/field_types'; import { MlCommonUI } from './common_ui'; diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index 28d912a756acd..847730ca73548 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index 547ff782bcbe5..f967099e10fa3 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { savedSearches, dashboards } from './test_resources_data'; import { COMMON_REQUEST_HEADERS } from './common_api'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional/services/transform/management.ts b/x-pack/test/functional/services/transform/management.ts index 807c3d49e344c..0aecc695049d1 100644 --- a/x-pack/test/functional/services/transform/management.ts +++ b/x-pack/test/functional/services/transform/management.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; export type TransformManagement = ProvidedType; diff --git a/x-pack/test/functional/services/transform/security_common.ts b/x-pack/test/functional/services/transform/security_common.ts index 12b24d45f5448..bae31dffa1412 100644 --- a/x-pack/test/functional/services/transform/security_common.ts +++ b/x-pack/test/functional/services/transform/security_common.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ProvidedType } from '@kbn/test/types/ftr'; +import { ProvidedType } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/functional_basic/config.ts b/x-pack/test/functional_basic/config.ts index ce72362076da1..e1dac88436e4c 100644 --- a/x-pack/test/functional_basic/config.ts +++ b/x-pack/test/functional_basic/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); diff --git a/x-pack/test/functional_basic/ftr_provider_context.d.ts b/x-pack/test/functional_basic/ftr_provider_context.d.ts index 0225c54768af5..66d4e37b795ca 100644 --- a/x-pack/test/functional_basic/ftr_provider_context.d.ts +++ b/x-pack/test/functional_basic/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from '../functional/services'; diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts index 81870a948dc15..738285b4ff40f 100644 --- a/x-pack/test/functional_cors/config.ts +++ b/x-pack/test/functional_cors/config.ts @@ -7,7 +7,7 @@ import Url from 'url'; import Path from 'path'; -import type { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import type { FtrConfigProviderContext } from '@kbn/test'; import { kbnTestConfig } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/functional_cors/ftr_provider_context.d.ts b/x-pack/test/functional_cors/ftr_provider_context.d.ts index b1301d9ee8c49..d6c0afa5ceffd 100644 --- a/x-pack/test/functional_cors/ftr_provider_context.d.ts +++ b/x-pack/test/functional_cors/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from './services'; diff --git a/x-pack/test/functional_embedded/config.firefox.ts b/x-pack/test/functional_embedded/config.firefox.ts index a8a16f5c59b9e..49359d37673de 100644 --- a/x-pack/test/functional_embedded/config.firefox.ts +++ b/x-pack/test/functional_embedded/config.firefox.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const chromeConfig = await readConfigFile(require.resolve('./config')); diff --git a/x-pack/test/functional_embedded/config.ts b/x-pack/test/functional_embedded/config.ts index 33656eb857f58..868d53ee17ee9 100644 --- a/x-pack/test/functional_embedded/config.ts +++ b/x-pack/test/functional_embedded/config.ts @@ -8,7 +8,7 @@ import Fs from 'fs'; import { resolve } from 'path'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/functional_embedded/ftr_provider_context.d.ts b/x-pack/test/functional_embedded/ftr_provider_context.d.ts index b1301d9ee8c49..d6c0afa5ceffd 100644 --- a/x-pack/test/functional_embedded/ftr_provider_context.d.ts +++ b/x-pack/test/functional_embedded/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from './services'; diff --git a/x-pack/test/functional_enterprise_search/base_config.ts b/x-pack/test/functional_enterprise_search/base_config.ts index 5687bd48c9bad..2c21ccf5c5c39 100644 --- a/x-pack/test/functional_enterprise_search/base_config.ts +++ b/x-pack/test/functional_enterprise_search/base_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts b/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts +++ b/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/functional_enterprise_search/with_host_configured.config.ts b/x-pack/test/functional_enterprise_search/with_host_configured.config.ts index 66e638b69f5b8..1da62ffe56a5b 100644 --- a/x-pack/test/functional_enterprise_search/with_host_configured.config.ts +++ b/x-pack/test/functional_enterprise_search/with_host_configured.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const baseConfig = await readConfigFile(require.resolve('./base_config')); diff --git a/x-pack/test/functional_enterprise_search/without_host_configured.config.ts b/x-pack/test/functional_enterprise_search/without_host_configured.config.ts index 6d9fe32312d20..f5af2bddd8531 100644 --- a/x-pack/test/functional_enterprise_search/without_host_configured.config.ts +++ b/x-pack/test/functional_enterprise_search/without_host_configured.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const baseConfig = await readConfigFile(require.resolve('./base_config')); diff --git a/x-pack/test/functional_vis_wizard/config.ts b/x-pack/test/functional_vis_wizard/config.ts index 643718d78df08..523b59b6ccd1c 100644 --- a/x-pack/test/functional_vis_wizard/config.ts +++ b/x-pack/test/functional_vis_wizard/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); diff --git a/x-pack/test/functional_vis_wizard/ftr_provider_context.d.ts b/x-pack/test/functional_vis_wizard/ftr_provider_context.d.ts index 89681747cbb55..ab1e999222808 100644 --- a/x-pack/test/functional_vis_wizard/ftr_provider_context.d.ts +++ b/x-pack/test/functional_vis_wizard/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from '../functional/services'; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 91a349e1bf44a..3ed382053f561 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -8,7 +8,7 @@ import Fs from 'fs'; import { resolve, join } from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; // .server-log is specifically not enabled diff --git a/x-pack/test/functional_with_es_ssl/ftr_provider_context.d.ts b/x-pack/test/functional_with_es_ssl/ftr_provider_context.d.ts index 576d296e72e88..821731a07f457 100644 --- a/x-pack/test/functional_with_es_ssl/ftr_provider_context.d.ts +++ b/x-pack/test/functional_with_es_ssl/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from '../functional/services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/licensing_plugin/config.public.ts b/x-pack/test/licensing_plugin/config.public.ts index 0de536d7125ca..a9df9f6ad1e96 100644 --- a/x-pack/test/licensing_plugin/config.public.ts +++ b/x-pack/test/licensing_plugin/config.public.ts @@ -8,7 +8,7 @@ import path from 'path'; // @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { KIBANA_ROOT } from '@kbn/test'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const commonConfig = await readConfigFile(require.resolve('./config')); diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index ccd9494052817..155d761020b29 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services, pageObjects } from './services'; const license = 'basic'; diff --git a/x-pack/test/licensing_plugin/services.ts b/x-pack/test/licensing_plugin/services.ts index eb5b22731718c..fe98c9f23d174 100644 --- a/x-pack/test/licensing_plugin/services.ts +++ b/x-pack/test/licensing_plugin/services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services as functionalTestServices } from '../functional/services'; import { services as kibanaApiIntegrationServices } from '../api_integration/services'; diff --git a/x-pack/test/lists_api_integration/common/config.ts b/x-pack/test/lists_api_integration/common/config.ts index 693155d376b53..24b47472d7945 100644 --- a/x-pack/test/lists_api_integration/common/config.ts +++ b/x-pack/test/lists_api_integration/common/config.ts @@ -7,7 +7,7 @@ import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; interface CreateTestConfigOptions { diff --git a/x-pack/test/lists_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/lists_api_integration/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/lists_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/lists_api_integration/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/load/config.ts b/x-pack/test/load/config.ts index f8de5c28b8a10..54789b56d9912 100644 --- a/x-pack/test/load/config.ts +++ b/x-pack/test/load/config.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { GatlingTestRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/observability_api_integration/common/config.ts b/x-pack/test/observability_api_integration/common/config.ts index 748b6ef7215e6..83249182084f3 100644 --- a/x-pack/test/observability_api_integration/common/config.ts +++ b/x-pack/test/observability_api_integration/common/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; interface Settings { license: 'basic' | 'trial'; diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index 87f8b77028bda..09bec3330c390 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -7,7 +7,7 @@ import path from 'path'; import fs from 'fs'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/plugin_api_integration/ftr_provider_context.d.ts b/x-pack/test/plugin_api_integration/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/plugin_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/plugin_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/plugin_api_perf/ftr_provider_context.d.ts b/x-pack/test/plugin_api_perf/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/plugin_api_perf/ftr_provider_context.d.ts +++ b/x-pack/test/plugin_api_perf/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 104d11eb87f7c..8b0ad12891dc3 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -9,7 +9,7 @@ import { resolve } from 'path'; import fs from 'fs'; // @ts-expect-error https://github.com/elastic/kibana/issues/95679 import { KIBANA_ROOT } from '@kbn/test'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/plugin_functional/ftr_provider_context.d.ts b/x-pack/test/plugin_functional/ftr_provider_context.d.ts index 2dce44a533405..13bd4b46607c4 100644 --- a/x-pack/test/plugin_functional/ftr_provider_context.d.ts +++ b/x-pack/test/plugin_functional/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts b/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts index 671866cad6ff5..647664d640466 100644 --- a/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts index 623799c84d860..0d1839c7a138f 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; import { ReportingAPIProvider } from './services'; diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index b962ab30876a5..dfd79916b5ce0 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('./reporting_and_security.config')); diff --git a/x-pack/test/reporting_functional/ftr_provider_context.d.ts b/x-pack/test/reporting_functional/ftr_provider_context.d.ts index 58ebd71086130..e66e69e5ab2f7 100644 --- a/x-pack/test/reporting_functional/ftr_provider_context.d.ts +++ b/x-pack/test/reporting_functional/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from './services'; diff --git a/x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts b/x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts index 214667ff71c0d..a65755e97b0c9 100644 --- a/x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts +++ b/x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/reporting_functional/reporting_and_security.config.ts b/x-pack/test/reporting_functional/reporting_and_security.config.ts index 1f9ec5754e0bd..536695b4c6dc9 100644 --- a/x-pack/test/reporting_functional/reporting_and_security.config.ts +++ b/x-pack/test/reporting_functional/reporting_and_security.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; import { ReportingAPIProvider } from '../reporting_api_integration/services'; import { ReportingFunctionalProvider } from './services'; diff --git a/x-pack/test/reporting_functional/reporting_without_security.config.ts b/x-pack/test/reporting_functional/reporting_without_security.config.ts index b88c611543953..0269f57bf08cb 100644 --- a/x-pack/test/reporting_functional/reporting_without_security.config.ts +++ b/x-pack/test/reporting_functional/reporting_without_security.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { resolve } from 'path'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/saved_object_api_integration/common/config.ts b/x-pack/test/saved_object_api_integration/common/config.ts index ee219e9d246a7..f83afb4db2412 100644 --- a/x-pack/test/saved_object_api_integration/common/config.ts +++ b/x-pack/test/saved_object_api_integration/common/config.ts @@ -8,7 +8,7 @@ import path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/saved_object_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/saved_object_api_integration/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/saved_object_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/saved_object_api_integration/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts index e7b015ff92296..08ba10b64e579 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/config.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/services.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/services.ts index 1d26b3dd35c96..194d6ec533066 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/services.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services as apiIntegrationServices } from '../../../api_integration/services'; export const services = { diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts index 11f776356df69..ebdb055bd5e89 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/config.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/services.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/services.ts index 1d26b3dd35c96..194d6ec533066 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/services.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services as apiIntegrationServices } from '../../../api_integration/services'; export const services = { diff --git a/x-pack/test/saved_object_tagging/functional/config.ts b/x-pack/test/saved_object_tagging/functional/config.ts index ccb55d37f5fa0..0044063e18c69 100644 --- a/x-pack/test/saved_object_tagging/functional/config.ts +++ b/x-pack/test/saved_object_tagging/functional/config.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services, pageObjects } from './ftr_provider_context'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/saved_object_tagging/functional/ftr_provider_context.ts b/x-pack/test/saved_object_tagging/functional/ftr_provider_context.ts index 849dca0989335..643f34a59f69c 100644 --- a/x-pack/test/saved_object_tagging/functional/ftr_provider_context.ts +++ b/x-pack/test/saved_object_tagging/functional/ftr_provider_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from '../../functional/services'; import { pageObjects } from '../../functional/page_objects'; diff --git a/x-pack/test/saved_objects_field_count/config.ts b/x-pack/test/saved_objects_field_count/config.ts index ab9c88ae86330..8144bac35ec7b 100644 --- a/x-pack/test/saved_objects_field_count/config.ts +++ b/x-pack/test/saved_objects_field_count/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { testRunner } from './runner'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/search_sessions_integration/config.ts b/x-pack/test/search_sessions_integration/config.ts index 831257738f64c..1e2d648712098 100644 --- a/x-pack/test/search_sessions_integration/config.ts +++ b/x-pack/test/search_sessions_integration/config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from '../functional/services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/search_sessions_integration/ftr_provider_context.d.ts b/x-pack/test/search_sessions_integration/ftr_provider_context.d.ts index c65eeb91fe5b9..8ce17281223e1 100644 --- a/x-pack/test/search_sessions_integration/ftr_provider_context.d.ts +++ b/x-pack/test/search_sessions_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from '../functional/services'; diff --git a/x-pack/test/security_api_integration/anonymous.config.ts b/x-pack/test/security_api_integration/anonymous.config.ts index ab2573822d440..80ac45507de73 100644 --- a/x-pack/test/security_api_integration/anonymous.config.ts +++ b/x-pack/test/security_api_integration/anonymous.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( diff --git a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts index 25fb8154dd8d4..60691769729fa 100644 --- a/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts +++ b/x-pack/test/security_api_integration/anonymous_es_anonymous.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const anonymousAPITestsConfig = await readConfigFile(require.resolve('./anonymous.config.ts')); diff --git a/x-pack/test/security_api_integration/audit.config.ts b/x-pack/test/security_api_integration/audit.config.ts index 60b1c0bf1fa80..02b3870c18f89 100644 --- a/x-pack/test/security_api_integration/audit.config.ts +++ b/x-pack/test/security_api_integration/audit.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/security_api_integration/ftr_provider_context.d.ts b/x-pack/test/security_api_integration/ftr_provider_context.d.ts index 671866cad6ff5..647664d640466 100644 --- a/x-pack/test/security_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/security_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/security_api_integration/kerberos.config.ts b/x-pack/test/security_api_integration/kerberos.config.ts index c3f647aadc6d1..7dba77e61999e 100644 --- a/x-pack/test/security_api_integration/kerberos.config.ts +++ b/x-pack/test/security_api_integration/kerberos.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts b/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts index 40ec36e51f702..355f0e90bcd91 100644 --- a/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts +++ b/x-pack/test/security_api_integration/kerberos_anonymous_access.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kerberosAPITestsConfig = await readConfigFile(require.resolve('./kerberos.config.ts')); diff --git a/x-pack/test/security_api_integration/login_selector.config.ts b/x-pack/test/security_api_integration/login_selector.config.ts index f9ef0903b39aa..a21bc0d58733e 100644 --- a/x-pack/test/security_api_integration/login_selector.config.ts +++ b/x-pack/test/security_api_integration/login_selector.config.ts @@ -8,7 +8,7 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaAPITestsConfig = await readConfigFile( diff --git a/x-pack/test/security_api_integration/oidc.config.ts b/x-pack/test/security_api_integration/oidc.config.ts index 05fbaa4122818..a475d77aa568b 100644 --- a/x-pack/test/security_api_integration/oidc.config.ts +++ b/x-pack/test/security_api_integration/oidc.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts b/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts index 685b71358846e..3b9edcbec6826 100644 --- a/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts +++ b/x-pack/test/security_api_integration/oidc_implicit_flow.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const oidcAPITestsConfig = await readConfigFile(require.resolve('./oidc.config.ts')); diff --git a/x-pack/test/security_api_integration/pki.config.ts b/x-pack/test/security_api_integration/pki.config.ts index c284ddc800327..d920a4375753c 100644 --- a/x-pack/test/security_api_integration/pki.config.ts +++ b/x-pack/test/security_api_integration/pki.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import { services } from './services'; diff --git a/x-pack/test/security_api_integration/saml.config.ts b/x-pack/test/security_api_integration/saml.config.ts index a3e5c4a3419c8..c8de50008c3bb 100644 --- a/x-pack/test/security_api_integration/saml.config.ts +++ b/x-pack/test/security_api_integration/saml.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_api_integration/session_idle.config.ts b/x-pack/test/security_api_integration/session_idle.config.ts index 92549b8369365..77d91f6df3cef 100644 --- a/x-pack/test/security_api_integration/session_idle.config.ts +++ b/x-pack/test/security_api_integration/session_idle.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_invalidate.config.ts b/x-pack/test/security_api_integration/session_invalidate.config.ts index 82510062035a9..7d69ca50c0320 100644 --- a/x-pack/test/security_api_integration/session_invalidate.config.ts +++ b/x-pack/test/security_api_integration/session_invalidate.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/session_lifespan.config.ts b/x-pack/test/security_api_integration/session_lifespan.config.ts index 1dbcfbf96d551..b692f10bad5a6 100644 --- a/x-pack/test/security_api_integration/session_lifespan.config.ts +++ b/x-pack/test/security_api_integration/session_lifespan.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; // the default export of config files must be a config provider diff --git a/x-pack/test/security_api_integration/token.config.ts b/x-pack/test/security_api_integration/token.config.ts index 170d98bf5efa4..54efd77ca8ae9 100644 --- a/x-pack/test/security_api_integration/token.config.ts +++ b/x-pack/test/security_api_integration/token.config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { diff --git a/x-pack/test/security_functional/ftr_provider_context.d.ts b/x-pack/test/security_functional/ftr_provider_context.d.ts index 0225c54768af5..66d4e37b795ca 100644 --- a/x-pack/test/security_functional/ftr_provider_context.d.ts +++ b/x-pack/test/security_functional/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from '../functional/page_objects'; import { services } from '../functional/services'; diff --git a/x-pack/test/security_functional/login_selector.config.ts b/x-pack/test/security_functional/login_selector.config.ts index 6f7a2a8b3de70..e2ddf8dacb79c 100644 --- a/x-pack/test/security_functional/login_selector.config.ts +++ b/x-pack/test/security_functional/login_selector.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from '../functional/services'; import { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/security_functional/oidc.config.ts b/x-pack/test/security_functional/oidc.config.ts index 056d7b8197c9a..db8799ba1acf7 100644 --- a/x-pack/test/security_functional/oidc.config.ts +++ b/x-pack/test/security_functional/oidc.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from '../functional/services'; import { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/security_functional/saml.config.ts b/x-pack/test/security_functional/saml.config.ts index 50948ce6d411e..a983e2747239c 100644 --- a/x-pack/test/security_functional/saml.config.ts +++ b/x-pack/test/security_functional/saml.config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from '../functional/services'; import { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/security_solution_cypress/cli_config.ts b/x-pack/test/security_solution_cypress/cli_config.ts index 2fc32a5210813..443c3dd423409 100644 --- a/x-pack/test/security_solution_cypress/cli_config.ts +++ b/x-pack/test/security_solution_cypress/cli_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { SecuritySolutionCypressCliTestRunner } from './runner'; diff --git a/x-pack/test/security_solution_cypress/config.firefox.ts b/x-pack/test/security_solution_cypress/config.firefox.ts index 5d11c6d9364a1..9c9f2c2314a31 100644 --- a/x-pack/test/security_solution_cypress/config.firefox.ts +++ b/x-pack/test/security_solution_cypress/config.firefox.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index e096bafc9e262..95743369de0d7 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; diff --git a/x-pack/test/security_solution_cypress/ftr_provider_context.d.ts b/x-pack/test/security_solution_cypress/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/security_solution_cypress/ftr_provider_context.d.ts +++ b/x-pack/test/security_solution_cypress/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/security_solution_cypress/visual_config.ts b/x-pack/test/security_solution_cypress/visual_config.ts index 489dd1e626221..b6808a4c35638 100644 --- a/x-pack/test/security_solution_cypress/visual_config.ts +++ b/x-pack/test/security_solution_cypress/visual_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { SecuritySolutionCypressVisualTestRunner } from './runner'; diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 77a63acc389e2..188cccac9301b 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -6,7 +6,7 @@ */ import { resolve } from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; import { diff --git a/x-pack/test/security_solution_endpoint/ftr_provider_context.d.ts b/x-pack/test/security_solution_endpoint/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/security_solution_endpoint/ftr_provider_context.d.ts +++ b/x-pack/test/security_solution_endpoint/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/security_solution_endpoint_api_int/config.ts b/x-pack/test/security_solution_endpoint_api_int/config.ts index a2eec01046161..d53365a8b6ec6 100644 --- a/x-pack/test/security_solution_endpoint_api_int/config.ts +++ b/x-pack/test/security_solution_endpoint_api_int/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { createEndpointDockerConfig, getRegistryUrlAsArray } from './registry'; import { services } from './services'; diff --git a/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts b/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts +++ b/x-pack/test/security_solution_endpoint_api_int/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/spaces_api_integration/common/config.ts b/x-pack/test/spaces_api_integration/common/config.ts index 2daebba801200..d1e4dae76b636 100644 --- a/x-pack/test/spaces_api_integration/common/config.ts +++ b/x-pack/test/spaces_api_integration/common/config.ts @@ -7,7 +7,7 @@ import path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; interface CreateTestConfigOptions { license: string; diff --git a/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts +++ b/x-pack/test/spaces_api_integration/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/ui_capabilities/common/config.ts b/x-pack/test/ui_capabilities/common/config.ts index 0c40df181ffdc..9c0ce92755e8e 100644 --- a/x-pack/test/ui_capabilities/common/config.ts +++ b/x-pack/test/ui_capabilities/common/config.ts @@ -6,7 +6,7 @@ */ import path from 'path'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/ui_capabilities/common/ftr_provider_context.d.ts b/x-pack/test/ui_capabilities/common/ftr_provider_context.d.ts index d612023be9160..aa56557c09df8 100644 --- a/x-pack/test/ui_capabilities/common/ftr_provider_context.d.ts +++ b/x-pack/test/ui_capabilities/common/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/upgrade/config.ts b/x-pack/test/upgrade/config.ts index 86555335ba47b..472b83fe7a934 100644 --- a/x-pack/test/upgrade/config.ts +++ b/x-pack/test/upgrade/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { ReportingAPIProvider } from './reporting_services'; diff --git a/x-pack/test/upgrade/ftr_provider_context.d.ts b/x-pack/test/upgrade/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/upgrade/ftr_provider_context.d.ts +++ b/x-pack/test/upgrade/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; diff --git a/x-pack/test/usage_collection/config.ts b/x-pack/test/usage_collection/config.ts index df1d1d9fd21f7..beb934219422a 100644 --- a/x-pack/test/usage_collection/config.ts +++ b/x-pack/test/usage_collection/config.ts @@ -7,7 +7,7 @@ import { resolve } from 'path'; import fs from 'fs'; -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/usage_collection/ftr_provider_context.d.ts b/x-pack/test/usage_collection/ftr_provider_context.d.ts index 2dce44a533405..13bd4b46607c4 100644 --- a/x-pack/test/usage_collection/ftr_provider_context.d.ts +++ b/x-pack/test/usage_collection/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { services } from './services'; import { pageObjects } from './page_objects'; diff --git a/x-pack/test/visual_regression/config.ts b/x-pack/test/visual_regression/config.ts index 5d36f200d149f..c211918ef8e52 100644 --- a/x-pack/test/visual_regression/config.ts +++ b/x-pack/test/visual_regression/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { FtrConfigProviderContext } from '@kbn/test'; import { services } from './services'; diff --git a/x-pack/test/visual_regression/ftr_provider_context.d.ts b/x-pack/test/visual_regression/ftr_provider_context.d.ts index ec28c00e72e47..24f5087ef7fe2 100644 --- a/x-pack/test/visual_regression/ftr_provider_context.d.ts +++ b/x-pack/test/visual_regression/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { GenericFtrProviderContext } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; From b461d8279866c66f0c1ea63cd0328d46d08e4f78 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 17 May 2021 18:48:41 +0100 Subject: [PATCH 089/186] skip failing es promotion suite (#99915) --- .../security_and_spaces/tests/finalize_signals_migrations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts index e65086b43a377..6ab23edae9b3b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts @@ -47,7 +47,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('Finalizing signals migrations', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 + describe.skip('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; From 52010f49c6e3209290f6123a2f9d5b3c10afcde1 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 17 May 2021 15:53:20 -0400 Subject: [PATCH 090/186] skip flaky suite (#100236) --- .../security_solution_endpoint/apps/endpoint/policy_details.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index bd8ce806609b3..4c40cb005e8c4 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -21,7 +21,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // Failing: See https://github.com/elastic/kibana/issues/100236 + describe.skip('When on the Endpoint Policy Details Page', function () { describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); From 125d587fd44b468becd0c980507a992b10a25ad1 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 17 May 2021 21:04:35 +0100 Subject: [PATCH 091/186] [ML] Fixing use_null setting in advanced job wizard (#100028) * [ML] Fixing use_null setting in advanced job wizard * fixing types * fixing false checks for detector fields Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../job_creator/advanced_job_creator.ts | 23 ++++++++++++++----- .../common/job_creator/util/general.ts | 5 ++-- .../advanced_detector_modal.tsx | 1 + .../advanced_view/metric_selection.tsx | 7 ++++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index aac36f3e4f573..45b26226def8f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -32,6 +32,7 @@ export interface RichDetector { excludeFrequent: estypes.ExcludeFrequent | null; description: string | null; customRules: CustomRule[] | null; + useNull: boolean | null; } export class AdvancedJobCreator extends JobCreator { @@ -58,7 +59,8 @@ export class AdvancedJobCreator extends JobCreator { overField: SplitField, partitionField: SplitField, excludeFrequent: estypes.ExcludeFrequent | null, - description: string | null + description: string | null, + useNull: boolean | null ) { // addDetector doesn't support adding new custom rules. // this will be added in the future once it's supported in the UI @@ -71,7 +73,8 @@ export class AdvancedJobCreator extends JobCreator { partitionField, excludeFrequent, description, - customRules + customRules, + useNull ); this._addDetector(detector, agg, field); @@ -86,7 +89,8 @@ export class AdvancedJobCreator extends JobCreator { partitionField: SplitField, excludeFrequent: estypes.ExcludeFrequent | null, description: string | null, - index: number + index: number, + useNull: boolean | null ) { const customRules = this._detectors[index] !== undefined ? this._detectors[index].custom_rules || null : null; @@ -99,7 +103,8 @@ export class AdvancedJobCreator extends JobCreator { partitionField, excludeFrequent, description, - customRules + customRules, + useNull ); this._editDetector(detector, agg, field, index); @@ -117,7 +122,8 @@ export class AdvancedJobCreator extends JobCreator { partitionField: SplitField, excludeFrequent: estypes.ExcludeFrequent | null, description: string | null, - customRules: CustomRule[] | null + customRules: CustomRule[] | null, + useNull: boolean | null ): { detector: Detector; richDetector: RichDetector } { const detector: Detector = createBasicDetector(agg, field); @@ -139,6 +145,9 @@ export class AdvancedJobCreator extends JobCreator { if (customRules !== null) { detector.custom_rules = customRules; } + if (useNull !== null) { + detector.use_null = useNull; + } const richDetector: RichDetector = { agg, @@ -149,6 +158,7 @@ export class AdvancedJobCreator extends JobCreator { excludeFrequent, description, customRules, + useNull, }; return { detector, richDetector }; @@ -209,7 +219,8 @@ export class AdvancedJobCreator extends JobCreator { dtr.overField, dtr.partitionField, dtr.excludeFrequent, - dtr.description + dtr.description, + dtr.useNull ); } }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 3f306f9bcc996..bab6800c08335 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -79,8 +79,9 @@ export function getRichDetectors( byField, overField, partitionField, - excludeFrequent: d.exclude_frequent || null, - description: d.detector_description || null, + excludeFrequent: d.exclude_frequent ?? null, + description: d.detector_description ?? null, + useNull: d.use_null ?? null, }; }); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index d3108eef04983..2b1a35bcb8c46 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -177,6 +177,7 @@ export const AdvancedDetectorModal: FC = ({ : null, description: descriptionOption !== '' ? descriptionOption : null, customRules: null, + useNull: null, }; setDetector(dtr); setDescriptionPlaceholder(dtr); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx index b4508af7803dd..8f53e1283faa0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/metric_selection.tsx @@ -29,6 +29,7 @@ const emptyRichDetector: RichDetector = { excludeFrequent: null, description: null, customRules: null, + useNull: null, }; export const AdvancedDetectors: FC = ({ setIsValid }) => { @@ -51,7 +52,8 @@ export const AdvancedDetectors: FC = ({ setIsValid }) => { dtr.overField, dtr.partitionField, dtr.excludeFrequent, - dtr.description + dtr.description, + dtr.useNull ); } else { jobCreator.editDetector( @@ -62,7 +64,8 @@ export const AdvancedDetectors: FC = ({ setIsValid }) => { dtr.partitionField, dtr.excludeFrequent, dtr.description, - index + index, + dtr.useNull ); } jobCreatorUpdate(); From 0384134727851c7d9c48d8b33796d4d6a3a023d7 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 17 May 2021 16:44:30 -0400 Subject: [PATCH 092/186] [Uptime] [Synthetics Integration] Add functional tests for Synthetics Integration (#100161) * add functional tests for synthetics fleet package --- .../components/fleet_package/combo_box.tsx | 3 +- .../fleet_package/custom_fields.tsx | 8 + .../fleet_package/http_advanced_fields.tsx | 12 + .../index_response_body_field.tsx | 2 +- .../fleet_package/request_body_field.tsx | 4 + .../fleet_package/tcp_advanced_fields.tsx | 11 +- .../components/fleet_package/tls_fields.tsx | 5 + x-pack/test/functional/apps/uptime/index.ts | 1 + .../apps/uptime/synthetics_integration.ts | 442 ++++++++++++++++++ x-pack/test/functional/config.js | 3 + x-pack/test/functional/page_objects/index.ts | 2 + .../synthetics_integration_page.ts | 384 +++++++++++++++ .../services/uptime/synthetics_package.ts | 176 +++++++ .../test/functional/services/uptime/uptime.ts | 3 + 14 files changed, 1052 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional/apps/uptime/synthetics_integration.ts create mode 100644 x-pack/test/functional/page_objects/synthetics_integration_page.ts create mode 100644 x-pack/test/functional/services/uptime/synthetics_package.ts diff --git a/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx b/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx index 12ee154dbcac4..7a1df79b0a59a 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/combo_box.tsx @@ -13,7 +13,7 @@ export interface Props { selectedOptions: string[]; } -export const ComboBox = ({ onChange, selectedOptions }: Props) => { +export const ComboBox = ({ onChange, selectedOptions, ...props }: Props) => { const [formattedSelectedOptions, setSelectedOptions] = useState< Array> >(selectedOptions.map((option) => ({ label: option, key: option }))); @@ -66,6 +66,7 @@ export const ComboBox = ({ onChange, selectedOptions }: Props) => { onChange={onOptionsChange} onSearchChange={onSearchChange} isInvalid={isInvalid} + {...props} /> ); }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 0bc2fc8823cec..e6703a6eaa97c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -76,6 +76,7 @@ export const CustomFields = memo( defaultMessage="Configure your monitor with the following options." /> } + data-test-subj="monitorSettingsSection" > @@ -104,6 +105,7 @@ export const CustomFields = memo( configKey: ConfigKeys.MONITOR_TYPE, }) } + data-test-subj="syntheticsMonitorTypeField" /> )} @@ -128,6 +130,7 @@ export const CustomFields = memo( onChange={(event) => handleInputChange({ value: event.target.value, configKey: ConfigKeys.URLS }) } + data-test-subj="syntheticsUrlField" /> )} @@ -155,6 +158,7 @@ export const CustomFields = memo( configKey: ConfigKeys.HOSTS, }) } + data-test-subj="syntheticsTCPHostField" /> )} @@ -182,6 +186,7 @@ export const CustomFields = memo( configKey: ConfigKeys.HOSTS, }) } + data-test-subj="syntheticsICMPHostField" /> )} @@ -268,6 +273,7 @@ export const CustomFields = memo( configKey: ConfigKeys.APM_SERVICE_NAME, }) } + data-test-subj="syntheticsAPMServiceName" /> {isHTTP && ( @@ -364,6 +370,7 @@ export const CustomFields = memo( handleInputChange({ value, configKey: ConfigKeys.TAGS })} + data-test-subj="syntheticsTags" /> @@ -385,6 +392,7 @@ export const CustomFields = memo( defaultMessage="Configure TLS options, including verification mode, certificate authorities, and client certificates." /> } + data-test-subj="syntheticsIsTLSEnabled" > (({ validate }) => { defaultMessage="Advanced HTTP options" /> } + data-test-subj="syntheticsHTTPAdvancedFieldsAccordion" > (({ validate }) => { defaultMessage="Configure an optional request to send to the remote host including method, body, and headers." /> } + data-test-subj="httpAdvancedFieldsSection" > (({ validate }) => { configKey: ConfigKeys.USERNAME, }) } + data-test-subj="syntheticsUsername" /> (({ validate }) => { configKey: ConfigKeys.PASSWORD, }) } + data-test-subj="syntheticsPassword" /> (({ validate }) => { configKey: ConfigKeys.PROXY_URL, }) } + data-test-subj="syntheticsProxyUrl" /> (({ validate }) => { configKey: ConfigKeys.REQUEST_METHOD_CHECK, }) } + data-test-subj="syntheticsRequestMethod" /> (({ validate }) => { defaultMessage="A dictionary of additional HTTP headers to send. By default the client will set the User-Agent header to identify itself." /> } + data-test-subj="syntheticsRequestHeaders" > (({ validate }) => { http.response.body.headers } + data-test-subj="syntheticsIndexResponseHeaders" > (({ validate }) => { configKey: ConfigKeys.RESPONSE_STATUS_CHECK, }) } + data-test-subj="syntheticsResponseStatusCheck" /> (({ validate }) => { defaultMessage="A list of expected response headers." /> } + data-test-subj="syntheticsResponseHeaders" > (({ validate }) => { }), [handleInputChange] )} + data-test-subj="syntheticsResponseBodyCheckPositive" /> (({ validate }) => { }), [handleInputChange] )} + data-test-subj="syntheticsResponseBodyCheckNegative" /> diff --git a/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx b/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx index a82e7a0938078..fc53b275f0828 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/index_response_body_field.tsx @@ -38,7 +38,7 @@ export const ResponseBodyIndexField = ({ defaultValue, onChange }: Props) => { return ( - + { { id: Mode.TEXT, name: modeLabels[Mode.TEXT], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.TEXT}`, content: ( { { id: Mode.JSON, name: modeLabels[Mode.JSON], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.JSON}`, content: ( { { id: Mode.XML, name: modeLabels[Mode.XML], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.XML}`, content: ( { { id: Mode.FORM, name: modeLabels[Mode.FORM], + 'data-test-subj': `syntheticsRequestBodyTab__${Mode.FORM}`, content: ( { ); return ( - + { configKey: ConfigKeys.PROXY_URL, }) } + data-test-subj="syntheticsProxyUrl" /> {!!fields[ConfigKeys.PROXY_URL] && ( - + { }), [handleInputChange] )} + data-test-subj="syntheticsTCPRequestSendCheck" /> @@ -166,6 +172,7 @@ export const TCPAdvancedFields = () => { }), [handleInputChange] )} + data-test-subj="syntheticsTCPResponseReceiveCheck" /> diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx index de8879ec3a819..a2db0d99088f7 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tls_fields.tsx @@ -139,6 +139,7 @@ export const TLSFields: React.FunctionComponent<{ }, })); }} + data-test-subj="syntheticsTLSVerificationMode" /> {fields[ConfigKeys.TLS_VERIFICATION_MODE].value === VerificationMode.NONE && ( @@ -229,6 +230,7 @@ export const TLSFields: React.FunctionComponent<{ }, })); }} + data-test-subj="syntheticsTLSCA" /> diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index f96d2d0255d2a..0b02fd2bf322b 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -59,6 +59,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./locations')); loadTestFile(require.resolve('./settings')); loadTestFile(require.resolve('./certificates')); + loadTestFile(require.resolve('./synthetics_integration')); }); describe('with generated data but no data reset', () => { diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts new file mode 100644 index 0000000000000..52ec81b8bf7db --- /dev/null +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -0,0 +1,442 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { FullAgentPolicy } from '../../../../plugins/fleet/common'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const monitorName = 'Sample Synthetics integration'; + + const uptimePage = getPageObjects(['syntheticsIntegration']); + const testSubjects = getService('testSubjects'); + const uptimeService = getService('uptime'); + + const generatePolicy = ({ + agentFullPolicy, + version, + monitorType, + name, + config, + }: { + agentFullPolicy: FullAgentPolicy; + version: string; + monitorType: string; + name: string; + config: Record; + }) => ({ + data_stream: { + namespace: 'default', + }, + id: agentFullPolicy.inputs[0].id, + meta: { + package: { + name: 'synthetics', + version, + }, + }, + name, + revision: 1, + streams: [ + { + data_stream: { + dataset: monitorType, + type: 'synthetics', + }, + id: `${agentFullPolicy.inputs[0]?.streams?.[0]?.id}`, + name, + type: monitorType, + processors: [ + { + add_observer_metadata: { + geo: { + name: 'Fleet managed', + }, + }, + }, + { + add_fields: { + fields: { + 'monitor.fleet_managed': true, + }, + target: '', + }, + }, + ], + ...config, + }, + ], + type: `synthetics/${monitorType}`, + use_output: 'default', + }); + + describe('When on the Synthetics Integration Policy Create Page', function () { + this.tags(['ciGroup6']); + const basicConfig = { + name: monitorName, + apmServiceName: 'Sample APM Service', + tags: 'sample tag', + }; + + const generateHTTPConfig = (url: string) => ({ + ...basicConfig, + url, + }); + + const generateTCPorICMPConfig = (host: string) => ({ + ...basicConfig, + host, + }); + + describe('displays custom UI', () => { + before(async () => { + const version = await uptimeService.syntheticsPackage.getSyntheticsPackageVersion(); + await uptimePage.syntheticsIntegration.navigateToPackagePage(version!); + }); + + it('should display policy view', async () => { + await uptimePage.syntheticsIntegration.ensureIsOnPackagePage(); + }); + + it('prevent saving when integration name, url/host, or schedule is missing', async () => { + const saveButton = await uptimePage.syntheticsIntegration.findSaveButton(); + await saveButton.click(); + + await testSubjects.missingOrFail('packagePolicyCreateSuccessToast'); + }); + }); + + describe('create new policy', () => { + let version: string; + before(async () => { + await uptimeService.syntheticsPackage.deletePolicyByName('system-1'); + }); + + beforeEach(async () => { + version = (await uptimeService.syntheticsPackage.getSyntheticsPackageVersion())!; + await uptimePage.syntheticsIntegration.navigateToPackagePage(version!); + await uptimeService.syntheticsPackage.deletePolicyByName(monitorName); + }); + + afterEach(async () => { + await uptimeService.syntheticsPackage.deletePolicyByName(monitorName); + }); + + it('allows saving when user enters a valid integration name and url/host', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: '@every 3m', + timeout: '16s', + urls: config.url, + 'service.name': config.apmServiceName, + tags: [config.tags], + 'check.request.method': 'GET', + }, + }), + ]); + }); + + it('allows enabling tls with defaults', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + await uptimePage.syntheticsIntegration.enableTLS(); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'check.request.method': 'GET', + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: '@every 3m', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': 'full', + timeout: '16s', + urls: config.url, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows configuring tls', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + + const tlsConfig = { + verificationMode: 'strict', + ca: 'ca', + cert: 'cert', + certKey: 'certKey', + certKeyPassphrase: 'certKeyPassphrase', + }; + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + await uptimePage.syntheticsIntegration.configureTLSOptions(tlsConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'check.request.method': 'GET', + 'response.include_body': 'on_error', + 'response.include_headers': true, + schedule: '@every 3m', + 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], + 'ssl.verification_mode': tlsConfig.verificationMode, + 'ssl.certificate': tlsConfig.cert, + 'ssl.certificate_authorities': tlsConfig.ca, + 'ssl.key': tlsConfig.certKey, + 'ssl.key_passphrase': tlsConfig.certKeyPassphrase, + timeout: '16s', + urls: config.url, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows configuring http advanced options', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateHTTPConfig('http://elastic.co'); + + await uptimePage.syntheticsIntegration.createBasicHTTPMonitorDetails(config); + const advancedConfig = { + username: 'username', + password: 'password', + proxyUrl: 'proxyUrl', + requestMethod: 'POST', + responseStatusCheck: '204', + responseBodyCheckPositive: 'success', + responseBodyCheckNegative: 'failure', + requestHeaders: { + sampleRequestHeader1: 'sampleRequestKey1', + sampleRequestHeader2: 'sampleRequestKey2', + }, + responseHeaders: { + sampleResponseHeader1: 'sampleResponseKey1', + sampleResponseHeader2: 'sampleResponseKey2', + }, + requestBody: { + type: 'xml', + value: 'samplexml', + }, + indexResponseBody: false, + indexResponseHeaders: false, + }; + await uptimePage.syntheticsIntegration.configureHTTPAdvancedOptions(advancedConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'http', + config: { + max_redirects: 0, + 'check.request.method': advancedConfig.requestMethod, + 'check.request.headers': { + 'Content-Type': 'application/xml', + ...advancedConfig.requestHeaders, + }, + 'check.response.headers': advancedConfig.responseHeaders, + 'check.response.status': [advancedConfig.responseStatusCheck], + 'check.request.body': `${advancedConfig.requestBody.value}`, // code editor adds closing tag + 'check.response.body.positive': [advancedConfig.responseBodyCheckPositive], + 'check.response.body.negative': [advancedConfig.responseBodyCheckNegative], + 'response.include_body': advancedConfig.indexResponseBody ? 'on_error' : 'never', + 'response.include_headers': advancedConfig.indexResponseHeaders, + schedule: '@every 3m', + timeout: '16s', + urls: config.url, + proxy_url: advancedConfig.proxyUrl, + username: advancedConfig.username, + password: advancedConfig.password, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows saving tcp monitor when user enters a valid integration name and host+port', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateTCPorICMPConfig('smtp.gmail.com:587'); + + await uptimePage.syntheticsIntegration.createBasicTCPMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'tcp', + config: { + proxy_use_local_resolver: false, + schedule: '@every 3m', + timeout: '16s', + hosts: config.host, + tags: [config.tags], + 'service.name': config.apmServiceName, + }, + }), + ]); + }); + + it('allows configuring tcp advanced options', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateTCPorICMPConfig('smtp.gmail.com:587'); + + await uptimePage.syntheticsIntegration.createBasicTCPMonitorDetails(config); + const advancedConfig = { + proxyUrl: 'proxyUrl', + requestSendCheck: 'body', + responseReceiveCheck: 'success', + proxyUseLocalResolver: true, + }; + await uptimePage.syntheticsIntegration.configureTCPAdvancedOptions(advancedConfig); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'tcp', + config: { + schedule: '@every 3m', + timeout: '16s', + hosts: config.host, + proxy_url: advancedConfig.proxyUrl, + proxy_use_local_resolver: advancedConfig.proxyUseLocalResolver, + 'check.receive': advancedConfig.responseReceiveCheck, + 'check.send': advancedConfig.requestSendCheck, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + + it('allows saving icmp monitor when user enters a valid integration name and host', async () => { + // This test ensures that updates made to the Synthetics Policy are carried all the way through + // to the generated Agent Policy that is dispatch down to the Elastic Agent. + const config = generateTCPorICMPConfig('1.1.1.1'); + + await uptimePage.syntheticsIntegration.createBasicICMPMonitorDetails(config); + await uptimePage.syntheticsIntegration.confirmAndSave(); + + await uptimePage.syntheticsIntegration.isPolicyCreatedSuccessfully(); + + const [agentPolicy] = await uptimeService.syntheticsPackage.getAgentPolicyList(); + const agentPolicyId = agentPolicy.id; + const agentFullPolicy = await uptimeService.syntheticsPackage.getFullAgentPolicy( + agentPolicyId + ); + + expect(agentFullPolicy.inputs).to.eql([ + generatePolicy({ + agentFullPolicy, + version, + name: monitorName, + monitorType: 'icmp', + config: { + schedule: '@every 3m', + timeout: '16s', + wait: '1s', + hosts: config.host, + 'service.name': config.apmServiceName, + tags: [config.tags], + }, + }), + ]); + }); + }); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f171e247472f1..573350dad24d0 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -154,6 +154,9 @@ export default async function ({ readConfigFile }) { uptime: { pathname: '/app/uptime', }, + fleet: { + pathname: '/app/fleet', + }, ml: { pathname: '/app/ml', }, diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 81c0328e76342..e83420a9cea1d 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -24,6 +24,7 @@ import { StatusPagePageProvider } from './status_page'; import { UpgradeAssistantPageProvider } from './upgrade_assistant_page'; import { RollupPageProvider } from './rollup_page'; import { UptimePageProvider } from './uptime_page'; +import { SyntheticsIntegrationPageProvider } from './synthetics_integration_page'; import { ApiKeysPageProvider } from './api_keys_page'; import { LicenseManagementPageProvider } from './license_management_page'; import { IndexManagementPageProvider } from './index_management_page'; @@ -64,6 +65,7 @@ export const pageObjects = { statusPage: StatusPagePageProvider, upgradeAssistant: UpgradeAssistantPageProvider, uptime: UptimePageProvider, + syntheticsIntegration: SyntheticsIntegrationPageProvider, rollup: RollupPageProvider, apiKeys: ApiKeysPageProvider, licenseManagement: LicenseManagementPageProvider, diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts new file mode 100644 index 0000000000000..69ae3f43d26f2 --- /dev/null +++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts @@ -0,0 +1,384 @@ +/* + * 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 { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SyntheticsIntegrationPageProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { + const pageObjects = getPageObjects(['common', 'header']); + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + + return { + /** + * Navigates to the Synthetics Integration page + * + */ + async navigateToPackagePage(packageVersion: string) { + await pageObjects.common.navigateToUrl( + 'fleet', + `/integrations/synthetics-${packageVersion}/add-integration`, + { + shouldUseHashForSubUrl: true, + useActualUrl: true, + } + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }, + + async navigateToPackageEditPage(packageId: string, agentId: string) { + await pageObjects.common.navigateToUrl( + 'fleet', + `/policies/${agentId}/edit-integration/${packageId}`, + { + shouldUseHashForSubUrl: true, + useActualUrl: true, + } + ); + await pageObjects.header.waitUntilLoadingHasFinished(); + }, + + /** + * Finds and returns the Policy Details Page Save button + */ + async findSaveButton(isEditPage?: boolean) { + await this.ensureIsOnPackagePage(); + return await testSubjects.find( + isEditPage ? 'saveIntegration' : 'createPackagePolicySaveButton' + ); + }, + + /** + * Finds and returns the Policy Details Page Cancel Button + */ + async findCancelButton() { + await this.ensureIsOnPackagePage(); + return await testSubjects.find('policyDetailsCancelButton'); + }, + + /** + * Determines if the policy was created successfully by looking for the creation success toast + */ + async isPolicyCreatedSuccessfully() { + await testSubjects.existOrFail('packagePolicyCreateSuccessToast'); + }, + + /** + * Selects the monitor type + * @params {monitorType} the type of monitor, tcp, http, or icmp + */ + async selectMonitorType(monitorType: string) { + await testSubjects.selectValue('syntheticsMonitorTypeField', monitorType); + }, + + /** + * Fills a text input + * @params {testSubj} the testSubj of the input to fill + * @params {value} the value of the input + */ + async fillTextInputByTestSubj(testSubj: string, value: string) { + const field = await testSubjects.find(testSubj, 5000); + await field.click(); + await field.clearValue(); + await field.type(value); + }, + + /** + * Fills a text input + * @params {testSubj} the testSubj of the input to fill + * @params {value} the value of the input + */ + async fillTextInput(field: WebElementWrapper, value: string) { + await field.click(); + await field.clearValue(); + await field.type(value); + }, + + /** + * Fills a text input + * @params {testSubj} the testSubj of the comboBox + */ + async setComboBox(testSubj: string, value: string) { + await comboBox.setCustom(`${testSubj} > comboBoxInput`, value); + }, + + /** + * Finds and returns the HTTP advanced options accordion trigger + */ + async findHTTPAdvancedOptionsAccordion() { + await this.ensureIsOnPackagePage(); + const accordion = await testSubjects.find('syntheticsHTTPAdvancedFieldsAccordion', 5000); + return accordion; + }, + + /** + * Finds and returns the enable TLS checkbox + */ + async findEnableTLSCheckbox() { + await this.ensureIsOnPackagePage(); + const tlsCheckboxContainer = await testSubjects.find('syntheticsIsTLSEnabled'); + return await tlsCheckboxContainer.findByCssSelector('label'); + }, + + /** + * ensures that the package page is the currently display view + */ + async ensureIsOnPackagePage() { + await testSubjects.existOrFail('monitorSettingsSection'); + }, + + /** + * Clicks save button and confirms update on the Policy Details page + */ + async confirmAndSave(isEditPage?: boolean) { + await this.ensureIsOnPackagePage(); + const saveButton = await this.findSaveButton(isEditPage); + saveButton.click(); + }, + + /** + * Fills in the username and password field + * @params username {string} the value of the username + * @params password {string} the value of the password + */ + async configureUsernameAndPassword({ username, password }: Record) { + await this.fillTextInputByTestSubj('syntheticsUsername', username); + await this.fillTextInputByTestSubj('syntheticsPassword', password); + }, + + /** + * + * Configures request headers + * @params headers {string} an object containing desired headers + * + */ + async configureRequestHeaders(headers: Record) { + await this.configureHeaders('syntheticsRequestHeaders', headers); + }, + + /** + * + * Configures response headers + * @params headers {string} an object containing desired headers + * + */ + async configureResponseHeaders(headers: Record) { + await this.configureHeaders('syntheticsResponseHeaders', headers); + }, + + /** + * + * Configures headers + * @params testSubj {string} test subj + * @params headers {string} an object containing desired headers + * + */ + async configureHeaders(testSubj: string, headers: Record) { + const headersContainer = await testSubjects.find(testSubj); + const addHeaderButton = await headersContainer.findByCssSelector('button'); + const keys = Object.keys(headers); + + await Promise.all( + keys.map(async (key, index) => { + await addHeaderButton.click(); + const keyField = await headersContainer.findByCssSelector( + `[data-test-subj="keyValuePairsKey${index}"]` + ); + const valueField = await headersContainer.findByCssSelector( + `[data-test-subj="keyValuePairsValue${index}"]` + ); + await this.fillTextInput(keyField, key); + await this.fillTextInput(valueField, headers[key]); + }) + ); + }, + + /** + * + * Configures request body + * @params contentType {string} contentType of the request body + * @params value {string} value of the request body + * + */ + async configureRequestBody(testSubj: string, value: string) { + await testSubjects.click(`syntheticsRequestBodyTab__${testSubj}`); + const codeEditorContainer = await testSubjects.find('codeEditorContainer'); + const textArea = await codeEditorContainer.findByCssSelector('textarea'); + await textArea.clearValue(); + await textArea.type(value); + }, + + /** + * Creates basic common monitor details + * @params name {string} the name of the monitor + * @params url {string} the url of the monitor + * + */ + async createBasicMonitorDetails({ name, apmServiceName, tags }: Record) { + await this.fillTextInputByTestSubj('packagePolicyNameInput', name); + await this.fillTextInputByTestSubj('syntheticsAPMServiceName', apmServiceName); + await this.setComboBox('syntheticsTags', tags); + }, + + /** + * Fills in the fields to create a basic HTTP monitor + * @params name {string} the name of the monitor + * @params url {string} the url of the monitor + * + */ + async createBasicHTTPMonitorDetails({ + name, + url, + apmServiceName, + tags, + }: Record) { + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + await this.fillTextInputByTestSubj('syntheticsUrlField', url); + }, + + /** + * Fills in the fields to create a basic TCP monitor + * @params name {string} the name of the monitor + * @params host {string} the host (and port) of the monitor + * + */ + async createBasicTCPMonitorDetails({ + name, + host, + apmServiceName, + tags, + }: Record) { + await this.selectMonitorType('tcp'); + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + await this.fillTextInputByTestSubj('syntheticsTCPHostField', host); + }, + + /** + * Creates a basic ICMP monitor + * @params name {string} the name of the monitor + * @params host {string} the host of the monitor + */ + async createBasicICMPMonitorDetails({ + name, + host, + apmServiceName, + tags, + }: Record) { + await this.selectMonitorType('icmp'); + await this.fillTextInputByTestSubj('packagePolicyNameInput', name); + await this.createBasicMonitorDetails({ name, apmServiceName, tags }); + await this.fillTextInputByTestSubj('syntheticsICMPHostField', host); + }, + + /** + * Enables TLS + */ + async enableTLS() { + const tlsCheckbox = await this.findEnableTLSCheckbox(); + await tlsCheckbox.click(); + }, + + /** + * Configures TLS settings + * @params verificationMode {string} the name of the monitor + */ + async configureTLSOptions({ + verificationMode, + ca, + cert, + certKey, + certKeyPassphrase, + }: Record) { + await this.enableTLS(); + await testSubjects.selectValue('syntheticsTLSVerificationMode', verificationMode); + await this.fillTextInputByTestSubj('syntheticsTLSCA', ca); + await this.fillTextInputByTestSubj('syntheticsTLSCert', cert); + await this.fillTextInputByTestSubj('syntheticsTLSCertKey', certKey); + await this.fillTextInputByTestSubj('syntheticsTLSCertKeyPassphrase', certKeyPassphrase); + }, + + /** + * Configure http advanced settings + */ + async configureHTTPAdvancedOptions({ + username, + password, + proxyUrl, + requestMethod, + requestHeaders, + responseStatusCheck, + responseBodyCheckPositive, + responseBodyCheckNegative, + requestBody, + responseHeaders, + indexResponseBody, + indexResponseHeaders, + }: { + username: string; + password: string; + proxyUrl: string; + requestMethod: string; + responseStatusCheck: string; + responseBodyCheckPositive: string; + responseBodyCheckNegative: string; + requestBody: { value: string; type: string }; + requestHeaders: Record; + responseHeaders: Record; + indexResponseBody: boolean; + indexResponseHeaders: boolean; + }) { + await testSubjects.click('syntheticsHTTPAdvancedFieldsAccordion'); + await this.configureResponseHeaders(responseHeaders); + await this.configureRequestHeaders(requestHeaders); + await this.configureRequestBody(requestBody.type, requestBody.value); + await this.configureUsernameAndPassword({ username, password }); + await this.setComboBox('syntheticsResponseStatusCheck', responseStatusCheck); + await this.setComboBox('syntheticsResponseBodyCheckPositive', responseBodyCheckPositive); + await this.setComboBox('syntheticsResponseBodyCheckNegative', responseBodyCheckNegative); + await this.fillTextInputByTestSubj('syntheticsProxyUrl', proxyUrl); + await testSubjects.selectValue('syntheticsRequestMethod', requestMethod); + if (!indexResponseBody) { + const field = await testSubjects.find('syntheticsIndexResponseBody'); + const label = await field.findByCssSelector('label'); + await label.click(); + } + if (!indexResponseHeaders) { + const field = await testSubjects.find('syntheticsIndexResponseHeaders'); + const label = await field.findByCssSelector('label'); + await label.click(); + } + }, + + /** + * Configure tcp advanced settings + */ + async configureTCPAdvancedOptions({ + proxyUrl, + requestSendCheck, + responseReceiveCheck, + proxyUseLocalResolver, + }: { + proxyUrl: string; + requestSendCheck: string; + responseReceiveCheck: string; + proxyUseLocalResolver: boolean; + }) { + await testSubjects.click('syntheticsTCPAdvancedFieldsAccordion'); + await this.fillTextInputByTestSubj('syntheticsProxyUrl', proxyUrl); + await this.fillTextInputByTestSubj('syntheticsTCPRequestSendCheck', requestSendCheck); + await this.fillTextInputByTestSubj('syntheticsTCPResponseReceiveCheck', responseReceiveCheck); + if (proxyUseLocalResolver) { + const field = await testSubjects.find('syntheticsUseLocalResolver'); + const label = await field.findByCssSelector('label'); + await label.click(); + } + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/synthetics_package.ts b/x-pack/test/functional/services/uptime/synthetics_package.ts new file mode 100644 index 0000000000000..78d0fcd61fde4 --- /dev/null +++ b/x-pack/test/functional/services/uptime/synthetics_package.ts @@ -0,0 +1,176 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + DeletePackagePoliciesRequest, + GetPackagePoliciesResponse, + GetFullAgentPolicyResponse, + GetPackagesResponse, + GetAgentPoliciesResponse, +} from '../../../../plugins/fleet/common'; + +const INGEST_API_ROOT = '/api/fleet'; +const INGEST_API_AGENT_POLICIES = `${INGEST_API_ROOT}/agent_policies`; +const INGEST_API_PACKAGE_POLICIES = `${INGEST_API_ROOT}/package_policies`; +const INGEST_API_PACKAGE_POLICIES_DELETE = `${INGEST_API_PACKAGE_POLICIES}/delete`; +const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; + +export function SyntheticsPackageProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + const retry = getService('retry'); + + const logSupertestApiErrorAndThrow = (message: string, error: any): never => { + const responseBody = error?.response?.body; + const responseText = error?.response?.text; + log.error(`Error occurred at ${Date.now()} | ${new Date().toISOString()}`); + log.error(JSON.stringify(responseBody || responseText, null, 2)); + log.error(error); + throw new Error(message); + }; + const retrieveSyntheticsPackageInfo = (() => { + // Retrieve information about the Synthetics package + // EPM does not currently have an API to get the "lastest" information for a page given its name, + // so we'll retrieve a list of packages and then find the package info in the list. + let apiRequest: Promise; + + return () => { + if (!apiRequest) { + log.info(`Setting up call to retrieve Synthetics package`); + + // Currently (as of 2020-june) the package registry used in CI is the public one and + // at times it encounters network connection issues. We use `retry.try` below to see if + // subsequent requests get through. + apiRequest = retry.try(() => { + return supertest + .get(INGEST_API_EPM_PACKAGES) + .set('kbn-xsrf', 'xxx') + .expect(200) + .catch((error) => { + return logSupertestApiErrorAndThrow(`Unable to retrieve packages via Ingest!`, error); + }) + .then((response: { body: GetPackagesResponse }) => { + const { body } = response; + const syntheticsPackageInfo = body.response.find( + (epmPackage) => epmPackage.name === 'synthetics' + ); + if (!syntheticsPackageInfo) { + throw new Error( + `Synthetics package was not in response from ${INGEST_API_EPM_PACKAGES}` + ); + } + return Promise.resolve(syntheticsPackageInfo); + }); + }); + } else { + log.info('Using cached retrieval of synthetics package'); + } + return apiRequest; + }; + })(); + + return { + /** + * Returns the synthetics package version for the currently installed package. This version can then + * be used to build URLs for Fleet pages or APIs + */ + async getSyntheticsPackageVersion() { + const syntheticsPackage = await retrieveSyntheticsPackageInfo()!; + + return syntheticsPackage?.version; + }, + + /** + * Retrieves the full Agent policy by id, which mirrors what the Elastic Agent would get + * once they checkin. + */ + async getFullAgentPolicy(agentPolicyId: string): Promise { + let fullAgentPolicy: GetFullAgentPolicyResponse['item']; + try { + const apiResponse: { body: GetFullAgentPolicyResponse } = await supertest + .get(`${INGEST_API_AGENT_POLICIES}/${agentPolicyId}/full`) + .expect(200); + + fullAgentPolicy = apiResponse.body.item; + } catch (error) { + return logSupertestApiErrorAndThrow('Unable to get full Agent policy', error); + } + + return fullAgentPolicy!; + }, + + /** + * Retrieves all the agent policies. + */ + async getAgentPolicyList(): Promise { + let agentPolicyList: GetAgentPoliciesResponse['items']; + try { + const apiResponse: { body: GetAgentPoliciesResponse } = await supertest + .get(INGEST_API_AGENT_POLICIES) + .expect(200); + + agentPolicyList = apiResponse.body.items; + } catch (error) { + return logSupertestApiErrorAndThrow('Unable to get full Agent policy list', error); + } + + return agentPolicyList!; + }, + + /** + * Deletes a policy (Package Policy) by using the policy name + * @param name + */ + async deletePolicyByName(name: string) { + const id = await this.getPackagePolicyIdByName(name); + + if (id) { + try { + const deletePackagePolicyData: DeletePackagePoliciesRequest['body'] = { + packagePolicyIds: [id], + }; + await supertest + .post(INGEST_API_PACKAGE_POLICIES_DELETE) + .set('kbn-xsrf', 'xxx') + .send(deletePackagePolicyData) + .expect(200); + } catch (error) { + logSupertestApiErrorAndThrow( + `Unable to delete Package Policy via Ingest! ${name}`, + error + ); + } + } + }, + + /** + * Gets the policy id (Package Policy) by using the policy name + * @param name + */ + async getPackagePolicyIdByName(name: string) { + const { + body: packagePoliciesResponse, + }: { body: GetPackagePoliciesResponse } = await supertest + .get(INGEST_API_PACKAGE_POLICIES) + .set('kbn-xsrf', 'xxx') + .query({ kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: ${name}` }) + .send() + .expect(200); + const packagePolicyList: GetPackagePoliciesResponse['items'] = packagePoliciesResponse.items; + + if (packagePolicyList.length > 1) { + throw new Error(`Found ${packagePolicyList.length} Policies - was expecting only one!`); + } + + if (packagePolicyList.length) { + return packagePolicyList[0].id; + } + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/uptime.ts b/x-pack/test/functional/services/uptime/uptime.ts index b345be012968d..1f808d4e5939a 100644 --- a/x-pack/test/functional/services/uptime/uptime.ts +++ b/x-pack/test/functional/services/uptime/uptime.ts @@ -15,6 +15,7 @@ import { UptimeAlertsProvider } from './alerts'; import { UptimeMLAnomalyProvider } from './ml_anomaly'; import { UptimeCertProvider } from './certificates'; import { UptimeOverviewProvider } from './overview'; +import { SyntheticsPackageProvider } from './synthetics_package'; export function UptimeProvider(context: FtrProviderContext) { const common = UptimeCommonProvider(context); @@ -25,6 +26,7 @@ export function UptimeProvider(context: FtrProviderContext) { const ml = UptimeMLAnomalyProvider(context); const cert = UptimeCertProvider(context); const overview = UptimeOverviewProvider(context); + const syntheticsPackage = SyntheticsPackageProvider(context); return { common, @@ -35,5 +37,6 @@ export function UptimeProvider(context: FtrProviderContext) { ml, cert, overview, + syntheticsPackage, }; } From 2d09e264613d4d5a799d08b777aa398ba8e0773d Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 17 May 2021 23:28:06 -0600 Subject: [PATCH 093/186] [Security Solutions] Replaces most deprecated io-ts alerting and list types (#100234) ## Summary Replaces most of the deprecated io-ts alerting and list types within securitysolution as part of Phase 3 of 4 phases outlined in earlier PR's such as https://github.com/elastic/kibana/pull/99260 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../src/get_index_aliases/index.ts | 7 +- .../src/get_index_count/index.ts | 7 +- .../src/index.ts | 3 + .../src/read_index/index.ts | 7 +- .../src/from/index.ts | 3 + .../src/index.ts | 2 + .../src/language/index.ts | 3 + .../src/machine_learning_job_id/index.ts | 19 + .../src/max_signals/index.ts | 3 + .../src/risk_score/index.ts | 8 + .../src/risk_score_mapping/index.ts | 8 +- .../src/threat/index.ts | 3 + .../src/threat_subtechnique/index.ts | 2 + .../src/threat_technique/index.ts | 2 + .../src/type/index.ts | 22 + .../src/index.ts | 2 - .../src/type/index.ts | 1 + .../src/default_version_number/index.test.ts | 0 .../src/default_version_number/index.ts | 0 .../src/index.ts | 2 + .../src/version/index.ts | 5 +- .../exceptions/build_exceptions_filter.ts | 6 +- .../plugins/lists/common/exceptions/utils.ts | 2 +- .../lists/common/schemas/common/schemas.ts | 18 - .../request/create_exception_list_schema.ts | 8 +- .../schemas/request/create_list_schema.ts | 8 +- .../schemas/request/patch_list_schema.ts | 3 +- .../request/update_exception_list_schema.ts | 3 +- .../schemas/request/update_list_schema.ts | 3 +- .../schemas/response/exception_list_schema.ts | 2 +- .../common/schemas/response/list_schema.ts | 2 +- x-pack/plugins/lists/common/shared_exports.ts | 5 +- .../builder/exception_items_renderer.tsx | 2 +- .../exceptions/components/builder/helpers.ts | 2 +- x-pack/plugins/lists/public/index.ts | 6 +- .../elastic_query/index_es_list_schema.ts | 2 +- .../elastic_response/search_es_list_schema.ts | 2 +- .../exceptions_list_so_schema.ts | 2 +- .../create_endoint_event_filters_list.ts | 3 +- .../exception_lists/create_endpoint_list.ts | 3 +- .../create_endpoint_trusted_apps_list.ts | 3 +- .../exception_lists/create_exception_list.ts | 3 +- .../exception_list_client_types.ts | 4 +- .../exception_lists/update_exception_list.ts | 2 +- .../items/write_lines_to_bulk_list_items.ts | 2 +- .../server/services/lists/create_list.ts | 2 +- .../lists/create_list_if_it_does_not_exist.ts | 2 +- .../services/lists/list_client_types.ts | 3 +- .../server/services/lists/update_list.ts | 3 +- .../common/add_remove_id_to_item.test.ts | 77 --- .../common/add_remove_id_to_item.ts | 50 -- .../detection_engine/get_query_filter.ts | 3 +- .../schemas/common/schemas.ts | 452 +----------------- .../request/add_prepackaged_rules_schema.ts | 77 ++- .../add_prepackged_rules_schema.test.ts | 3 +- .../request/create_rules_bulk_schema.test.ts | 4 +- .../create_signals_migration_schema.ts | 2 +- .../request/export_rules_schema.test.ts | 3 +- .../schemas/request/export_rules_schema.ts | 5 +- .../schemas/request/find_rules_schema.test.ts | 3 +- .../schemas/request/find_rules_schema.ts | 3 +- .../get_signals_migration_status_schema.ts | 2 +- .../request/import_rules_schema.test.ts | 3 +- .../schemas/request/import_rules_schema.ts | 83 ++-- .../request/patch_rules_bulk_schema.test.ts | 4 +- .../request/patch_rules_schema.test.ts | 3 +- .../schemas/request/patch_rules_schema.ts | 50 +- .../request/query_rules_bulk_schema.test.ts | 4 +- .../request/query_rules_schema.test.ts | 3 +- .../query_signals_index_schema.test.ts | 3 +- .../request/query_signals_index_schema.ts | 2 +- .../schemas/request/rule_schemas.test.ts | 3 +- .../schemas/request/rule_schemas.ts | 27 +- .../request/set_signal_status_schema.test.ts | 3 +- .../request/update_rules_bulk_schema.test.ts | 4 +- .../schemas/response/error_schema.test.ts | 3 +- .../response/import_rules_schema.test.ts | 3 +- .../response/prepackaged_rules_schema.test.ts | 3 +- .../prepackaged_rules_status_schema.test.ts | 3 +- .../response/rules_bulk_schema.test.ts | 3 +- .../schemas/response/rules_schema.test.ts | 5 +- .../schemas/response/rules_schema.ts | 52 +- .../type_timeline_only_schema.test.ts | 3 +- .../response/type_timeline_only_schema.ts | 3 +- .../types/deafult_boolean_true.test.ts | 51 -- .../schemas/types/deafult_from_string.test.ts | 42 -- .../types/default_actions_array.test.ts | 57 --- .../schemas/types/default_actions_array.ts | 23 - .../schemas/types/default_array.test.ts | 81 ---- .../schemas/types/default_array.ts | 27 -- .../types/default_boolean_false.test.ts | 51 -- .../schemas/types/default_boolean_false.ts | 22 - .../schemas/types/default_boolean_true.ts | 22 - .../types/default_empty_string.test.ts | 42 -- .../schemas/types/default_empty_string.ts | 22 - .../types/default_export_file_name.test.ts | 42 -- .../schemas/types/default_export_file_name.ts | 22 - .../schemas/types/default_from_string.ts | 26 - .../types/default_interval_string.test.ts | 42 -- .../schemas/types/default_interval_string.ts | 22 - .../types/default_language_string.test.ts | 43 -- .../schemas/types/default_language_string.ts | 23 - .../types/default_max_signals_number.test.ts | 65 --- .../types/default_max_signals_number.ts | 27 -- .../schemas/types/default_page.test.ts | 84 ---- .../schemas/types/default_page.ts | 32 -- .../schemas/types/default_per_page.test.ts | 84 ---- .../schemas/types/default_per_page.ts | 32 -- .../types/default_risk_score_mapping_array.ts | 27 -- .../types/default_severity_mapping_array.ts | 27 -- .../types/default_string_array.test.ts | 51 -- .../schemas/types/default_string_array.ts | 23 - .../default_string_boolean_false.test.ts | 100 ---- .../types/default_string_boolean_false.ts | 34 -- .../types/default_threat_array.test.ts | 65 --- .../schemas/types/default_threat_array.ts | 23 - .../types/default_throttle_null.test.ts | 43 -- .../schemas/types/default_throttle_null.ts | 23 - .../schemas/types/default_to_string.test.ts | 42 -- .../schemas/types/default_to_string.ts | 22 - .../schemas/types/default_uuid.test.ts | 42 -- .../schemas/types/default_uuid.ts | 26 - .../types/default_version_number.test.ts | 64 --- .../schemas/types/default_version_number.ts | 25 - .../detection_engine/schemas/types/index.ts | 40 -- .../schemas/types/iso_date_string.test.ts | 55 --- .../schemas/types/iso_date_string.ts | 38 -- .../schemas/types/lists.mock.ts | 2 +- .../schemas/types/lists.test.ts | 134 ------ .../detection_engine/schemas/types/lists.ts | 30 -- .../schemas/types/lists_default_array.test.ts | 64 --- .../schemas/types/lists_default_array.ts | 24 - .../schemas/types/non_empty_array.test.ts | 95 ---- .../schemas/types/non_empty_array.ts | 32 -- .../schemas/types/non_empty_string.test.ts | 55 --- .../schemas/types/non_empty_string.ts | 29 -- .../schemas/types/normalized_ml_job_id.ts | 36 -- .../schemas/types/only_false_allowed.test.ts | 53 -- .../schemas/types/only_false_allowed.ts | 33 -- .../schemas/types/positive_integer.ts | 26 - ...positive_integer_greater_than_zero.test.ts | 55 --- .../positive_integer_greater_than_zero.ts | 26 - .../schemas/types/postive_integer.test.ts | 53 -- .../types/references_default_array.test.ts | 51 -- .../schemas/types/references_default_array.ts | 22 - .../schemas/types/risk_score.test.ts | 60 --- .../schemas/types/risk_score.ts | 28 -- .../schemas/types/threat.mock.ts | 2 +- .../schemas/types/threat_mapping.test.ts | 236 --------- .../schemas/types/threat_mapping.ts | 214 --------- .../schemas/types/uuid.test.ts | 42 -- .../detection_engine/schemas/types/uuid.ts | 30 -- .../common/detection_engine/utils.ts | 3 +- .../common/exact_check.test.ts | 177 ------- .../security_solution/common/exact_check.ts | 94 ---- .../common/format_errors.test.ts | 188 -------- .../security_solution/common/format_errors.ts | 35 -- .../plugins/security_solution/common/index.ts | 2 - .../common/machine_learning/helpers.ts | 2 +- .../common/shared_exports.ts | 20 - .../common/shared_imports.ts | 3 - .../security_solution/common/test_utils.ts | 61 --- .../common/types/timeline/index.ts | 2 +- .../security_solution/common/validate.test.ts | 49 -- .../security_solution/common/validate.ts | 55 --- .../autocomplete/field_value_lists.test.tsx | 6 +- .../autocomplete/field_value_lists.tsx | 2 +- .../common/components/autocomplete/helpers.ts | 2 +- .../exceptions/add_exception_modal/index.tsx | 7 +- .../exceptions/edit_exception_modal/index.tsx | 7 +- .../components/exceptions/error_callout.tsx | 2 +- .../common/components/exceptions/types.ts | 13 +- .../exceptions/use_add_exception.test.tsx | 4 +- .../exceptions/use_add_exception.tsx | 2 +- ...tch_or_create_rule_exception_list.test.tsx | 3 +- ...se_fetch_or_create_rule_exception_list.tsx | 4 +- .../exception_item/exception_details.tsx | 4 +- .../viewer/exception_item/index.tsx | 4 +- .../viewer/exceptions_viewer_items.tsx | 4 +- .../components/exceptions/viewer/helpers.tsx | 4 +- .../exceptions/viewer/index.test.tsx | 4 +- .../components/exceptions/viewer/index.tsx | 2 +- .../components/exceptions/viewer/reducer.ts | 6 +- .../common/components/links/index.test.tsx | 2 +- .../markdown_editor/renderer.test.tsx | 2 +- .../threat_match/entry_delete_button.test.tsx | 2 +- .../components/threat_match/helpers.test.tsx | 2 +- .../components/threat_match/helpers.tsx | 8 +- .../common/components/threat_match/index.tsx | 2 +- .../components/threat_match/reducer.test.ts | 2 +- .../common/components/threat_match/types.ts | 2 +- .../rules/description_step/helpers.tsx | 4 +- .../rules/description_step/index.tsx | 3 +- .../rules/description_step/types.ts | 2 +- .../components/rules/mitre/helpers.ts | 2 +- .../components/rules/mitre/index.tsx | 2 +- .../rules/mitre/subtechnique_fields.tsx | 5 +- .../rules/mitre/technique_fields.tsx | 5 +- .../components/rules/query_preview/helpers.ts | 2 +- .../components/rules/query_preview/index.tsx | 2 +- .../components/rules/query_preview/reducer.ts | 2 +- .../rules/risk_score_mapping/index.tsx | 2 +- .../rules/select_rule_type/index.tsx | 2 +- .../rules/severity_mapping/index.tsx | 10 +- .../components/rules/step_about_rule/data.tsx | 2 +- .../detection_engine/rules/transforms.ts | 2 +- .../detection_engine/rules/types.ts | 27 +- .../rules/use_dissasociate_exception_list.tsx | 2 +- .../detections/mitre/valid_threat_mock.ts | 2 +- .../rules/create/helpers.test.ts | 4 +- .../detection_engine/rules/create/helpers.ts | 20 +- .../pages/detection_engine/rules/helpers.tsx | 12 +- .../pages/detection_engine/rules/types.ts | 20 +- .../public/lists_plugin_deps.ts | 10 - .../network/components/port/index.test.tsx | 2 +- .../source_destination/index.test.tsx | 2 +- .../source_destination_ip.test.tsx | 2 +- .../certificate_fingerprint/index.test.tsx | 2 +- .../components/ja3_fingerprint/index.test.tsx | 2 +- .../components/netflow/index.test.tsx | 2 +- .../body/renderers/get_row_renderer.test.tsx | 2 +- .../suricata/suricata_details.test.tsx | 2 +- .../suricata/suricata_row_renderer.test.tsx | 2 +- .../system/generic_row_renderer.test.tsx | 2 +- .../body/renderers/zeek/zeek_details.test.tsx | 2 +- .../renderers/zeek/zeek_row_renderer.test.tsx | 2 +- .../renderers/zeek/zeek_signature.test.tsx | 2 +- .../server/endpoint/lib/artifacts/lists.ts | 2 +- .../server/endpoint/lib/artifacts/manifest.ts | 2 +- .../endpoint/routes/trusted_apps/mapping.ts | 2 +- .../endpoint/schemas/artifacts/lists.ts | 2 +- .../services/artifacts/manifest_client.ts | 2 +- .../errors/bad_request_error.ts | 8 - .../index/create_bootstrap_index.ts | 34 -- .../index/delete_all_index.ts | 51 -- .../detection_engine/index/delete_policy.ts | 23 - .../detection_engine/index/delete_template.ts | 21 - .../index/get_index_exists.test.ts | 61 --- .../index/get_index_exists.ts | 34 -- .../index/get_policy_exists.ts | 33 -- .../index/get_template_exists.ts | 22 - .../lib/detection_engine/index/set_policy.ts | 25 - .../detection_engine/index/set_template.ts | 24 - .../create_migration_saved_object.ts | 2 +- .../migrations/finalize_migration.test.ts | 4 +- .../migrations/finalize_migration.ts | 2 +- .../find_migration_saved_objects.ts | 2 +- .../get_migration_saved_objects_by_id.ts | 2 +- .../migrations/saved_objects_schema.ts | 2 +- .../update_migration_saved_object.ts | 2 +- .../routes/index/create_index_route.ts | 15 +- .../routes/index/delete_index_route.ts | 17 +- .../routes/index/get_index_version.ts | 2 +- .../routes/index/read_index_route.ts | 5 +- .../privileges/read_privileges_route.ts | 4 +- .../rules/add_prepackaged_rules_route.ts | 6 +- .../routes/rules/create_rules_bulk_route.ts | 4 +- .../routes/rules/create_rules_route.ts | 5 +- .../routes/rules/delete_rules_bulk_route.ts | 2 +- .../routes/rules/delete_rules_route.ts | 4 +- .../routes/rules/export_rules_route.ts | 3 +- .../routes/rules/find_rules_route.ts | 4 +- .../routes/rules/find_rules_status_route.ts | 4 +- .../get_prepackaged_rules_status_route.ts | 6 +- .../routes/rules/import_rules_route.ts | 6 +- .../routes/rules/patch_rules_bulk_route.ts | 2 +- .../routes/rules/patch_rules_route.ts | 4 +- .../routes/rules/read_rules_route.ts | 4 +- .../routes/rules/update_rules_bulk_route.ts | 2 +- .../routes/rules/update_rules_route.ts | 4 +- .../routes/rules/utils.test.ts | 2 +- .../detection_engine/routes/rules/validate.ts | 2 +- .../create_signals_migration_route.test.ts | 10 +- .../signals/create_signals_migration_route.ts | 6 +- .../signals/delete_signals_migration_route.ts | 4 +- .../finalize_signals_migration_route.ts | 5 +- .../get_signals_migration_status_route.ts | 4 +- .../signals/open_close_signals_route.ts | 4 +- .../routes/signals/query_signals_route.ts | 3 +- .../routes/tags/read_tags_route.ts | 4 +- .../lib/detection_engine/routes/utils.test.ts | 93 +--- .../lib/detection_engine/routes/utils.ts | 43 +- .../create_rules_stream_from_ndjson.test.ts | 2 +- .../rules/create_rules_stream_from_ndjson.ts | 5 +- .../rules/get_prepackaged_rules.ts | 5 +- .../lib/detection_engine/rules/patch_rules.ts | 2 +- .../lib/detection_engine/rules/types.ts | 62 +-- .../lib/detection_engine/rules/utils.ts | 42 +- .../detection_engine/schemas/rule_schemas.ts | 27 +- .../detection_engine/signals/filters/types.ts | 3 +- .../detection_engine/signals/get_filter.ts | 6 +- .../build_risk_score_from_mapping.test.ts | 5 +- .../mappings/build_risk_score_from_mapping.ts | 7 +- .../build_severity_from_mapping.test.ts | 6 +- .../mappings/build_severity_from_mapping.ts | 5 +- .../signals/signal_rule_alert_type.ts | 2 +- .../build_threat_mapping_filter.mock.ts | 2 +- .../build_threat_mapping_filter.test.ts | 5 +- .../build_threat_mapping_filter.ts | 2 +- .../signals/threat_mapping/types.ts | 10 +- .../lib/detection_engine/signals/utils.ts | 2 +- .../server/lib/detection_engine/types.ts | 43 +- .../server/lib/machine_learning/authz.ts | 3 +- .../clean_draft_timelines/index.ts | 4 +- .../get_draft_timelines/index.ts | 4 +- .../lib/timeline/routes/notes/persist_note.ts | 3 +- .../pinned_events/persist_pinned_event.ts | 3 +- .../install_prepackaged_timelines/index.ts | 5 +- .../timelines/create_timelines/index.ts | 3 +- .../timelines/delete_timelines/index.ts | 4 +- .../timelines/export_timelines/index.ts | 3 +- .../routes/timelines/get_timeline/index.ts | 3 +- .../routes/timelines/get_timelines/index.ts | 3 +- .../create_timelines_stream_from_ndjson.ts | 2 +- .../timelines/import_timelines/helpers.ts | 2 +- .../timelines/import_timelines/index.ts | 3 +- .../routes/timelines/patch_timelines/index.ts | 3 +- .../timelines/persist_favorite/index.ts | 3 +- .../server/lib/timeline/utils/common.ts | 28 +- .../build_validation/route_validation.ts | 3 +- .../read_stream/create_stream_from_ndjson.ts | 5 +- .../detection_engine_api_integration/utils.ts | 2 +- x-pack/test/lists_api_integration/utils.ts | 2 +- 323 files changed, 719 insertions(+), 5578 deletions(-) rename x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_aliases.ts => packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts (80%) rename x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_count.ts => packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts (71%) rename x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts => packages/kbn-securitysolution-es-utils/src/read_index/index.ts (55%) create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts create mode 100644 packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts rename packages/{kbn-securitysolution-io-ts-list-types => kbn-securitysolution-io-ts-types}/src/default_version_number/index.test.ts (100%) rename packages/{kbn-securitysolution-io-ts-list-types => kbn-securitysolution-io-ts-types}/src/default_version_number/index.ts (100%) rename packages/{kbn-securitysolution-io-ts-list-types => kbn-securitysolution-io-ts-types}/src/version/index.ts (78%) delete mode 100644 x-pack/plugins/security_solution/common/add_remove_id_to_item.test.ts delete mode 100644 x-pack/plugins/security_solution/common/add_remove_id_to_item.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/postive_integer.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.test.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts delete mode 100644 x-pack/plugins/security_solution/common/exact_check.test.ts delete mode 100644 x-pack/plugins/security_solution/common/exact_check.ts delete mode 100644 x-pack/plugins/security_solution/common/format_errors.test.ts delete mode 100644 x-pack/plugins/security_solution/common/format_errors.ts delete mode 100644 x-pack/plugins/security_solution/common/shared_exports.ts delete mode 100644 x-pack/plugins/security_solution/common/test_utils.ts delete mode 100644 x-pack/plugins/security_solution/common/validate.test.ts delete mode 100644 x-pack/plugins/security_solution/common/validate.ts delete mode 100644 x-pack/plugins/security_solution/public/lists_plugin_deps.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/errors/bad_request_error.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_aliases.ts b/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts similarity index 80% rename from x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_aliases.ts rename to packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts index d9e90cccc20af..885103c1fb584 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_aliases.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_index_aliases/index.ts @@ -1,11 +1,12 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { ElasticsearchClient } from 'src/core/server'; +import { ElasticsearchClient } from '../elasticsearch_client'; interface AliasesResponse { [indexName: string]: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_count.ts b/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts similarity index 71% rename from x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_count.ts rename to packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts index f45ef0a9ff59f..523b41303a569 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_count.ts +++ b/packages/kbn-securitysolution-es-utils/src/get_index_count/index.ts @@ -1,11 +1,12 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { ElasticsearchClient } from 'src/core/server'; +import { ElasticsearchClient } from '../elasticsearch_client'; /** * Retrieves the count of documents in a given index diff --git a/packages/kbn-securitysolution-es-utils/src/index.ts b/packages/kbn-securitysolution-es-utils/src/index.ts index 657a63eef15cd..cfa6820e9aac5 100644 --- a/packages/kbn-securitysolution-es-utils/src/index.ts +++ b/packages/kbn-securitysolution-es-utils/src/index.ts @@ -12,9 +12,12 @@ export * from './delete_all_index'; export * from './delete_policy'; export * from './delete_template'; export * from './elasticsearch_client'; +export * from './get_index_aliases'; +export * from './get_index_count'; export * from './get_index_exists'; export * from './get_policy_exists'; export * from './get_template_exists'; +export * from './read_index'; export * from './read_privileges'; export * from './set_policy'; export * from './set_template'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts b/packages/kbn-securitysolution-es-utils/src/read_index/index.ts similarity index 55% rename from x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts rename to packages/kbn-securitysolution-es-utils/src/read_index/index.ts index 7674ca3b48304..cc16645120b70 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts +++ b/packages/kbn-securitysolution-es-utils/src/read_index/index.ts @@ -1,11 +1,12 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient } from '../elasticsearch_client'; export const readIndex = async (esClient: ElasticsearchClient, index: string): Promise => { return esClient.indices.get({ diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts index 3bf4592a581f5..37ed4b2daa510 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts @@ -24,3 +24,6 @@ export const from = new t.Type( t.identity ); export type From = t.TypeOf; + +export const fromOrUndefined = t.union([from, t.undefined]); +export type FromOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts index 639140be049f2..c6f29862206e6 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts @@ -24,6 +24,7 @@ export * from './default_to_string'; export * from './default_uuid'; export * from './from'; export * from './language'; +export * from './machine_learning_job_id'; export * from './max_signals'; export * from './normalized_ml_job_id'; export * from './references_default_array'; @@ -38,3 +39,4 @@ export * from './threat_subtechnique'; export * from './threat_tactic'; export * from './threat_technique'; export * from './throttle'; +export * from './type'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts index fc3f70f1f2d88..0632f09e6a393 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/language/index.ts @@ -10,3 +10,6 @@ import * as t from 'io-ts'; export const language = t.keyof({ eql: null, kuery: null, lucene: null }); export type Language = t.TypeOf; + +export const languageOrUndefined = t.union([language, t.undefined]); +export type LanguageOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts new file mode 100644 index 0000000000000..9e9c25c62b938 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/machine_learning_job_id/index.ts @@ -0,0 +1,19 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +import { machine_learning_job_id_normalized } from '../normalized_ml_job_id'; + +export const machine_learning_job_id = t.union([t.string, machine_learning_job_id_normalized]); +export type MachineLearningJobId = t.TypeOf; + +export const machineLearningJobIdOrUndefined = t.union([machine_learning_job_id, t.undefined]); +export type MachineLearningJobIdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts index 83360234c65a1..ef7a225d93733 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/max_signals/index.ts @@ -13,3 +13,6 @@ import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-type export const max_signals = PositiveIntegerGreaterThanZero; export type MaxSignals = t.TypeOf; + +export const maxSignalsOrUndefined = t.union([max_signals, t.undefined]); +export type MaxSignalsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts index 0aca7dd70ba1d..98b9c33e7e3ea 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score/index.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +/* eslint-disable @typescript-eslint/naming-convention */ + import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; @@ -26,3 +28,9 @@ export const RiskScore = new t.Type( ); export type RiskScoreC = typeof RiskScore; + +export const risk_score = RiskScore; +export type RiskScore = t.TypeOf; + +export const riskScoreOrUndefined = t.union([risk_score, t.undefined]); +export type RiskScoreOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts index b35b502811ec9..be07bab64f469 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/risk_score_mapping/index.ts @@ -10,10 +10,7 @@ import * as t from 'io-ts'; import { operator } from '@kbn/securitysolution-io-ts-types'; -import { RiskScore } from '../risk_score'; - -export const riskScoreOrUndefined = t.union([RiskScore, t.undefined]); -export type RiskScoreOrUndefined = t.TypeOf; +import { riskScoreOrUndefined } from '../risk_score'; export const risk_score_mapping_field = t.string; export const risk_score_mapping_value = t.string; @@ -28,3 +25,6 @@ export const risk_score_mapping_item = t.exact( export const risk_score_mapping = t.array(risk_score_mapping_item); export type RiskScoreMapping = t.TypeOf; + +export const riskScoreMappingOrUndefined = t.union([risk_score_mapping, t.undefined]); +export type RiskScoreMappingOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts index 0e4022e3ec26e..08ff6cca60a49 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat/index.ts @@ -32,3 +32,6 @@ export type Threat = t.TypeOf; export const threats = t.array(threat); export type Threats = t.TypeOf; + +export const threatsOrUndefined = t.union([threats, t.undefined]); +export type ThreatsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts index 8d64f53cb1623..4909b82d8ec54 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_subtechnique/index.ts @@ -21,3 +21,5 @@ export const threat_subtechnique = t.type({ }); export const threat_subtechniques = t.array(threat_subtechnique); + +export type ThreatSubtechnique = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts index ed2e771e1e118..2d56e842287d8 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/threat_technique/index.ts @@ -30,3 +30,5 @@ export const threat_technique = t.intersection([ ), ]); export const threat_techniques = t.array(threat_technique); + +export type ThreatTechnique = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts new file mode 100644 index 0000000000000..0e74037878992 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/type/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const type = t.keyof({ + eql: null, + machine_learning: null, + query: null, + saved_query: null, + threshold: null, + threat_match: null, +}); +export type Type = t.TypeOf; + +export const typeOrUndefined = t.union([type, t.undefined]); +export type TypeOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts index 1a1c1c3314821..3c60df315e430 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts @@ -16,7 +16,6 @@ export * from './default_create_comments_array'; export * from './default_namespace'; export * from './default_namespace_array'; export * from './default_update_comments_array'; -export * from './default_version_number'; export * from './description'; export * from './endpoint'; export * from './entries'; @@ -43,4 +42,3 @@ export * from './type'; export * from './update_comment'; export * from './updated_at'; export * from './updated_by'; -export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts index 90a8c36eb8b31..50cacb8e0259b 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/type/index.ts @@ -40,3 +40,4 @@ export const type = t.keyof({ export const typeOrUndefined = t.union([type, t.undefined]); export type Type = t.TypeOf; +export type TypeOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.test.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.test.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.test.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-list-types/src/default_version_number/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/default_version_number/index.ts diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts index 8b5a4d9e4de9a..fc0f017016e9f 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -13,6 +13,7 @@ export * from './default_empty_string'; export * from './default_string_array'; export * from './default_string_boolean_false'; export * from './default_uuid'; +export * from './default_version_number'; export * from './empty_string_array'; export * from './iso_date_string'; export * from './non_empty_array'; @@ -26,3 +27,4 @@ export * from './positive_integer'; export * from './positive_integer_greater_than_zero'; export * from './string_to_positive_number'; export * from './uuid'; +export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts b/packages/kbn-securitysolution-io-ts-types/src/version/index.ts similarity index 78% rename from packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts rename to packages/kbn-securitysolution-io-ts-types/src/version/index.ts index 97a81b546c841..245b64781a7f8 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/version/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/version/index.ts @@ -7,7 +7,7 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; /** * Note this is just a positive number, but we use it as a type here which is still ok. @@ -16,3 +16,6 @@ import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-type */ export const version = PositiveIntegerGreaterThanZero; export type Version = t.TypeOf; + +export const versionOrUndefined = t.union([version, t.undefined]); +export type VersionOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts index eda81f91cd983..6e76076bc63ef 100644 --- a/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts +++ b/x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts @@ -17,10 +17,10 @@ import { entriesNested, } from '@kbn/securitysolution-io-ts-list-types'; -import { Filter } from '../../../../../src/plugins/data/common'; -import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../schemas'; +import type { Filter } from '../../../../../src/plugins/data/common'; +import type { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../schemas'; -import { BooleanFilter, NestedFilter } from './types'; +import type { BooleanFilter, NestedFilter } from './types'; import { hasLargeValueList } from './utils'; type NonListEntry = EntryMatch | EntryMatchAny | EntryNested | EntryExists; diff --git a/x-pack/plugins/lists/common/exceptions/utils.ts b/x-pack/plugins/lists/common/exceptions/utils.ts index f5881c1d3cbf4..350cb581153b5 100644 --- a/x-pack/plugins/lists/common/exceptions/utils.ts +++ b/x-pack/plugins/lists/common/exceptions/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; +import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 612b7ea559e4a..83ec27d60b76c 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -11,18 +11,6 @@ import * as t from 'io-ts'; import { NonEmptyString } from '@kbn/securitysolution-io-ts-types'; import { DefaultNamespace } from '@kbn/securitysolution-io-ts-list-types'; -/** - * @deprecated Directly use the type from the package and not from here - */ -export { - DefaultNamespace, - Type, - OsType, - OsTypeArray, - listOperator as operator, - NonEmptyEntriesArray, -} from '@kbn/securitysolution-io-ts-list-types'; - export const list_id = NonEmptyString; export type ListId = t.TypeOf; export const list_idOrUndefined = t.union([list_id, t.undefined]); @@ -91,12 +79,6 @@ export const _version = t.string; export const _versionOrUndefined = t.union([_version, t.undefined]); export type _VersionOrUndefined = t.TypeOf; -export const version = t.number; -export type Version = t.TypeOf; - -export const versionOrUndefined = t.union([version, t.undefined]); -export type VersionOrUndefined = t.TypeOf; - export const immutable = t.boolean; export type Immutable = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts index 91b3a98bdd5ac..30e4ff908ee80 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts @@ -7,8 +7,6 @@ import * as t from 'io-ts'; import { - DefaultVersionNumber, - DefaultVersionNumberDecoded, NamespaceType, OsTypeArray, Tags, @@ -19,7 +17,11 @@ import { osTypeArrayOrUndefined, tags, } from '@kbn/securitysolution-io-ts-list-types'; -import { DefaultUuid } from '@kbn/securitysolution-io-ts-types'; +import { + DefaultUuid, + DefaultVersionNumber, + DefaultVersionNumberDecoded, +} from '@kbn/securitysolution-io-ts-types'; import { ListId, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts index 5fa9da0cdc597..1c197a37c0cbd 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -9,12 +9,8 @@ import * as t from 'io-ts'; import { DefaultVersionNumber, DefaultVersionNumberDecoded, - description, - id, - meta, - name, - type, -} from '@kbn/securitysolution-io-ts-list-types'; +} from '@kbn/securitysolution-io-ts-types'; +import { description, id, meta, name, type } from '@kbn/securitysolution-io-ts-list-types'; import { deserializer, serializer } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts index eea4ba9fc87d7..2cd41584ef9ff 100644 --- a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -7,8 +7,9 @@ import * as t from 'io-ts'; import { description, id, meta, name } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; -import { _version, version } from '../common/schemas'; +import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const patchListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts index c58c1c253a8c4..08f15f52977fd 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts @@ -18,8 +18,9 @@ import { osTypeArrayOrUndefined, tags, } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; -import { _version, list_id, namespace_type, version } from '../common/schemas'; +import { _version, list_id, namespace_type } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; export const updateExceptionListSchema = t.intersection([ diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts index 230853e69fae4..253c4cec566f4 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -6,7 +6,8 @@ */ import * as t from 'io-ts'; -import { description, id, meta, name, version } from '@kbn/securitysolution-io-ts-list-types'; +import { description, id, meta, name } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; import { _version } from '../common/schemas'; import { RequiredKeepUndefined } from '../../types'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts index 7bfc2af9863e2..f96496343fb7e 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts @@ -19,6 +19,7 @@ import { updated_at, updated_by, } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; import { _versionOrUndefined, @@ -26,7 +27,6 @@ import { list_id, namespace_type, tie_breaker_id, - version, } from '../common/schemas'; export const exceptionListSchema = t.exact( diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts index 21504d64fdeaa..5b478cd25daa6 100644 --- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -17,6 +17,7 @@ import { updated_at, updated_by, } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; import { _versionOrUndefined, @@ -24,7 +25,6 @@ import { immutable, serializerOrUndefined, tie_breaker_id, - version, } from '../common/schemas'; export const listSchema = t.exact( diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index f00afb7ac810d..3f1dc01644e21 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -5,16 +5,13 @@ * 2.0. */ -export { +export type { ListSchema, ExceptionListSchema, ExceptionListItemSchema, CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, - exceptionListItemSchema, - createExceptionListItemSchema, - listSchema, } from './schemas'; export { buildExceptionFilter } from './exceptions'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index f771969a92025..6058d4f7b725b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -23,7 +23,7 @@ import { CreateExceptionListItemSchema, ExceptionListItemSchema, exceptionListItemSchema, -} from '../../../../common'; +} from '../../../../common/schemas'; import { AutocompleteStart, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { AndOrBadge } from '../and_or_badge'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts index 6cd9dec0dc7a1..c4052acda6045 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts +++ b/x-pack/plugins/lists/public/exceptions/components/builder/helpers.ts @@ -29,7 +29,7 @@ import { ListSchema, createExceptionListItemSchema, exceptionListItemSchema, -} from '../../../../common'; +} from '../../../../common/schemas'; import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { EXCEPTION_OPERATORS, diff --git a/x-pack/plugins/lists/public/index.ts b/x-pack/plugins/lists/public/index.ts index f940bbe02b8fb..0b67ab05f5bd4 100644 --- a/x-pack/plugins/lists/public/index.ts +++ b/x-pack/plugins/lists/public/index.ts @@ -7,11 +7,11 @@ export * from './shared_exports'; -import { PluginInitializerContext } from '../../../../src/core/public'; +import type { PluginInitializerContext } from '../../../../src/core/public'; import { Plugin } from './plugin'; -import { PluginSetup, PluginStart } from './types'; +import type { PluginSetup, PluginStart } from './types'; export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); -export { Plugin, PluginSetup, PluginStart }; +export type { Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts index 4383e93346291..607535b68c1e5 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_query/index_es_list_schema.ts @@ -16,13 +16,13 @@ import { updated_at, updated_by, } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; import { deserializerOrUndefined, immutable, serializerOrUndefined, tie_breaker_id, - version, } from '../../../common/schemas'; export const indexEsListSchema = t.exact( diff --git a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts index 536269b9c0ae2..f6d6ae4effe72 100644 --- a/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts +++ b/x-pack/plugins/lists/server/schemas/elastic_response/search_es_list_schema.ts @@ -16,13 +16,13 @@ import { updated_at, updated_by, } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; import { deserializerOrUndefined, immutable, serializerOrUndefined, tie_breaker_id, - version, } from '../../../common/schemas'; export const searchEsListSchema = t.exact( diff --git a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts index c1f480e50c8f7..d815dbaae0432 100644 --- a/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts +++ b/x-pack/plugins/lists/server/schemas/saved_objects/exceptions_list_so_schema.ts @@ -20,6 +20,7 @@ import { tags, updated_by, } from '@kbn/securitysolution-io-ts-list-types'; +import { versionOrUndefined } from '@kbn/securitysolution-io-ts-types'; import { immutableOrUndefined, @@ -27,7 +28,6 @@ import { list_id, list_type, tie_breaker_id, - versionOrUndefined, } from '../../../common/schemas'; /** diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts index dba729437b814..9bcf6c63d065d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts @@ -7,13 +7,14 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; +import { Version } from '@kbn/securitysolution-io-ts-types'; import { ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, } from '../../../common/constants'; -import { ExceptionListSchema, Version } from '../../../common/schemas'; +import { ExceptionListSchema } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts index de2be0cb72735..86891e5f83955 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts @@ -7,13 +7,14 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; +import { Version } from '@kbn/securitysolution-io-ts-types'; import { ENDPOINT_LIST_DESCRIPTION, ENDPOINT_LIST_ID, ENDPOINT_LIST_NAME, } from '../../../common/constants'; -import { ExceptionListSchema, Version } from '../../../common/schemas'; +import { ExceptionListSchema } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts index 6f6ad7c357f14..ada043403f248 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts @@ -7,13 +7,14 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; +import { Version } from '@kbn/securitysolution-io-ts-types'; import { ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, ENDPOINT_TRUSTED_APPS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_NAME, } from '../../../common/constants'; -import { ExceptionListSchema, Version } from '../../../common/schemas'; +import { ExceptionListSchema } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index 5f2587fc1e986..c6110dc4f470c 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -15,8 +15,9 @@ import { NamespaceType, Tags, } from '@kbn/securitysolution-io-ts-list-types'; +import { Version } from '@kbn/securitysolution-io-ts-types'; -import { ExceptionListSchema, Immutable, ListId, Version } from '../../../common/schemas'; +import { ExceptionListSchema, Immutable, ListId } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 576b0c4d25aa0..c6f5e3a3bc166 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -30,6 +30,8 @@ import { import { EmptyStringArrayDecoded, NonEmptyStringArrayDecoded, + Version, + VersionOrUndefined, } from '@kbn/securitysolution-io-ts-types'; import { @@ -43,8 +45,6 @@ import { PerPageOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, - Version, - VersionOrUndefined, _VersionOrUndefined, } from '../../../common/schemas'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index 3daa2e9157b5d..43c319cca0005 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -16,11 +16,11 @@ import { OsTypeArray, TagsOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; +import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; import { ExceptionListSchema, ListIdOrUndefined, - VersionOrUndefined, _VersionOrUndefined, } from '../../../common/schemas'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index b3ce823f9ac29..392c44cf72b00 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -9,6 +9,7 @@ import { Readable } from 'stream'; import { ElasticsearchClient } from 'kibana/server'; import { MetaOrUndefined, Type } from '@kbn/securitysolution-io-ts-list-types'; +import { Version } from '@kbn/securitysolution-io-ts-types'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { @@ -16,7 +17,6 @@ import { ListIdOrUndefined, ListSchema, SerializerOrUndefined, - Version, } from '../../../common/schemas'; import { ConfigType } from '../../config'; diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index d139ef3ea4bb1..bd5b3c901fdc5 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -14,6 +14,7 @@ import { Name, Type, } from '@kbn/securitysolution-io-ts-list-types'; +import { Version } from '@kbn/securitysolution-io-ts-types'; import { encodeHitVersion } from '../utils/encode_hit_version'; import { @@ -21,7 +22,6 @@ import { Immutable, ListSchema, SerializerOrUndefined, - Version, } from '../../../common/schemas'; import { IndexEsListSchema } from '../../schemas/elastic_query'; diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts index 71094a5ab49de..4d4e634a465a7 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -13,13 +13,13 @@ import { Name, Type, } from '@kbn/securitysolution-io-ts-list-types'; +import { Version } from '@kbn/securitysolution-io-ts-types'; import { DeserializerOrUndefined, Immutable, ListSchema, SerializerOrUndefined, - Version, } from '../../../common/schemas'; import { getList } from './get_list'; diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index b4fe52019ec7b..28732090342cd 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -18,6 +18,7 @@ import { NameOrUndefined, Type, } from '@kbn/securitysolution-io-ts-list-types'; +import { Version, VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; import { DeserializerOrUndefined, @@ -30,8 +31,6 @@ import { SerializerOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, - Version, - VersionOrUndefined, _VersionOrUndefined, } from '../../../common/schemas'; import { ConfigType } from '../../config'; diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 374c3cd0e2def..2e1cc43826817 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -12,10 +12,11 @@ import { MetaOrUndefined, NameOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; +import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; import { decodeVersion } from '../utils/decode_version'; import { encodeHitVersion } from '../utils/encode_hit_version'; -import { ListSchema, VersionOrUndefined, _VersionOrUndefined } from '../../../common/schemas'; +import { ListSchema, _VersionOrUndefined } from '../../../common/schemas'; import { UpdateEsListSchema } from '../../schemas/elastic_query'; import { getList } from '.'; diff --git a/x-pack/plugins/security_solution/common/add_remove_id_to_item.test.ts b/x-pack/plugins/security_solution/common/add_remove_id_to_item.test.ts deleted file mode 100644 index 0bd88adda7264..0000000000000 --- a/x-pack/plugins/security_solution/common/add_remove_id_to_item.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 { addIdToItem, removeIdFromItem } from './add_remove_id_to_item'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); - -describe('add_remove_id_to_item', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('addIdToItem', () => { - test('it adds an id to an empty item', () => { - expect(addIdToItem({})).toEqual({ id: '123' }); - }); - - test('it adds a complex object', () => { - expect( - addIdToItem({ - field: '', - type: 'mapping', - value: '', - }) - ).toEqual({ - id: '123', - field: '', - type: 'mapping', - value: '', - }); - }); - - test('it adds an id to an existing item', () => { - expect(addIdToItem({ test: '456' })).toEqual({ id: '123', test: '456' }); - }); - - test('it does not change the id if it already exists', () => { - expect(addIdToItem({ id: '456' })).toEqual({ id: '456' }); - }); - - test('it returns the same reference if it has an id already', () => { - const obj = { id: '456' }; - expect(addIdToItem(obj)).toBe(obj); - }); - - test('it returns a new reference if it adds an id to an item', () => { - const obj = { test: '456' }; - expect(addIdToItem(obj)).not.toBe(obj); - }); - }); - - describe('removeIdFromItem', () => { - test('it removes an id from an item', () => { - expect(removeIdFromItem({ id: '456' })).toEqual({}); - }); - - test('it returns a new reference if it removes an id from an item', () => { - const obj = { id: '123', test: '456' }; - expect(removeIdFromItem(obj)).not.toBe(obj); - }); - - test('it does not effect an item without an id', () => { - expect(removeIdFromItem({ test: '456' })).toEqual({ test: '456' }); - }); - - test('it returns the same reference if it does not have an id already', () => { - const obj = { test: '456' }; - expect(removeIdFromItem(obj)).toBe(obj); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/common/add_remove_id_to_item.ts b/x-pack/plugins/security_solution/common/add_remove_id_to_item.ts deleted file mode 100644 index 1f35574038fd7..0000000000000 --- a/x-pack/plugins/security_solution/common/add_remove_id_to_item.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 uuid from 'uuid'; - -/** - * This is useful for when you have arrays without an ID and need to add one for - * ReactJS keys. I break the types slightly by introducing an id to an arbitrary item - * but then cast it back to the regular type T. - * Usage of this could be considered tech debt as I am adding an ID when the backend - * could be doing the same thing but it depends on how you want to model your data and - * if you view modeling your data with id's to please ReactJS a good or bad thing. - * @param item The item to add an id to. - */ -type NotArray = T extends unknown[] ? never : T; -export const addIdToItem = (item: NotArray): T => { - const maybeId: typeof item & { id?: string } = item; - if (maybeId.id != null) { - return item; - } else { - return { ...item, id: uuid.v4() }; - } -}; - -/** - * This is to reverse the id you added to your arrays for ReactJS keys. - * @param item The item to remove the id from. - */ -export const removeIdFromItem = ( - item: NotArray -): - | T - | Pick< - T & { - id?: string | undefined; - }, - Exclude - > => { - const maybeId: typeof item & { id?: string } = item; - if (maybeId.id != null) { - const { id, ...noId } = maybeId; - return noId; - } else { - return item; - } -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index e562d186bc424..7d24c1e157e40 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Language } from '@kbn/securitysolution-io-ts-alerting-types'; import { Filter, IIndexPattern, @@ -17,7 +18,7 @@ import { } from '../../../lists/common/schemas'; import { ESBoolQuery } from '../typed_json'; import { buildExceptionFilter } from '../shared_imports'; -import { Query, Language, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; +import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; export const getQueryFilter = ( query: Query, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index e085d500bee0a..7b49b68ab79a1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -8,21 +8,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ import * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; import { - SavedObjectAttributes, - SavedObjectAttribute, - SavedObjectAttributeSingle, -} from 'src/core/types'; -import { RiskScore } from '../types/risk_score'; -import { UUID } from '../types/uuid'; -import { IsoDateString } from '../types/iso_date_string'; -import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; -import { PositiveInteger } from '../types/positive_integer'; -import { NonEmptyString } from '../types/non_empty_string'; -import { parseScheduleDates } from '../../parse_schedule_dates'; -import { machine_learning_job_id_normalized } from '../types/normalized_ml_job_id'; + UUID, + NonEmptyString, + IsoDateString, + PositiveIntegerGreaterThanZero, + PositiveInteger, +} from '@kbn/securitysolution-io-ts-types'; export const author = t.array(t.string); export type Author = t.TypeOf; @@ -76,79 +69,6 @@ export type Filters = t.TypeOf; // Filters are not easily type-a export const filtersOrUndefined = t.union([filters, t.undefined]); export type FiltersOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const saved_object_attribute_single: t.Type = t.recursion( - 'saved_object_attribute_single', - () => t.union([t.string, t.number, t.boolean, t.null, t.undefined, saved_object_attributes]) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const saved_object_attribute: t.Type = t.recursion( - 'saved_object_attribute', - () => t.union([saved_object_attribute_single, t.array(saved_object_attribute_single)]) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const saved_object_attributes: t.Type = t.recursion( - 'saved_object_attributes', - () => t.record(t.string, saved_object_attribute) -); - -/** - * Params is an "object", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts - */ -export const action_group = t.string; -export const action_id = t.string; -export const action_action_type_id = t.string; -export const action_params = saved_object_attributes; -export const action = t.exact( - t.type({ - group: action_group, - id: action_id, - action_type_id: action_action_type_id, - params: action_params, - }) -); - -export const actions = t.array(action); -export type Actions = t.TypeOf; - -export const actionsCamel = t.array( - t.exact( - t.type({ - group: action_group, - id: action_id, - actionTypeId: action_action_type_id, - params: action_params, - }) - ) -); -export type ActionsCamel = t.TypeOf; - -const stringValidator = (input: unknown): input is string => typeof input === 'string'; -export const from = new t.Type( - 'From', - t.string.is, - (input, context): Either => { - if (stringValidator(input) && parseScheduleDates(input) == null) { - return t.failure(input, context, 'Failed to parse "from" on rule param'); - } - return t.string.validate(input, context); - }, - t.identity -); -export type From = t.TypeOf; - -export const fromOrUndefined = t.union([from, t.undefined]); -export type FromOrUndefined = t.TypeOf; - export const immutable = t.boolean; export type Immutable = t.TypeOf; @@ -183,26 +103,6 @@ export type Query = t.TypeOf; export const queryOrUndefined = t.union([query, t.undefined]); export type QueryOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const language = t.keyof({ eql: null, kuery: null, lucene: null }); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Language = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const languageOrUndefined = t.union([language, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type LanguageOrUndefined = t.TypeOf; - export const license = t.string; export type License = t.TypeOf; @@ -241,27 +141,12 @@ export type TimestampOverride = t.TypeOf; export const timestampOverrideOrUndefined = t.union([timestamp_override, t.undefined]); export type TimestampOverrideOrUndefined = t.TypeOf; -export const throttle = t.string; -export type Throttle = t.TypeOf; - -export const throttleOrNull = t.union([throttle, t.null]); -export type ThrottleOrNull = t.TypeOf; - -export const throttleOrNullOrUndefined = t.union([throttle, t.null, t.undefined]); -export type ThrottleOrUndefinedOrNull = t.TypeOf; - export const anomaly_threshold = PositiveInteger; export type AnomalyThreshold = t.TypeOf; export const anomalyThresholdOrUndefined = t.union([anomaly_threshold, t.undefined]); export type AnomalyThresholdOrUndefined = t.TypeOf; -export const machine_learning_job_id = t.union([t.string, machine_learning_job_id_normalized]); -export type MachineLearningJobId = t.TypeOf; - -export const machineLearningJobIdOrUndefined = t.union([machine_learning_job_id, t.undefined]); -export type MachineLearningJobIdOrUndefined = t.TypeOf; - /** * Note that this is a non-exact io-ts type as we allow extra meta information * to be added to the meta object @@ -271,158 +156,18 @@ export type Meta = t.TypeOf; export const metaOrUndefined = t.union([meta, t.undefined]); export type MetaOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const max_signals = PositiveIntegerGreaterThanZero; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type MaxSignals = t.TypeOf; - -export const maxSignalsOrUndefined = t.union([max_signals, t.undefined]); -export type MaxSignalsOrUndefined = t.TypeOf; - export const name = NonEmptyString; export type Name = t.TypeOf; export const nameOrUndefined = t.union([name, t.undefined]); export type NameOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const operator = t.keyof({ - equals: null, -}); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Operator = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export enum OperatorEnum { - EQUALS = 'equals', -} - -export const risk_score = RiskScore; -export type RiskScore = t.TypeOf; - -export const riskScoreOrUndefined = t.union([risk_score, t.undefined]); -export type RiskScoreOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const risk_score_mapping_field = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const risk_score_mapping_value = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const risk_score_mapping_item = t.exact( - t.type({ - field: risk_score_mapping_field, - value: risk_score_mapping_value, - operator, - risk_score: riskScoreOrUndefined, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const risk_score_mapping = t.array(risk_score_mapping_item); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type RiskScoreMapping = t.TypeOf; - -export const riskScoreMappingOrUndefined = t.union([risk_score_mapping, t.undefined]); -export type RiskScoreMappingOrUndefined = t.TypeOf; - export const rule_name_override = t.string; export type RuleNameOverride = t.TypeOf; export const ruleNameOverrideOrUndefined = t.union([rule_name_override, t.undefined]); export type RuleNameOverrideOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severity = t.keyof({ low: null, medium: null, high: null, critical: null }); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Severity = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severityOrUndefined = t.union([severity, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type SeverityOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severity_mapping_field = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severity_mapping_value = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severity_mapping_item = t.exact( - t.type({ - field: severity_mapping_field, - operator, - value: severity_mapping_value, - severity, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type SeverityMappingItem = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severity_mapping = t.array(severity_mapping_item); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type SeverityMapping = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const severityMappingOrUndefined = t.union([severity_mapping, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type SeverityMappingOrUndefined = t.TypeOf; - export const status = t.keyof({ open: null, closed: null, 'in-progress': null }); export type Status = t.TypeOf; @@ -445,19 +190,6 @@ export type To = t.TypeOf; export const toOrUndefined = t.union([to, t.undefined]); export type ToOrUndefined = t.TypeOf; -export const type = t.keyof({ - eql: null, - machine_learning: null, - query: null, - saved_query: null, - threshold: null, - threat_match: null, -}); -export type Type = t.TypeOf; - -export const typeOrUndefined = t.union([type, t.undefined]); -export type TypeOrUndefined = t.TypeOf; - export const queryFilter = t.string; export type QueryFilter = t.TypeOf; @@ -511,152 +243,6 @@ export type Fields = t.TypeOf; export const fieldsOrUndefined = t.union([fields, t.undefined]); export type FieldsOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_framework = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_tactic_id = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_tactic_name = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_tactic_reference = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_tactic = t.type({ - id: threat_tactic_id, - name: threat_tactic_name, - reference: threat_tactic_reference, -}); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatTactic = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_subtechnique_id = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_subtechnique_name = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_subtechnique_reference = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_subtechnique = t.type({ - id: threat_subtechnique_id, - name: threat_subtechnique_name, - reference: threat_subtechnique_reference, -}); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatSubtechnique = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_subtechniques = t.array(threat_subtechnique); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_technique_id = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_technique_name = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_technique_reference = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_technique = t.intersection([ - t.exact( - t.type({ - id: threat_technique_id, - name: threat_technique_name, - reference: threat_technique_reference, - }) - ), - t.exact( - t.partial({ - subtechnique: threat_subtechniques, - }) - ), -]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatTechnique = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_techniques = t.array(threat_technique); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat = t.intersection([ - t.exact( - t.type({ - framework: threat_framework, - tactic: threat_tactic, - }) - ), - t.exact( - t.partial({ - technique: threat_techniques, - }) - ), -]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Threat = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threats = t.array(threat); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Threats = t.TypeOf; - -export const threatsOrUndefined = t.union([threats, t.undefined]); -export type ThreatsOrUndefined = t.TypeOf; - export const thresholdField = t.exact( t.type({ field: t.union([t.string, t.array(t.string)]), // Covers pre- and post-7.12 @@ -707,43 +293,19 @@ export type ThresholdNormalized = t.TypeOf; export const thresholdNormalizedOrUndefined = t.union([thresholdNormalized, t.undefined]); export type ThresholdNormalizedOrUndefined = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ export const created_at = IsoDateString; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ export const updated_at = IsoDateString; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ export const updated_by = t.string; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ export const created_by = t.string; + export const updatedByOrNull = t.union([updated_by, t.null]); export type UpdatedByOrNull = t.TypeOf; export const createdByOrNull = t.union([created_by, t.null]); export type CreatedByOrNull = t.TypeOf; -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const version = PositiveIntegerGreaterThanZero; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type Version = t.TypeOf; - -export const versionOrUndefined = t.union([version, t.undefined]); -export type VersionOrUndefined = t.TypeOf; - export const last_success_at = IsoDateString; export type LastSuccessAt = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index 981a5422a0594..eed20951190b7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -7,6 +7,44 @@ import * as t from 'io-ts'; +import { + Actions, + DefaultActionsArray, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, + DefaultThreatArray, + DefaultThrottleNull, + DefaultToString, + From, + RiskScoreMapping, + machine_learning_job_id, + risk_score, + threat_index, + concurrent_searches, + items_per_search, + threat_query, + threat_filters, + threat_mapping, + threat_language, + threat_indicator_path, + Threats, + type, + language, + severity, + SeverityMapping, + ThrottleOrNull, + MaxSignals, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { + version, + DefaultStringArray, + DefaultBooleanFalse, +} from '@kbn/securitysolution-io-ts-types'; + +import { DefaultListArray, ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { description, anomaly_threshold, @@ -16,63 +54,24 @@ import { timeline_id, timeline_title, meta, - machine_learning_job_id, - risk_score, - MaxSignals, name, - severity, Tags, To, - type, - Threats, threshold, - ThrottleOrNull, note, References, - Actions, Enabled, FalsePositives, - From, Interval, - language, query, rule_id, - version, building_block_type, license, rule_name_override, timestamp_override, Author, - RiskScoreMapping, - SeverityMapping, event_category_override, } from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, - threat_indicator_path, -} from '../types/threat_mapping'; - -import { - DefaultStringArray, - DefaultActionsArray, - DefaultBooleanFalse, - DefaultFromString, - DefaultIntervalString, - DefaultMaxSignalsNumber, - DefaultToString, - DefaultThreatArray, - DefaultThrottleNull, - DefaultListArray, - ListArray, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; /** * Big differences between this schema and the createRulesSchema diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index f3bef5ad7445f..03c0947aaf50c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -11,9 +11,8 @@ import { AddPrepackagedRulesSchema, } from './add_prepackaged_rules_schema'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { getAddPrepackagedRulesSchemaMock, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts index 9fe602bca8de4..e606e1f77fc2b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_bulk_schema.test.ts @@ -6,9 +6,7 @@ */ import { createRulesBulkSchema, CreateRulesBulkSchema } from './create_rules_bulk_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight } from '../../../test_utils'; -import { formatErrors } from '../../../format_errors'; +import { exactCheck, foldLeftRight, formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { getCreateRulesSchemaMock } from './rule_schemas.mock'; // only the basics of testing are here. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_signals_migration_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_signals_migration_schema.ts index f522448102405..55267c27ee37f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_signals_migration_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_signals_migration_schema.ts @@ -7,8 +7,8 @@ import * as t from 'io-ts'; +import { PositiveInteger, PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; import { index } from '../common/schemas'; -import { PositiveInteger, PositiveIntegerGreaterThanZero } from '../types'; export const signalsReindexOptions = t.partial({ requests_per_second: t.number, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts index f9c2d38ea4da2..186f170ad700f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.test.ts @@ -12,9 +12,8 @@ import { ExportRulesQuerySchema, ExportRulesQuerySchemaDecoded, } from './export_rules_schema'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; describe('create rules schema', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts index 4dcb7351aebd6..a83e1c2c0f462 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/export_rules_schema.ts @@ -7,11 +7,10 @@ import * as t from 'io-ts'; +import { DefaultExportFileName } from '@kbn/securitysolution-io-ts-alerting-types'; +import { DefaultStringBooleanFalse } from '@kbn/securitysolution-io-ts-types'; import { rule_id, FileName, ExcludeExportDetails } from '../common/schemas'; -import { DefaultExportFileName } from '../types/default_export_file_name'; -import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false'; - const objects = t.array(t.exact(t.type({ rule_id }))); export const exportRulesSchema = t.union([t.exact(t.type({ objects })), t.null]); export type ExportRulesSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts index 923d54e1d6fa7..cd221c9f0a4b5 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.test.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { FindRulesSchema, findRulesSchema } from './find_rules_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts index 25c880290454c..b04afd14375cc 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts @@ -7,9 +7,8 @@ import * as t from 'io-ts'; +import { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types'; import { queryFilter, fields, sort_field, sort_order, PerPage, Page } from '../common/schemas'; -import { DefaultPerPage } from '../types/default_per_page'; -import { DefaultPage } from '../types/default_page'; export const findRulesSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_signals_migration_status_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_signals_migration_status_schema.ts index 9441cabf73bac..c0969768d8be6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_signals_migration_status_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_signals_migration_status_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; -import { from } from '../common/schemas'; +import { from } from '@kbn/securitysolution-io-ts-alerting-types'; export const getSignalsMigrationStatusSchema = t.exact( t.type({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index 2caedd2e01193..1a937473c4b11 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { ImportRulesSchema, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index 8fa5809abe68b..70a7b8245f176 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -7,6 +7,47 @@ import * as t from 'io-ts'; +import { + Actions, + DefaultActionsArray, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, + DefaultThreatArray, + DefaultThrottleNull, + DefaultToString, + From, + machine_learning_job_id, + risk_score, + RiskScoreMapping, + threat_index, + items_per_search, + concurrent_searches, + threat_query, + threat_filters, + threat_mapping, + threat_language, + threat_indicator_path, + Threats, + type, + language, + severity, + SeverityMapping, + ThrottleOrNull, + MaxSignals, +} from '@kbn/securitysolution-io-ts-alerting-types'; + +import { + DefaultVersionNumber, + Version, + DefaultStringArray, + DefaultBooleanTrue, + OnlyFalseAllowed, + DefaultStringBooleanFalse, +} from '@kbn/securitysolution-io-ts-types'; +import { DefaultListArray, ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { description, anomaly_threshold, @@ -18,26 +59,15 @@ import { timeline_id, timeline_title, meta, - machine_learning_job_id, - risk_score, - MaxSignals, name, - severity, Tags, To, - type, - Threats, threshold, - ThrottleOrNull, note, - Version, References, - Actions, Enabled, FalsePositives, - From, Interval, - language, query, rule_id, id, @@ -50,39 +80,8 @@ import { rule_name_override, timestamp_override, Author, - RiskScoreMapping, - SeverityMapping, event_category_override, } from '../common/schemas'; -import { - threat_index, - items_per_search, - concurrent_searches, - threat_query, - threat_filters, - threat_mapping, - threat_language, - threat_indicator_path, -} from '../types/threat_mapping'; - -import { - DefaultStringArray, - DefaultActionsArray, - DefaultBooleanTrue, - DefaultFromString, - DefaultIntervalString, - DefaultMaxSignalsNumber, - DefaultToString, - DefaultThreatArray, - DefaultThrottleNull, - DefaultVersionNumber, - OnlyFalseAllowed, - DefaultStringBooleanFalse, - DefaultListArray, - ListArray, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; /** * Differences from this and the createRulesSchema are diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts index caa57525ab349..290f190562fe2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_bulk_schema.test.ts @@ -6,9 +6,7 @@ */ import { patchRulesBulkSchema, PatchRulesBulkSchema } from './patch_rules_bulk_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight } from '../../../test_utils'; -import { formatErrors } from '../../../format_errors'; +import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; import { PatchRulesSchema } from './patch_rules_schema'; // only the basics of testing are here. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts index 3dfa12acc29d5..5ac49deb32e0d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts @@ -7,9 +7,8 @@ import { patchRulesSchema, PatchRulesSchema, PatchRulesSchemaDecoded } from './patch_rules_schema'; import { getPatchRulesSchemaMock, getPatchRulesSchemaDecodedMock } from './patch_rules_schema.mock'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { getListArrayMock } from '../types/lists.mock'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 920fbaf4915c5..8c801e75af08c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -7,6 +7,32 @@ import * as t from 'io-ts'; +import { + actions, + from, + machine_learning_job_id, + risk_score, + risk_score_mapping, + threat_index, + concurrent_searches, + items_per_search, + threat_query, + threat_filters, + threat_mapping, + threat_language, + threat_indicator_path, + threats, + type, + language, + severity, + severity_mapping, + max_signals, + throttle, +} from '@kbn/securitysolution-io-ts-alerting-types'; + +import { version } from '@kbn/securitysolution-io-ts-types'; + +import { listArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import { description, anomaly_threshold, @@ -17,27 +43,16 @@ import { timeline_id, timeline_title, meta, - machine_learning_job_id, - risk_score, rule_id, name, - severity, - type, note, - version, - actions, false_positives, interval, - max_signals, - from, enabled, tags, - threats, threshold, - throttle, references, to, - language, query, id, building_block_type, @@ -45,21 +60,8 @@ import { license, rule_name_override, timestamp_override, - risk_score_mapping, - severity_mapping, event_category_override, } from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, - threat_indicator_path, -} from '../types/threat_mapping'; -import { listArrayOrUndefined } from '../types/lists'; /** * All of the patch elements should default to undefined if not set diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts index 5c1ded9a6ec50..fa167090b2f3a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_bulk_schema.test.ts @@ -6,9 +6,7 @@ */ import { queryRulesBulkSchema, QueryRulesBulkSchema } from './query_rules_bulk_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight } from '../../../test_utils'; -import { formatErrors } from '../../../format_errors'; +import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; // only the basics of testing are here. // see: query_rules_schema.test.ts for the bulk of the validation tests diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts index 95c045b90f800..566d752a8623e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_rules_schema.test.ts @@ -6,9 +6,8 @@ */ import { queryRulesSchema, QueryRulesSchema } from './query_rules_schema'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; describe('query_rules_schema', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.test.ts index ef7dfb9496ec3..65107079c5800 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.test.ts @@ -6,9 +6,8 @@ */ import { QuerySignalsSchema, querySignalsSchema } from './query_signals_index_schema'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; describe('query, aggs, size, _source and track_total_hits on signals index', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts index d7f17fd842b9f..81761dd085df6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/query_signals_index_schema.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; +import { PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; export const querySignalsSchema = t.exact( t.partial({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 70ff921d3b334..2e4a53766448b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -6,9 +6,8 @@ */ import { createRulesSchema, CreateRulesSchema, SavedQueryCreateSchema } from './rule_schemas'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; import { getCreateSavedQueryRulesSchemaMock, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index c7b33372e5953..dbc40763a0c6c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -7,8 +7,12 @@ import * as t from 'io-ts'; -import { listArray } from '../types/lists'; import { + actions, + from, + risk_score, + machine_learning_job_id, + risk_score_mapping, threat_filters, threat_query, threat_mapping, @@ -16,14 +20,20 @@ import { threat_indicator_path, concurrent_searches, items_per_search, -} from '../types/threat_mapping'; + threats, + severity_mapping, + severity, + max_signals, + throttle, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { listArray } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; + import { id, index, filters, event_category_override, - risk_score_mapping, - severity_mapping, building_block_type, note, license, @@ -35,25 +45,17 @@ import { author, description, false_positives, - from, rule_id, immutable, output_index, query, - machine_learning_job_id, - max_signals, - risk_score, - severity, - threats, to, references, - version, saved_id, threshold, anomaly_threshold, name, tags, - actions, interval, enabled, updated_at, @@ -66,7 +68,6 @@ import { last_success_message, last_failure_at, last_failure_message, - throttle, } from '../common/schemas'; const createSchema = < diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts index c189821090f5d..994691ecfa571 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/set_signal_status_schema.test.ts @@ -6,9 +6,8 @@ */ import { setSignalsStatusSchema, SetSignalsStatusSchema } from './set_signal_status_schema'; -import { exactCheck } from '../../../exact_check'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; describe('set signal status schema', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts index 3d0c579e3c46f..63217186affe7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_bulk_schema.test.ts @@ -6,9 +6,7 @@ */ import { updateRulesBulkSchema, UpdateRulesBulkSchema } from './update_rules_bulk_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight } from '../../../test_utils'; -import { formatErrors } from '../../../format_errors'; +import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; import { getUpdateRulesSchemaMock } from './rule_schemas.mock'; import { UpdateRulesSchema } from './rule_schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts index a584c9254b171..5ef7f80a09a2d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/error_schema.test.ts @@ -9,8 +9,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { errorSchema, ErrorSchema } from './error_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getErrorSchemaMock } from './error_schema.mocks'; describe('error_schema', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.test.ts index cb9876b0a165a..4c8cdbdd427af 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/import_rules_schema.test.ts @@ -10,8 +10,7 @@ import { left, Either } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; import { Errors } from 'io-ts'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('import_rules_schema', () => { test('it should validate an empty import response with no errors', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts index 67f3da8459410..fbbc754f597a0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_schema.test.ts @@ -11,8 +11,7 @@ import { PrePackagedRulesAndTimelinesSchema, prePackagedRulesAndTimelinesSchema, } from './prepackaged_rules_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('prepackaged_rules_schema', () => { test('it should validate an empty prepackaged response with defaults', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts index 5a7b1dd14c341..de44c7e5e37ad 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/prepackaged_rules_status_schema.test.ts @@ -11,8 +11,7 @@ import { PrePackagedRulesAndTimelinesStatusSchema, prePackagedRulesAndTimelinesStatusSchema, } from './prepackaged_rules_status_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('prepackaged_rules_schema', () => { test('it should validate an empty prepackaged response with defaults', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts index 3501ecfc0c1ba..55d5444bccf7e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_bulk_schema.test.ts @@ -11,8 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { getRulesSchemaMock } from './rules_schema.mocks'; import { getErrorSchemaMock } from './error_schema.mocks'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index 4865c0ee77d54..a8521c013f451 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -21,8 +21,7 @@ import { addThreatMatchFields, addEqlFields, } from './rules_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { getRulesSchemaMock, @@ -30,7 +29,7 @@ import { getThreatMatchingSchemaMock, getRulesEqlSchemaMock, } from './rules_schema.mocks'; -import { ListArray } from '../types/lists'; +import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index 6bd54973e064f..0924588600d38 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -10,50 +10,64 @@ import { isObject } from 'lodash/fp'; import { Either, left, fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { + actions, + from, + machine_learning_job_id, + risk_score, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, + threat_index, + concurrent_searches, + items_per_search, + threat_query, + threat_filters, + threat_mapping, + threat_language, + threat_indicator_path, + threats, + type, + language, + severity, + throttle, + max_signals, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { DefaultStringArray, version } from '@kbn/securitysolution-io-ts-types'; + +import { DefaultListArray } from '@kbn/securitysolution-io-ts-list-types'; import { isMlRule } from '../../../machine_learning/helpers'; import { isThresholdRule } from '../../utils'; import { - actions, anomaly_threshold, description, enabled, event_category_override, false_positives, - from, id, immutable, index, interval, rule_id, - language, name, output_index, - max_signals, - machine_learning_job_id, query, references, - severity, updated_by, tags, to, - risk_score, created_at, created_by, updated_at, saved_id, timeline_id, timeline_title, - type, - threats, threshold, - throttle, job_status, status_date, last_success_at, last_success_message, last_failure_at, last_failure_message, - version, filters, meta, note, @@ -62,23 +76,7 @@ import { rule_name_override, timestamp_override, } from '../common/schemas'; -import { - threat_index, - concurrent_searches, - items_per_search, - threat_query, - threat_filters, - threat_mapping, - threat_language, - threat_indicator_path, -} from '../types/threat_mapping'; -import { DefaultListArray } from '../types/lists_default_array'; -import { - DefaultStringArray, - DefaultRiskScoreMappingArray, - DefaultSeverityMappingArray, -} from '../types'; import { typeAndTimelineOnlySchema, TypeAndTimelineOnly } from './type_timeline_only_schema'; /** diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts index 7099b03de58b1..58612376760ba 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.test.ts @@ -9,8 +9,7 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; -import { exactCheck } from '../../../exact_check'; -import { foldLeftRight, getPaths } from '../../../test_utils'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; describe('prepackaged_rule_schema', () => { test('it should validate a a type and timeline_id together', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts index 3aff1b62dfb90..b164ab9b44e4f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/type_timeline_only_schema.ts @@ -7,7 +7,8 @@ import * as t from 'io-ts'; -import { timeline_id, type } from '../common/schemas'; +import { type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { timeline_id } from '../common/schemas'; /** * Special schema type that is only the type and the timeline_id. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts deleted file mode 100644 index 14b4f034358cd..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_boolean_true.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { DefaultBooleanTrue } from './default_boolean_true'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_boolean_true', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultBooleanTrue"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default true', () => { - const payload = null; - const decoded = DefaultBooleanTrue.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts deleted file mode 100644 index ca3ecb2bfb904..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/deafult_from_string.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DefaultFromString } from './default_from_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_from_string', () => { - test('it should validate a from string', () => { - const payload = 'now-20m'; - const decoded = DefaultFromString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultFromString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultFromString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "now-6m"', () => { - const payload = null; - const decoded = DefaultFromString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('now-6m'); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts deleted file mode 100644 index 993e96f5d3191..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 { DefaultActionsArray } from './default_actions_array'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { Actions } from '../common/schemas'; - -describe('default_actions_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultActionsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of actions', () => { - const payload: Actions = [ - { id: '123', group: 'group', action_type_id: 'action_type_id', params: {} }, - ]; - const decoded = DefaultActionsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = [ - { id: '123', group: 'group', action_type_id: 'action_type_id', params: {} }, - 5, - ]; - const decoded = DefaultActionsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultActionsArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultActionsArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts deleted file mode 100644 index 49302102ddee9..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { actions, Actions } from '../common/schemas'; - -/** - * Types the DefaultStringArray as: - * - If undefined, then a default action array will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultActionsArray = new t.Type( - 'DefaultActionsArray', - actions.is, - (input, context): Either => - input == null ? t.success([]) : actions.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.test.ts deleted file mode 100644 index 92c2669071f11..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 * as t from 'io-ts'; - -import { DefaultArray } from './default_array'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -const testSchema = t.keyof({ - valid: true, - also_valid: true, -}); -type TestSchema = t.TypeOf; - -const defaultArraySchema = DefaultArray(testSchema); - -describe('default_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of testSchema', () => { - const payload: TestSchema[] = ['valid']; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of valid testSchema strings', () => { - const payload = ['valid', 'also_valid']; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['valid', 123]; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "123" supplied to "DefaultArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an array with an invalid string', () => { - const payload = ['valid', 'invalid']; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid" supplied to "DefaultArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = defaultArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts deleted file mode 100644 index a6397754b23d7..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultArray as: - * - If undefined, then a default array will be set - * - If an array is sent in, then the array will be validated to ensure all elements are type C - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultArray = (codec: C) => { - const arrType = t.array(codec); - type ArrType = t.TypeOf; - return new t.Type( - 'DefaultArray', - arrType.is, - (input, context): Either => - input == null ? t.success([]) : arrType.validate(input, context), - t.identity - ); -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts deleted file mode 100644 index c01efa6e55d4c..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { DefaultBooleanFalse } from './default_boolean_false'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_boolean_false', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = DefaultBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts deleted file mode 100644 index 46e7bde55638a..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultBooleanFalse as: - * - If null or undefined, then a default false will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultBooleanFalse = new t.Type( - 'DefaultBooleanFalse', - t.boolean.is, - (input, context): Either => - input == null ? t.success(false) : t.boolean.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts deleted file mode 100644 index 3845150841c22..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultBooleanTrue as: - * - If null or undefined, then a default true will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultBooleanTrue = new t.Type( - 'DefaultBooleanTrue', - t.boolean.is, - (input, context): Either => - input == null ? t.success(true) : t.boolean.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts deleted file mode 100644 index 08a5354dc74bd..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DefaultEmptyString } from './default_empty_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_empty_string', () => { - test('it should validate a regular string', () => { - const payload = 'some string'; - const decoded = DefaultEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of ""', () => { - const payload = null; - const decoded = DefaultEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(''); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts deleted file mode 100644 index 77a06a928a2e3..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultEmptyString as: - * - If null or undefined, then a default of an empty string "" will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultEmptyString = new t.Type( - 'DefaultEmptyString', - t.string.is, - (input, context): Either => - input == null ? t.success('') : t.string.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts deleted file mode 100644 index f53c6d801dc53..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DefaultExportFileName } from './default_export_file_name'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_export_file_name', () => { - test('it should validate a regular string', () => { - const payload = 'some string'; - const decoded = DefaultExportFileName.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultExportFileName.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultExportFileName"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "export.ndjson"', () => { - const payload = null; - const decoded = DefaultExportFileName.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('export.ndjson'); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts deleted file mode 100644 index a2b2fb6eaad31..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultExportFileName as: - * - If null or undefined, then a default of "export.ndjson" will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultExportFileName = new t.Type( - 'DefaultExportFileName', - t.string.is, - (input, context): Either => - input == null ? t.success('export.ndjson') : t.string.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts deleted file mode 100644 index a2eb2a2b9f46f..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { from } from '../common/schemas'; -/** - * Types the DefaultFromString as: - * - If null or undefined, then a default of the string "now-6m" will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultFromString = new t.Type( - 'DefaultFromString', - t.string.is, - (input, context): Either => { - if (input == null) { - return t.success('now-6m'); - } - return from.validate(input, context); - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts deleted file mode 100644 index 99e15403ef3a2..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DefaultIntervalString } from './default_interval_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_interval_string', () => { - test('it should validate a interval string', () => { - const payload = '20m'; - const decoded = DefaultIntervalString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultIntervalString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultIntervalString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "5m"', () => { - const payload = null; - const decoded = DefaultIntervalString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('5m'); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts deleted file mode 100644 index fb3a5931199a1..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultIntervalString as: - * - If null or undefined, then a default of the string "5m" will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultIntervalString = new t.Type( - 'DefaultIntervalString', - t.string.is, - (input, context): Either => - input == null ? t.success('5m') : t.string.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts deleted file mode 100644 index 615f27e08b197..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { DefaultLanguageString } from './default_language_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { Language } from '../common/schemas'; - -describe('default_language_string', () => { - test('it should validate a string', () => { - const payload: Language = 'lucene'; - const decoded = DefaultLanguageString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultLanguageString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultLanguageString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "kuery"', () => { - const payload = null; - const decoded = DefaultLanguageString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('kuery'); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts deleted file mode 100644 index a40b0d8eeacaa..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { language } from '../common/schemas'; - -/** - * Types the DefaultLanguageString as: - * - If null or undefined, then a default of the string "kuery" will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultLanguageString = new t.Type( - 'DefaultLanguageString', - t.string.is, - (input, context): Either => - input == null ? t.success('kuery') : language.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts deleted file mode 100644 index bdfdc15150b9a..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { DefaultMaxSignalsNumber } from './default_max_signals_number'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; - -describe('default_from_string', () => { - test('it should validate a max signal number', () => { - const payload = 5; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultMaxSignals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a zero', () => { - const payload = 0; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultMaxSignals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a negative number', () => { - const payload = -1; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultMaxSignals"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of DEFAULT_MAX_SIGNALS', () => { - const payload = null; - const decoded = DefaultMaxSignalsNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts deleted file mode 100644 index d9209cdba4d00..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { max_signals } from '../common/schemas'; -import { DEFAULT_MAX_SIGNALS } from '../../../constants'; - -/** - * Types the default max signal: - * - Natural Number (positive integer and not a float), - * - greater than 1 - * - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultMaxSignalsNumber = new t.Type( - 'DefaultMaxSignals', - t.number.is, - (input, context): Either => { - return input == null ? t.success(DEFAULT_MAX_SIGNALS) : max_signals.validate(input, context); - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts deleted file mode 100644 index 9392454127586..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 { DefaultPage } from './default_page'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_page', () => { - test('it should validate a regular number greater than zero', () => { - const payload = 5; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a string of a number', () => { - const payload = '5'; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(5); - }); - - test('it should not validate a junk string', () => { - const payload = 'invalid-string'; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a zero', () => { - const payload = 0; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a negative number', () => { - const payload = -1; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 20', () => { - const payload = null; - const decoded = DefaultPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts deleted file mode 100644 index 2f2cdaf3b6cd2..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; - -/** - * Types the DefaultPerPage as: - * - If a string this will convert the string to a number - * - If null or undefined, then a default of 1 will be used - * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultPage = new t.Type( - 'DefaultPerPage', - t.number.is, - (input, context): Either => { - if (input == null) { - return t.success(1); - } else if (typeof input === 'string') { - return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); - } else { - return PositiveIntegerGreaterThanZero.validate(input, context); - } - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts deleted file mode 100644 index 5a550dca120e8..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 { DefaultPerPage } from './default_per_page'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_per_page', () => { - test('it should validate a regular number greater than zero', () => { - const payload = 5; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a string of a number', () => { - const payload = '5'; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(5); - }); - - test('it should not validate a junk string', () => { - const payload = 'invalid-string'; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "NaN" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a zero', () => { - const payload = 0; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a negative number', () => { - const payload = -1; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultPerPage"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 20', () => { - const payload = null; - const decoded = DefaultPerPage.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(20); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts deleted file mode 100644 index 124fdf25917fa..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; - -/** - * Types the DefaultPerPage as: - * - If a string this will convert the string to a number - * - If null or undefined, then a default of 20 will be used - * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultPerPage = new t.Type( - 'DefaultPerPage', - t.number.is, - (input, context): Either => { - if (input == null) { - return t.success(20); - } else if (typeof input === 'string') { - return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); - } else { - return PositiveIntegerGreaterThanZero.validate(input, context); - } - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts deleted file mode 100644 index 219b9d30b52ea..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { risk_score_mapping, RiskScoreMapping } from '../common/schemas'; - -/** - * Types the DefaultStringArray as: - * - If null or undefined, then a default risk_score_mapping array will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultRiskScoreMappingArray = new t.Type< - RiskScoreMapping, - RiskScoreMapping | undefined, - unknown ->( - 'DefaultRiskScoreMappingArray', - risk_score_mapping.is, - (input, context): Either => - input == null ? t.success([]) : risk_score_mapping.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts deleted file mode 100644 index fe65574383294..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { severity_mapping, SeverityMapping } from '../common/schemas'; - -/** - * Types the DefaultStringArray as: - * - If null or undefined, then a default severity_mapping array will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultSeverityMappingArray = new t.Type< - SeverityMapping, - SeverityMapping | undefined, - unknown ->( - 'DefaultSeverityMappingArray', - severity_mapping.is, - (input, context): Either => - input == null ? t.success([]) : severity_mapping.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts deleted file mode 100644 index c8995f6a7cef0..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { DefaultStringArray } from './default_string_array'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_string_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of strings', () => { - const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts deleted file mode 100644 index be4b5563ddb13..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultStringArray as: - * - If undefined, then a default array will be set - * - If an array is sent in, then the array will be validated to ensure all elements are a string - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultStringArray = new t.Type( - 'DefaultStringArray', - t.array(t.string).is, - (input, context): Either => - input == null ? t.success([]) : t.array(t.string).validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts deleted file mode 100644 index 2ee3e87e37e00..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { DefaultStringBooleanFalse } from './default_string_boolean_false'; - -describe('default_string_boolean_false', () => { - test('it should validate a boolean false', () => { - const payload = false; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a boolean true', () => { - const payload = true; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); - - test('it should return a default false when given a string of "false"', () => { - const payload = 'false'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); - - test('it should return a default true when given a string of "true"', () => { - const payload = 'true'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); - - test('it should return a default true when given a string of "TruE"', () => { - const payload = 'TruE'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(true); - }); - - test('it should not work with a string of junk "junk"', () => { - const payload = 'junk'; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "junk" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not work with an empty string', () => { - const payload = ''; - const decoded = DefaultStringBooleanFalse.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "" supplied to "DefaultStringBooleanFalse"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts deleted file mode 100644 index 69bd4564fd3bb..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultStringBooleanFalse as: - * - If a string this will convert the string to a boolean - * - If null or undefined, then a default false will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultStringBooleanFalse = new t.Type( - 'DefaultStringBooleanFalse', - t.boolean.is, - (input, context): Either => { - if (input == null) { - return t.success(false); - } else if (typeof input === 'string' && input.toLowerCase() === 'true') { - return t.success(true); - } else if (typeof input === 'string' && input.toLowerCase() === 'false') { - return t.success(false); - } else { - return t.boolean.validate(input, context); - } - }, - t.identity -); - -export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts deleted file mode 100644 index d04338586371f..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 { DefaultThreatArray } from './default_threat_array'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { Threats } from '../common/schemas'; - -describe('default_threat_null', () => { - test('it should validate an empty array', () => { - const payload: Threats = []; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of threats', () => { - const payload: Threats = [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - ]; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - 5, - ]; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultThreatArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default empty array if not provided a value', () => { - const payload = null; - const decoded = DefaultThreatArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts deleted file mode 100644 index 86c7ab245d6f3..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { Threats, threats } from '../common/schemas'; - -/** - * Types the DefaultThreatArray as: - * - If null or undefined, then an empty array will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultThreatArray = new t.Type( - 'DefaultThreatArray', - threats.is, - (input, context): Either => - input == null ? t.success([]) : threats.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts deleted file mode 100644 index 1f29718533c13..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { DefaultThrottleNull } from './default_throttle_null'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { Throttle } from '../common/schemas'; - -describe('default_throttle_null', () => { - test('it should validate a throttle string', () => { - const payload: Throttle = 'some string'; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = 5; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultThreatNull"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default "null" if not provided a value', () => { - const payload = undefined; - const decoded = DefaultThrottleNull.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(null); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts deleted file mode 100644 index db6eb5eab32ae..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { ThrottleOrNull, throttle } from '../common/schemas'; - -/** - * Types the DefaultThrottleNull as: - * - If null or undefined, then a null will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultThrottleNull = new t.Type( - 'DefaultThreatNull', - throttle.is, - (input, context): Either => - input == null ? t.success(null) : throttle.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts deleted file mode 100644 index defb348b7e009..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DefaultToString } from './default_to_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_to_string', () => { - test('it should validate a to string', () => { - const payload = 'now-5m'; - const decoded = DefaultToString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultToString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultToString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of "now"', () => { - const payload = null; - const decoded = DefaultToString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('now'); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts deleted file mode 100644 index 05c93951bfe1e..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the DefaultToString as: - * - If null or undefined, then a default of the string "now" will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultToString = new t.Type( - 'DefaultToString', - t.string.is, - (input, context): Either => - input == null ? t.success('now') : t.string.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts deleted file mode 100644 index e2b2ac5015967..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { DefaultUuid } from './default_uuid'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_uuid', () => { - test('it should validate a regular string', () => { - const payload = '1'; - const decoded = DefaultUuid.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = DefaultUuid.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of a uuid', () => { - const payload = null; - const decoded = DefaultUuid.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts deleted file mode 100644 index ecb8cd00ff308..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import uuid from 'uuid'; - -import { NonEmptyString } from './non_empty_string'; - -/** - * Types the DefaultUuid as: - * - If null or undefined, then a default string uuid.v4() will be - * created otherwise it will be checked just against an empty string - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultUuid = new t.Type( - 'DefaultUuid', - t.string.is, - (input, context): Either => - input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts deleted file mode 100644 index a50a9ed7815b3..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { DefaultVersionNumber } from './default_version_number'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_version_number', () => { - test('it should validate a version number', () => { - const payload = 5; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a 0', () => { - const payload = 0; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a -1', () => { - const payload = -1; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a string', () => { - const payload = '5'; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultVersionNumber"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default of 1', () => { - const payload = null; - const decoded = DefaultVersionNumber.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(1); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts deleted file mode 100644 index 625973694a244..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; -import { version, Version } from '../common/schemas'; - -/** - * Types the DefaultVersionNumber as: - * - If null or undefined, then a default of the number 1 will be used - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultVersionNumber = new t.Type( - 'DefaultVersionNumber', - version.is, - (input, context): Either => - input == null ? t.success(1) : version.validate(input, context), - t.identity -); - -export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts deleted file mode 100644 index ff69075580ad9..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './default_actions_array'; -export * from './default_array'; -export * from './default_boolean_false'; -export * from './default_boolean_true'; -export * from './default_empty_string'; -export * from './default_export_file_name'; -export * from './default_from_string'; -export * from './default_interval_string'; -export * from './default_language_string'; -export * from './default_max_signals_number'; -export * from './default_page'; -export * from './default_per_page'; -export * from './default_risk_score_mapping_array'; -export * from './default_severity_mapping_array'; -export * from './default_string_array'; -export * from './default_string_boolean_false'; -export * from './default_threat_array'; -export * from './default_throttle_null'; -export * from './default_to_string'; -export * from './default_uuid'; -export * from './default_version_number'; -export * from './iso_date_string'; -export * from './lists'; -export * from './lists_default_array'; -export * from './non_empty_array'; -export * from './non_empty_string'; -export * from './only_false_allowed'; -export * from './positive_integer'; -export * from './positive_integer_greater_than_zero'; -export * from './references_default_array'; -export * from './risk_score'; -export * from './threat_mapping'; -export * from './uuid'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.test.ts deleted file mode 100644 index ff45aef21f837..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { IsoDateString } from './iso_date_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('ios_date_string', () => { - test('it should validate a iso string', () => { - const payload = '2020-02-26T00:32:34.541Z'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an epoch number', () => { - const payload = '1582677283067'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1582677283067" supplied to "IsoDateString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a number such as 2000', () => { - const payload = '2000'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "2000" supplied to "IsoDateString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a UTC', () => { - const payload = 'Wed, 26 Feb 2020 00:36:20 GMT'; - const decoded = IsoDateString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "Wed, 26 Feb 2020 00:36:20 GMT" supplied to "IsoDateString"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts deleted file mode 100644 index 9c8f490d2d59a..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the IsoDateString as: - * - A string that is an ISOString - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const IsoDateString = new t.Type( - 'IsoDateString', - t.string.is, - (input, context): Either => { - if (typeof input === 'string') { - try { - const parsed = new Date(input); - if (parsed.toISOString() === input) { - return t.success(input); - } else { - return t.failure(input, context); - } - } catch (err) { - return t.failure(input, context); - } - } else { - return t.failure(input, context); - } - }, - t.identity -); - -export type IsoDateStringC = typeof IsoDateString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts index 62a3c51b31bd0..70f41539e8466 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { List, ListArray } from './lists'; +import { List, ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { ENDPOINT_LIST_ID } from '../../../shared_imports'; export const getListMock = (): List => ({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts deleted file mode 100644 index 28b70f51742a7..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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 { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../../test_utils'; - -import { getEndpointListMock, getListMock, getListArrayMock } from './lists.mock'; -import { - List, - ListArray, - ListArrayOrUndefined, - list, - listArray, - listArrayOrUndefined, -} from './lists'; - -describe('Lists', () => { - describe('list', () => { - test('it should validate a list', () => { - const payload = getListMock(); - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a list with "namespace_type" of "agnostic"', () => { - const payload = getEndpointListMock(); - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a list without an "id"', () => { - const payload = getListMock(); - // @ts-expect-error - delete payload.id; - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "id"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a list without "namespace_type"', () => { - const payload = getListMock(); - // @ts-expect-error - delete payload.namespace_type; - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "undefined" supplied to "namespace_type"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should strip out extra keys', () => { - const payload: List & { - extraKey?: string; - } = getListMock(); - payload.extraKey = 'some value'; - const decoded = list.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(getListMock()); - }); - }); - - describe('listArray', () => { - test('it should validate an array of lists', () => { - const payload = getListArrayMock(); - const decoded = listArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate when unexpected type found in array', () => { - const payload = ([1] as unknown) as ListArray; - const decoded = listArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('listArrayOrUndefined', () => { - test('it should validate an array of lists', () => { - const payload = getListArrayMock(); - const decoded = listArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate when undefined', () => { - const payload = undefined; - const decoded = listArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not allow an item that is not of type "list" in array', () => { - const payload = ([1] as unknown) as ListArrayOrUndefined; - const decoded = listArrayOrUndefined.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', - 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', - ]); - expect(message.schema).toEqual({}); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts deleted file mode 100644 index e2c3ee88f6a65..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 * as t from 'io-ts'; - -import { exceptionListType, namespaceType } from '@kbn/securitysolution-io-ts-list-types'; - -import { NonEmptyString } from './non_empty_string'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const list = t.exact( - t.type({ - id: NonEmptyString, - list_id: NonEmptyString, - type: exceptionListType, - namespace_type: namespaceType, - }) -); - -export type List = t.TypeOf; -export const listArray = t.array(list); -export type ListArray = t.TypeOf; -export const listArrayOrUndefined = t.union([listArray, t.undefined]); -export type ListArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts deleted file mode 100644 index 0d989b4de0b28..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; - -import { foldLeftRight, getPaths } from '../../../test_utils'; - -import { DefaultListArray } from './lists_default_array'; -import { getListArrayMock } from './lists.mock'; - -describe('lists_default_array', () => { - test('it should return a default array when null', () => { - const payload = null; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should return a default array when undefined', () => { - const payload = undefined; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of lists', () => { - const payload = getListArrayMock(); - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array of non accepted types', () => { - // Terrible casting for purpose of tests - const payload = [1] as unknown; - const decoded = DefaultListArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "1" supplied to "DefaultListArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts deleted file mode 100644 index eba48c0419ec2..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -import { ListArray, list } from './lists'; - -/** - * Types the DefaultListArray as: - * - If null or undefined, then a default array of type list will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const DefaultListArray = new t.Type( - 'DefaultListArray', - t.array(list).is, - (input, context): Either => - input == null ? t.success([]) : t.array(list).validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts deleted file mode 100644 index c08cb70f544ee..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 * as t from 'io-ts'; - -import { NonEmptyArray } from './non_empty_array'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -const testSchema = t.keyof({ - valid: true, - also_valid: true, -}); -type TestSchema = t.TypeOf; - -const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray'); - -describe('non empty array', () => { - test('it should generate the correct name for non empty array', () => { - const newTestSchema = NonEmptyArray(testSchema); - expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">'); - }); - - test('it should use a supplied name override', () => { - const newTestSchema = NonEmptyArray(testSchema, 'someName'); - expect(newTestSchema.name).toEqual('someName'); - }); - - test('it should NOT validate an empty array', () => { - const payload: string[] = []; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should validate an array of testSchema', () => { - const payload: TestSchema[] = ['valid']; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of valid testSchema strings', () => { - const payload: TestSchema[] = ['valid', 'also_valid']; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['valid', 123]; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "123" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an array with an invalid string', () => { - const payload = ['valid', 'invalid']; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a null value', () => { - const payload = null; - const decoded = nonEmptyArraySchema.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "null" supplied to "TestSchemaArray"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts deleted file mode 100644 index 1b5261931230d..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const NonEmptyArray = ( - codec: C, - name: string = `NonEmptyArray<${codec.name}>` -) => { - const arrType = t.array(codec); - type ArrType = t.TypeOf; - return new t.Type( - name, - arrType.is, - (input, context): Either => { - if (Array.isArray(input) && input.length === 0) { - return t.failure(input, context); - } else { - return arrType.validate(input, context); - } - }, - t.identity - ); -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts deleted file mode 100644 index 470f8bc81c5d6..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { NonEmptyString } from './non_empty_string'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('non_empty_string', () => { - test('it should validate a regular string', () => { - const payload = '1'; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "NonEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "" supplied to "NonEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate empty spaces', () => { - const payload = ' '; - const decoded = NonEmptyString.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value " " supplied to "NonEmptyString"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts deleted file mode 100644 index e01eabd4c434e..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the NonEmptyString as: - * - A string that is not empty - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const NonEmptyString = new t.Type( - 'NonEmptyString', - t.string.is, - (input, context): Either => { - if (typeof input === 'string' && input.trim() !== '') { - return t.success(input); - } else { - return t.failure(input, context); - } - }, - t.identity -); - -export type NonEmptyStringC = typeof NonEmptyString; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts deleted file mode 100644 index 7001cb8254d2b..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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. - */ -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; - -import { NonEmptyArray } from './non_empty_array'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const machine_learning_job_id_normalized = NonEmptyArray(t.string); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type MachineLearningJobIdNormalized = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const machineLearningJobIdNormalizedOrUndefined = t.union([ - machine_learning_job_id_normalized, - t.undefined, -]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type MachineLearningJobIdNormalizedOrUndefined = t.TypeOf< - typeof machineLearningJobIdNormalizedOrUndefined ->; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.test.ts deleted file mode 100644 index a22266fb04f6a..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { OnlyFalseAllowed } from './only_false_allowed'; - -describe('only_false_allowed', () => { - test('it should validate a boolean false as false', () => { - const payload = false; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a boolean true', () => { - const payload = true; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "true" supplied to "DefaultBooleanTrue"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate a number', () => { - const payload = 5; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultBooleanTrue"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default false', () => { - const payload = null; - const decoded = OnlyFalseAllowed.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(false); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts deleted file mode 100644 index 8150979cf2947..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the OnlyFalseAllowed as: - * - If null or undefined, then a default false will be set - * - If true is sent in then this will return an error - * - If false is sent in then this will allow it only false - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const OnlyFalseAllowed = new t.Type( - 'DefaultBooleanTrue', - t.boolean.is, - (input, context): Either => { - if (input == null) { - return t.success(false); - } else { - if (typeof input === 'boolean' && input === false) { - return t.success(false); - } else { - return t.failure(input, context); - } - } - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts deleted file mode 100644 index 1771d0515fa20..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the positive integer are: - * - Natural Number (positive integer and not a float), - * - zero or greater - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const PositiveInteger = new t.Type( - 'PositiveInteger', - t.number.is, - (input, context): Either => { - return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 - ? t.success(input) - : t.failure(input, context); - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts deleted file mode 100644 index dfb8b90633c83..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('positive_integer_greater_than_zero', () => { - test('it should validate a positive number', () => { - const payload = 1; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a zero', () => { - const payload = 0; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a negative number', () => { - const payload = -1; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a string', () => { - const payload = 'some string'; - const decoded = PositiveIntegerGreaterThanZero.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts deleted file mode 100644 index ddfb82b748ee3..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the positive integer greater than zero is: - * - Natural Number (positive integer and not a float), - * - 1 or greater - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const PositiveIntegerGreaterThanZero = new t.Type( - 'PositiveIntegerGreaterThanZero', - t.number.is, - (input, context): Either => { - return typeof input === 'number' && Number.isSafeInteger(input) && input >= 1 - ? t.success(input) - : t.failure(input, context); - }, - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/postive_integer.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/postive_integer.test.ts deleted file mode 100644 index b0c64e70787af..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/postive_integer.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { PositiveInteger } from './positive_integer'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('positive_integer_greater_than_zero', () => { - test('it should validate a positive number', () => { - const payload = 1; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a zero', () => { - const payload = 0; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a negative number', () => { - const payload = -1; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveInteger"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a string', () => { - const payload = 'some string'; - const decoded = PositiveInteger.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "PositiveInteger"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts deleted file mode 100644 index c8995f6a7cef0..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { DefaultStringArray } from './default_string_array'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('default_string_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate an array of strings', () => { - const payload = ['value 1', 'value 2']; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an array with a number', () => { - const payload = ['value 1', 5]; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "DefaultStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts deleted file mode 100644 index a76606911f4ae..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the ReferencesDefaultArray as: - * - If null or undefined, then a default array will be set - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const ReferencesDefaultArray = new t.Type( - 'referencesWithDefaultArray', - t.array(t.string).is, - (input, context): Either => - input == null ? t.success([]) : t.array(t.string).validate(input, context), - t.identity -); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.test.ts deleted file mode 100644 index 67e8c801391ce..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 { RiskScore } from './risk_score'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('risk_score', () => { - test('it should validate a positive number', () => { - const payload = 1; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate a zero', () => { - const payload = 0; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should NOT validate a negative number', () => { - const payload = -1; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "RiskScore"']); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a string', () => { - const payload = 'some string'; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "some string" supplied to "RiskScore"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should NOT validate a risk score greater than 100', () => { - const payload = 101; - const decoded = RiskScore.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "101" supplied to "RiskScore"']); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts deleted file mode 100644 index e518bf13fd181..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -/** - * Types the risk score as: - * - Natural Number (positive integer and not a float), - * - Between the values [0 and 100] inclusive. - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const RiskScore = new t.Type( - 'RiskScore', - t.number.is, - (input, context): Either => { - return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 && input <= 100 - ? t.success(input) - : t.failure(input, context); - }, - t.identity -); - -export type RiskScoreC = typeof RiskScore; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts index e8a62822bfe5a..fb063a130e83f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Threats } from '../common/schemas'; +import { Threats } from '@kbn/securitysolution-io-ts-alerting-types'; export const getThreatMock = (): Threats => [ { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts deleted file mode 100644 index c89c1059eedf0..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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 { - concurrent_searches, - items_per_search, - ThreatMapping, - threatMappingEntries, - ThreatMappingEntries, - threat_mapping, -} from './threat_mapping'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; -import { exactCheck } from '../../../exact_check'; - -describe('threat_mapping', () => { - describe('threatMappingEntries', () => { - test('it should validate an entry', () => { - const payload: ThreatMappingEntries = [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - }, - ]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should fail validation with an extra entry item', () => { - const payload: ThreatMappingEntries & Array<{ extra: string }> = [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - extra: 'blah', - }, - ]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation with a non string', () => { - const payload = ([ - { - field: 5, - type: 'mapping', - value: 'field.one', - }, - ] as unknown) as ThreatMappingEntries[]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "field"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation with a wrong type', () => { - const payload = ([ - { - field: 'field.one', - type: 'invalid', - value: 'field.one', - }, - ] as unknown) as ThreatMappingEntries[]; - const decoded = threatMappingEntries.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "invalid" supplied to "type"', - ]); - expect(message.schema).toEqual({}); - }); - }); - - describe('threat_mapping', () => { - test('it should validate a threat mapping', () => { - const payload: ThreatMapping = [ - { - entries: [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - }, - ], - }, - ]; - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - }); - - test('it should fail validate with an extra key', () => { - const payload: ThreatMapping & Array<{ extra: string }> = [ - { - entries: [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - }, - ], - extra: 'invalid', - }, - ]; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validate with an extra inner entry', () => { - const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [ - { - entries: [ - { - field: 'field.one', - type: 'mapping', - value: 'field.one', - extra: 'blah', - }, - ], - }, - ]; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); - expect(message.schema).toEqual({}); - }); - - test('it should fail validate with an extra inner entry with the wrong data type', () => { - const payload = ([ - { - entries: [ - { - field: 5, - type: 'mapping', - value: 'field.one', - }, - ], - }, - ] as unknown) as ThreatMapping; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "entries,field"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validate with empty array', () => { - const payload: string[] = []; - - const decoded = threat_mapping.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "[]" supplied to "NonEmptyArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when concurrent_searches is < 0', () => { - const payload = -1; - const decoded = concurrent_searches.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when concurrent_searches is 0', () => { - const payload = 0; - const decoded = concurrent_searches.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when items_per_search is 0', () => { - const payload = 0; - const decoded = items_per_search.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should fail validation when items_per_search is < 0', () => { - const payload = -1; - const decoded = items_per_search.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', - ]); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts deleted file mode 100644 index 707b51e6b8965..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import * as t from 'io-ts'; -import { language } from '../common/schemas'; -import { NonEmptyArray } from './non_empty_array'; -import { NonEmptyString } from './non_empty_string'; -import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_query = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatQuery = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatQueryOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_indicator_path = t.string; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatIndicatorPath = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatIndicatorPathOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatFilters = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatFiltersOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatMapEntry = t.exact( - t.type({ - field: NonEmptyString, - type: t.keyof({ mapping: null }), - value: NonEmptyString, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatMapEntry = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatMappingEntries = t.array(threatMapEntry); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatMappingEntries = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatMap = t.exact( - t.type({ - entries: threatMappingEntries, - }) -); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatMap = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatMapping = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatMappingOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_index = t.array(t.string); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatIndex = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatIndexOrUndefined = t.union([threat_index, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatIndexOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threat_language = t.union([language, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatLanguage = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ThreatLanguageOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const concurrent_searches = PositiveIntegerGreaterThanZero; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ConcurrentSearches = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const concurrentSearchesOrUndefined = t.union([concurrent_searches, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ConcurrentSearchesOrUndefined = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const items_per_search = PositiveIntegerGreaterThanZero; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ItemsPerSearch = t.TypeOf; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const itemsPerSearchOrUndefined = t.union([items_per_search, t.undefined]); - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export type ItemsPerSearchOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.test.ts deleted file mode 100644 index 2a98fd16c7107..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { UUID } from './uuid'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('uuid', () => { - test('it should validate a uuid', () => { - const payload = '4656dc92-5832-11ea-8e2d-0242ac130003'; - const decoded = UUID.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate a non uuid', () => { - const payload = '4656dc92-5832-11ea-8e2d'; - const decoded = UUID.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "4656dc92-5832-11ea-8e2d" supplied to "UUID"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should not validate an empty string', () => { - const payload = ''; - const decoded = UUID.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "UUID"']); - expect(message.schema).toEqual({}); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts deleted file mode 100644 index 79a130b26a6fd..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { Either } from 'fp-ts/lib/Either'; - -const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -/** - * Types the risk score as: - * - Natural Number (positive integer and not a float), - * - Between the values [0 and 100] inclusive. - * @deprecated Use packages/kbn-securitysolution-io-ts-utils - */ -export const UUID = new t.Type( - 'UUID', - t.string.is, - (input, context): Either => { - return typeof input === 'string' && regex.test(input) - ? t.success(input) - : t.failure(input, context); - }, - t.identity -); - -export type UUIDC = typeof UUID; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 611d23fd1ce22..6aa672881ff70 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -9,9 +9,10 @@ import { isEmpty } from 'lodash'; import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; +import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { CreateExceptionListItemSchema, ExceptionListItemSchema } from '../shared_imports'; -import { Type, JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; +import { JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; export const hasLargeValueItem = ( exceptionItems: Array diff --git a/x-pack/plugins/security_solution/common/exact_check.test.ts b/x-pack/plugins/security_solution/common/exact_check.test.ts deleted file mode 100644 index d4a4ad4ce76ce..0000000000000 --- a/x-pack/plugins/security_solution/common/exact_check.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { left, right, Either } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { exactCheck, findDifferencesRecursive } from './exact_check'; -import { foldLeftRight, getPaths } from './test_utils'; - -describe('exact_check', () => { - test('it returns an error if given extra object properties', () => { - const someType = t.exact( - t.type({ - a: t.string, - }) - ); - const payload = { a: 'test', b: 'test' }; - const decoded = someType.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['invalid keys "b"']); - expect(message.schema).toEqual({}); - }); - - test('it returns an error if the data type is not as expected', () => { - type UnsafeCastForTest = Either< - t.Errors, - { - a: number; - } - >; - - const someType = t.exact( - t.type({ - a: t.string, - }) - ); - - const payload = { a: 1 }; - const decoded = someType.decode(payload); - const checked = exactCheck(payload, decoded as UnsafeCastForTest); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']); - expect(message.schema).toEqual({}); - }); - - test('it does NOT return an error if given normal object properties', () => { - const someType = t.exact( - t.type({ - a: t.string, - }) - ); - const payload = { a: 'test' }; - const decoded = someType.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it will return an existing error and not validate', () => { - const payload = { a: 'test' }; - const validationError: t.ValidationError = { - value: 'Some existing error', - context: [], - message: 'some error', - }; - const error: t.Errors = [validationError]; - const leftValue = left(error); - const checked = exactCheck(payload, leftValue); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual(['some error']); - expect(message.schema).toEqual({}); - }); - - test('it will work with a regular "right" payload without any decoding', () => { - const payload = { a: 'test' }; - const rightValue = right(payload); - const checked = exactCheck(payload, rightValue); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual({ a: 'test' }); - }); - - test('it will work with decoding a null payload when the schema expects a null', () => { - const someType = t.union([ - t.exact( - t.type({ - a: t.string, - }) - ), - t.null, - ]); - const payload = null; - const decoded = someType.decode(payload); - const checked = exactCheck(payload, decoded); - const message = pipe(checked, foldLeftRight); - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(null); - }); - - test('it should find no differences recursively with two empty objects', () => { - const difference = findDifferencesRecursive({}, {}); - expect(difference).toEqual([]); - }); - - test('it should find a single difference with two objects with different keys', () => { - const difference = findDifferencesRecursive({ a: 1 }, { b: 1 }); - expect(difference).toEqual(['a']); - }); - - test('it should find a two differences with two objects with multiple different keys', () => { - const difference = findDifferencesRecursive({ a: 1, c: 1 }, { b: 1 }); - expect(difference).toEqual(['a', 'c']); - }); - - test('it should find no differences with two objects with the same keys', () => { - const difference = findDifferencesRecursive({ a: 1, b: 1 }, { a: 1, b: 1 }); - expect(difference).toEqual([]); - }); - - test('it should find a difference with two deep objects with different same keys', () => { - const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1, b: { d: 1 } }); - expect(difference).toEqual(['c']); - }); - - test('it should find a difference within an array', () => { - const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ a: 1 }] }); - expect(difference).toEqual(['c']); - }); - - test('it should find a no difference when using arrays that are identical', () => { - const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ c: 1 }] }); - expect(difference).toEqual([]); - }); - - test('it should find differences when one has an array and the other does not', () => { - const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1 }); - expect(difference).toEqual(['b', '[{"c":1}]']); - }); - - test('it should find differences when one has an deep object and the other does not', () => { - const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1 }); - expect(difference).toEqual(['b', '{"c":1}']); - }); - - test('it should find differences when one has a deep object with multiple levels and the other does not', () => { - const difference = findDifferencesRecursive({ a: 1, b: { c: { d: 1 } } }, { a: 1 }); - expect(difference).toEqual(['b', '{"c":{"d":1}}']); - }); - - test('it tests two deep objects as the same with no key differences', () => { - const difference = findDifferencesRecursive( - { a: 1, b: { c: { d: 1 } } }, - { a: 1, b: { c: { d: 1 } } } - ); - expect(difference).toEqual([]); - }); - - test('it tests two deep objects with just one deep key difference', () => { - const difference = findDifferencesRecursive( - { a: 1, b: { c: { d: 1 } } }, - { a: 1, b: { c: { e: 1 } } } - ); - expect(difference).toEqual(['d']); - }); - - test('it should not find any differences when the original and decoded are both null', () => { - const difference = findDifferencesRecursive(null, null); - expect(difference).toEqual([]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/exact_check.ts b/x-pack/plugins/security_solution/common/exact_check.ts deleted file mode 100644 index 5334989ea085b..0000000000000 --- a/x-pack/plugins/security_solution/common/exact_check.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { left, Either, fold, right } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { isObject, get } from 'lodash/fp'; - -/** - * Given an original object and a decoded object this will return an error - * if and only if the original object has additional keys that the decoded - * object does not have. If the original decoded already has an error, then - * this will return the error as is and not continue. - * - * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema - * needs to remove additional keys before the compare - * - * You might not need this in the future if the below issue is solved: - * https://github.com/gcanti/io-ts/issues/322 - * - * @param original The original to check if it has additional keys - * @param decoded The decoded either which has either an existing error or the - * decoded object which could have additional keys stripped from it. - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.ts - */ -export const exactCheck = ( - original: unknown, - decoded: Either -): Either => { - const onLeft = (errors: t.Errors): Either => left(errors); - const onRight = (decodedValue: T): Either => { - const differences = findDifferencesRecursive(original, decodedValue); - if (differences.length !== 0) { - const validationError: t.ValidationError = { - value: differences, - context: [], - message: `invalid keys "${differences.join(',')}"`, - }; - const error: t.Errors = [validationError]; - return left(error); - } else { - return right(decodedValue); - } - }; - return pipe(decoded, fold(onLeft, onRight)); -}; - -export const findDifferencesRecursive = (original: unknown, decodedValue: T): string[] => { - if (decodedValue === null && original === null) { - // both the decodedValue and the original are null which indicates that they are equal - // so do not report differences - return []; - } else if (decodedValue == null) { - try { - // It is null and painful when the original contains an object or an array - // the the decoded value does not have. - return [JSON.stringify(original)]; - } catch (err) { - return ['circular reference']; - } - } else if (typeof original !== 'object' || original == null) { - // We are not an object or null so do not report differences - return []; - } else { - const decodedKeys = Object.keys(decodedValue); - const differences = Object.keys(original).flatMap((originalKey) => { - const foundKey = decodedKeys.some((key) => key === originalKey); - const topLevelKey = foundKey ? [] : [originalKey]; - // I use lodash to cheat and get an any (not going to lie ;-)) - const valueObjectOrArrayOriginal = get(originalKey, original); - const valueObjectOrArrayDecoded = get(originalKey, decodedValue); - if (isObject(valueObjectOrArrayOriginal)) { - return [ - ...topLevelKey, - ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), - ]; - } else if (Array.isArray(valueObjectOrArrayOriginal)) { - return [ - ...topLevelKey, - ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => - findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) - ), - ]; - } else { - return topLevelKey; - } - }); - return differences; - } -}; diff --git a/x-pack/plugins/security_solution/common/format_errors.test.ts b/x-pack/plugins/security_solution/common/format_errors.test.ts deleted file mode 100644 index 149bae85fec8a..0000000000000 --- a/x-pack/plugins/security_solution/common/format_errors.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { formatErrors } from './format_errors'; - -describe('utils', () => { - test('returns an empty error message string if there are no errors', () => { - const errors: t.Errors = []; - const output = formatErrors(errors); - expect(output).toEqual([]); - }); - - test('returns a single error message if given one', () => { - const validationError: t.ValidationError = { - value: 'Some existing error', - context: [], - message: 'some error', - }; - const errors: t.Errors = [validationError]; - const output = formatErrors(errors); - expect(output).toEqual(['some error']); - }); - - test('returns a two error messages if given two', () => { - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context: [], - message: 'some error 1', - }; - const validationError2: t.ValidationError = { - value: 'Some existing error 2', - context: [], - message: 'some error 2', - }; - const errors: t.Errors = [validationError1, validationError2]; - const output = formatErrors(errors); - expect(output).toEqual(['some error 1', 'some error 2']); - }); - - test('it filters out duplicate error messages', () => { - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context: [], - message: 'some error 1', - }; - const validationError2: t.ValidationError = { - value: 'Some existing error 1', - context: [], - message: 'some error 1', - }; - const errors: t.Errors = [validationError1, validationError2]; - const output = formatErrors(errors); - expect(output).toEqual(['some error 1']); - }); - - test('will use message before context if it is set', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - message: 'I should be used first', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['I should be used first']); - }); - - test('will use context entry of a single string', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); - }); - - test('will use two context entries of two strings', () => { - const context: t.Context = ([ - { key: 'some string key 1' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', - ]); - }); - - test('will filter out and not use any strings of numbers', () => { - const context: t.Context = ([ - { key: '5' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use null', () => { - const context: t.Context = ([ - { key: null }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use empty strings', () => { - const context: t.Context = ([ - { key: '' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will use a name context if it cannot find a keyContext', () => { - const context: t.Context = ([ - { key: '' }, - { key: '', type: { name: 'someName' } }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "someName"']); - }); - - test('will return an empty string if name does not exist but type does', () => { - const context: t.Context = ([{ key: '' }, { key: '', type: {} }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to ""']); - }); - - test('will stringify an error value', () => { - const context: t.Context = ([ - { key: '' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: { foo: 'some error' }, - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "{"foo":"some error"}" supplied to "some string key 2"', - ]); - }); -}); diff --git a/x-pack/plugins/security_solution/common/format_errors.ts b/x-pack/plugins/security_solution/common/format_errors.ts deleted file mode 100644 index 16925699b0fcf..0000000000000 --- a/x-pack/plugins/security_solution/common/format_errors.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { isObject } from 'lodash/fp'; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts - */ -export const formatErrors = (errors: t.Errors): string[] => { - const err = errors.map((error) => { - if (error.message != null) { - return error.message; - } else { - const keyContext = error.context - .filter( - (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' - ) - .map((entry) => entry.key) - .join(','); - - const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); - const suppliedValue = - keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; - const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; - return `Invalid value "${value}" supplied to "${suppliedValue}"`; - } - }); - - return [...new Set(err)]; -}; diff --git a/x-pack/plugins/security_solution/common/index.ts b/x-pack/plugins/security_solution/common/index.ts index 9f4af059632c4..1fec1c76430eb 100644 --- a/x-pack/plugins/security_solution/common/index.ts +++ b/x-pack/plugins/security_solution/common/index.ts @@ -4,5 +4,3 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -export * from './shared_exports'; diff --git a/x-pack/plugins/security_solution/common/machine_learning/helpers.ts b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts index 60477d3685db7..819337d8b715a 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/helpers.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Type } from '../detection_engine/schemas/common/schemas'; +import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; // Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js const enabledStates = ['started', 'opened']; diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts deleted file mode 100644 index bf740ddce9fd6..0000000000000 --- a/x-pack/plugins/security_solution/common/shared_exports.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { NonEmptyString } from './detection_engine/schemas/types/non_empty_string'; -export { DefaultArray } from './detection_engine/schemas/types/default_array'; -export { DefaultUuid } from './detection_engine/schemas/types/default_uuid'; -export { DefaultStringArray } from './detection_engine/schemas/types/default_string_array'; -export { - DefaultVersionNumber, - DefaultVersionNumberDecoded, -} from './detection_engine/schemas/types/default_version_number'; -export { exactCheck } from './exact_check'; -export { getPaths, foldLeftRight, removeExternalLinkText } from './test_utils'; -export { validate, validateEither } from './validate'; -export { formatErrors } from './format_errors'; -export { addIdToItem, removeIdFromItem } from './add_remove_id_to_item'; diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index a6bad0347e641..8f858e724394b 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -12,9 +12,6 @@ export { CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, - exceptionListItemSchema, - createExceptionListItemSchema, - listSchema, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, EXCEPTION_LIST_URL, diff --git a/x-pack/plugins/security_solution/common/test_utils.ts b/x-pack/plugins/security_solution/common/test_utils.ts deleted file mode 100644 index df9e9e12fc1d9..0000000000000 --- a/x-pack/plugins/security_solution/common/test_utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { formatErrors } from './format_errors'; - -interface Message { - errors: t.Errors; - schema: T | {}; -} - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -const onLeft = (errors: t.Errors): Message => { - return { schema: {}, errors }; -}; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -const onRight = (schema: T): Message => { - return { - schema, - errors: [], - }; -}; - -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -export const foldLeftRight = fold(onLeft, onRight); - -/** - * Convenience utility to keep the error message handling within tests to be - * very concise. - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - * @param validation The validation to get the errors from - */ -export const getPaths = (validation: t.Validation): string[] => { - return pipe( - validation, - fold( - (errors) => formatErrors(errors), - () => ['no errors'] - ) - ); -}; - -/** - * Convenience utility to remove text appended to links by EUI - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts - */ -export const removeExternalLinkText = (str: string) => - str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 5b6c9c532ba7c..7ae52a3990ff7 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -7,6 +7,7 @@ import * as runtimeTypes from 'io-ts'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; import { stringEnum, unionWithNullType } from '../../utility_types'; import { NoteResult, NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; import { @@ -19,7 +20,6 @@ import { success_count as successCount, } from '../../detection_engine/schemas/common/schemas'; import { FlowTarget } from '../../search_strategy/security_solution/network'; -import { PositiveInteger } from '../../detection_engine/schemas/types'; import { errorSchema } from '../../detection_engine/schemas/response/error_schema'; import { Direction, Maybe } from '../../search_strategy'; diff --git a/x-pack/plugins/security_solution/common/validate.test.ts b/x-pack/plugins/security_solution/common/validate.test.ts deleted file mode 100644 index e3fe5c55288a3..0000000000000 --- a/x-pack/plugins/security_solution/common/validate.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { left, right } from 'fp-ts/lib/Either'; -import * as t from 'io-ts'; - -import { validate, validateEither } from './validate'; - -describe('validate', () => { - test('it should do a validation correctly', () => { - const schema = t.exact(t.type({ a: t.number })); - const payload = { a: 1 }; - const [validated, errors] = validate(payload, schema); - - expect(validated).toEqual(payload); - expect(errors).toEqual(null); - }); - - test('it should do an in-validation correctly', () => { - const schema = t.exact(t.type({ a: t.number })); - const payload = { a: 'some other value' }; - const [validated, errors] = validate(payload, schema); - - expect(validated).toEqual(null); - expect(errors).toEqual('Invalid value "some other value" supplied to "a"'); - }); -}); - -describe('validateEither', () => { - it('returns the ORIGINAL payload as right if valid', () => { - const schema = t.exact(t.type({ a: t.number })); - const payload = { a: 1 }; - const result = validateEither(schema, payload); - - expect(result).toEqual(right(payload)); - }); - - it('returns an error string if invalid', () => { - const schema = t.exact(t.type({ a: t.number })); - const payload = { a: 'some other value' }; - const result = validateEither(schema, payload); - - expect(result).toEqual(left(new Error('Invalid value "some other value" supplied to "a"'))); - }); -}); diff --git a/x-pack/plugins/security_solution/common/validate.ts b/x-pack/plugins/security_solution/common/validate.ts deleted file mode 100644 index 1ac41ecbfb88b..0000000000000 --- a/x-pack/plugins/security_solution/common/validate.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { fold, Either, mapLeft } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fromEither, TaskEither } from 'fp-ts/lib/TaskEither'; -import * as t from 'io-ts'; -import { exactCheck } from './exact_check'; -import { formatErrors } from './format_errors'; - -export const validate = ( - obj: object, - schema: T -): [t.TypeOf | null, string | null] => { - const decoded = schema.decode(obj); - const checked = exactCheck(obj, decoded); - const left = (errors: t.Errors): [T | null, string | null] => [ - null, - formatErrors(errors).join(','), - ]; - const right = (output: T): [T | null, string | null] => [output, null]; - return pipe(checked, fold(left, right)); -}; - -export const validateNonExact = ( - obj: unknown, - schema: T -): [t.TypeOf | null, string | null] => { - const decoded = schema.decode(obj); - const left = (errors: t.Errors): [T | null, string | null] => [ - null, - formatErrors(errors).join(','), - ]; - const right = (output: T): [T | null, string | null] => [output, null]; - return pipe(decoded, fold(left, right)); -}; - -export const validateEither = ( - schema: T, - obj: A -): Either => - pipe( - obj, - (a) => schema.validate(a, t.getDefaultContext(schema.asDecoder())), - mapLeft((errors) => new Error(formatErrors(errors).join(','))) - ); - -export const validateTaskEither = ( - schema: T, - obj: A -): TaskEither => fromEither(validateEither(schema, obj)); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index c605a71c50e33..45d4137f8c5b0 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -11,7 +11,7 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; -import { ListSchema } from '../../../lists_plugin_deps'; +import { ListSchema } from '../../../shared_imports'; import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; import { DATE_NOW, VERSION, IMMUTABLE } from '../../../../../lists/common/constants.mock'; @@ -28,8 +28,8 @@ const mockKeywordList: ListSchema = { }; const mockResult = { ...getFoundListSchemaMock() }; mockResult.data = [...mockResult.data, mockKeywordList]; -jest.mock('../../../lists_plugin_deps', () => { - const originalModule = jest.requireActual('../../../lists_plugin_deps'); +jest.mock('../../../shared_imports', () => { + const originalModule = jest.requireActual('../../../shared_imports'); return { ...originalModule, diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx index 3efa8c4c2d605..37e5961c8cd7e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx @@ -9,7 +9,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { useFindLists, ListSchema } from '../../../lists_plugin_deps'; +import { useFindLists, ListSchema } from '../../../shared_imports'; import { useKibana } from '../../../common/lib/kibana'; import { filterFieldToList, getGenericComboBoxProps } from './helpers'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index bd79bb0fcc8e8..13f4e5e6fd6f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -9,7 +9,7 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import type { Type } from '@kbn/securitysolution-io-ts-list-types'; -import type { ListSchema } from '../../../lists_plugin_deps'; +import type { ListSchema } from '../../../shared_imports'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 6efbbcf64406b..3216a020c3b04 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -25,7 +25,11 @@ import { EuiComboBox, EuiComboBoxOptionOption, } from '@elastic/eui'; -import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; +import type { + ExceptionListType, + OsTypeArray, + OsType, +} from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -63,7 +67,6 @@ import { ErrorInfo, ErrorCallout } from '../error_callout'; import { AlertData, ExceptionsBuilderExceptionItem } from '../types'; import { useFetchIndex } from '../../../containers/source'; import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs'; -import { OsTypeArray, OsType } from '../../../../../../lists/common/schemas'; export interface AddExceptionModalProps { ruleName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 6c68dcf934b71..ed050574c3994 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -22,7 +22,11 @@ import { EuiCallOut, } from '@elastic/eui'; -import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; +import type { + ExceptionListType, + OsTypeArray, + OsType, +} from '@kbn/securitysolution-io-ts-list-types'; import { hasEqlSequenceQuery, isEqlRule, @@ -54,7 +58,6 @@ import { import { Loader } from '../../loader'; import { ErrorInfo, ErrorCallout } from '../error_callout'; import { useGetInstalledJob } from '../../ml/hooks/use_get_jobs'; -import { OsTypeArray, OsType } from '../../../../../../lists/common/schemas'; interface EditExceptionModalProps { ruleName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx index 2317ec353a3bf..ba8b5b522f0a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/error_callout.tsx @@ -16,8 +16,8 @@ import { EuiSpacer, } from '@elastic/eui'; +import { List } from '@kbn/securitysolution-io-ts-list-types'; import { HttpSetup } from '../../../../../../../src/core/public'; -import { List } from '../../../../common/detection_engine/schemas/types/lists'; import { Rule } from '../../../detections/containers/detection_engine/rules/types'; import * as i18n from './translations'; import { useDissasociateExceptionList } from '../../../detections/containers/detection_engine/rules/use_dissasociate_exception_list'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 49cdd7103c48b..bbf83a58e3679 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -17,11 +17,14 @@ import type { ListOperatorTypeEnum as OperatorTypeEnum, ListOperatorEnum as OperatorEnum, } from '@kbn/securitysolution-io-ts-list-types'; -import { Ecs } from '../../../../common/ecs'; -import { CodeSignature } from '../../../../common/ecs/file'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { OperatorOption } from '../autocomplete/types'; -import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../lists_plugin_deps'; +import type { Ecs } from '../../../../common/ecs'; +import type { CodeSignature } from '../../../../common/ecs/file'; +import type { IFieldType } from '../../../../../../../src/plugins/data/common'; +import type { OperatorOption } from '../autocomplete/types'; +import type { + ExceptionListItemSchema, + CreateExceptionListItemSchema, +} from '../../../shared_imports'; export interface FormattedEntry { fieldName: string; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx index f609acf9c6c63..c8a624b009c43 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx @@ -16,11 +16,11 @@ import * as buildFilterHelpers from '../../../detections/components/alerts_table import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getUpdateExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/request/update_exception_list_item_schema.mock'; -import { +import type { ExceptionListItemSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, -} from '../../../lists_plugin_deps'; +} from '../../../shared_imports'; import { useAddOrUpdateException, UseAddOrUpdateExceptionProps, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx index 614f5301c82e2..6aa68373d5eb5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx @@ -13,7 +13,7 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema, useApi, -} from '../../../lists_plugin_deps'; +} from '../../../shared_imports'; import { updateAlertStatus } from '../../../detections/containers/detection_engine/alerts/api'; import { getUpdateAlertsQuery } from '../../../detections/components/alerts_table/actions'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index 17237f4f94c61..d38d920eee188 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -12,8 +12,7 @@ import * as rulesApi from '../../../detections/containers/detection_engine/rules import * as listsApi from '../../../../../lists/public/exceptions/api'; import { getExceptionListSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { savedRuleMock } from '../../../detections/containers/detection_engine/rules/mock'; -import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; -import { ListArray } from '../../../../common/detection_engine/schemas/types'; +import type { ExceptionListType, ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { useFetchOrCreateRuleExceptionList, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 9b970e2caab15..98c207f47a45b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -6,10 +6,10 @@ */ import { useEffect, useState } from 'react'; +import { List, ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { HttpStart } from '../../../../../../../src/core/public'; import { Rule } from '../../../detections/containers/detection_engine/rules/types'; -import { List, ListArray } from '../../../../common/detection_engine/schemas/types'; import { fetchRuleById, patchRule, @@ -18,7 +18,7 @@ import { fetchExceptionListById, addExceptionList, addEndpointExceptionList, -} from '../../../lists_plugin_deps'; +} from '../../../shared_imports'; import { ExceptionListSchema, CreateExceptionListSchema, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx index 86eca688ef082..ff242506927f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx @@ -16,10 +16,10 @@ import { import React, { useMemo, Fragment } from 'react'; import styled, { css } from 'styled-components'; -import { DescriptionListItem } from '../../types'; +import type { DescriptionListItem } from '../../types'; import { getDescriptionListContent } from '../helpers'; import * as i18n from '../../translations'; -import { ExceptionListItemSchema } from '../../../../../../public/lists_plugin_deps'; +import type { ExceptionListItemSchema } from '../../../../../../public/shared_imports'; const MyExceptionDetails = styled(EuiFlexItem)` ${({ theme }) => css` diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx index caff2be29ab2d..7909366e7a32e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx @@ -20,8 +20,8 @@ import { ExceptionDetails } from './exception_details'; import { ExceptionEntries } from './exception_entries'; import { getFormattedComments } from '../../helpers'; import { getFormattedEntries } from '../helpers'; -import { FormattedEntry, ExceptionListItemIdentifiers } from '../../types'; -import { ExceptionListItemSchema } from '../../../../../../public/lists_plugin_deps'; +import type { FormattedEntry, ExceptionListItemIdentifiers } from '../../types'; +import type { ExceptionListItemSchema } from '../../../../../../public/shared_imports'; const MyFlexItem = styled(EuiFlexItem)` &.comments--show { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index 825bbf5f6f050..1e4cd306c4661 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -12,8 +12,8 @@ import styled from 'styled-components'; import * as i18n from '../translations'; import { ExceptionItem } from './exception_item'; import { AndOrBadge } from '../../and_or_badge'; -import { ExceptionListItemSchema } from '../../../../../public/lists_plugin_deps'; -import { ExceptionListItemIdentifiers } from '../types'; +import type { ExceptionListItemSchema } from '../../../../../public/shared_imports'; +import type { ExceptionListItemIdentifiers } from '../types'; const MyFlexItem = styled(EuiFlexItem)` margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index abd45cf2945cb..936423d0c362b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -12,10 +12,10 @@ import { ExceptionListItemSchema, getEntryValue, getExceptionOperatorSelect, -} from '../../../../lists_plugin_deps'; +} from '../../../../shared_imports'; import { formatOperatingSystems } from '../helpers'; -import { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types'; +import type { FormattedEntry, BuilderEntry, DescriptionListItem } from '../types'; import * as i18n from '../translations'; /** diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index 971b3fda47191..45db39d6a03ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -11,7 +11,7 @@ import { ThemeProvider } from 'styled-components'; import { ExceptionsViewer } from './'; import { useKibana } from '../../../../common/lib/kibana'; -import { useExceptionListItems, useApi } from '../../../../../public/lists_plugin_deps'; +import { useExceptionListItems, useApi } from '../../../../../public/shared_imports'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; @@ -31,7 +31,7 @@ const mockTheme = getMockTheme({ }); jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../../public/lists_plugin_deps'); +jest.mock('../../../../../public/shared_imports'); describe('ExceptionsViewer', () => { const ruleName = 'test rule'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index da7607f40ab72..8055e771a1647 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -24,7 +24,7 @@ import { ExceptionListItemSchema, UseExceptionListItemsSuccess, useApi, -} from '../../../../../public/lists_plugin_deps'; +} from '../../../../../public/shared_imports'; import { ExceptionsViewerPagination } from './exceptions_pagination'; import { ExceptionsViewerUtility } from './exceptions_utility'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts index bf8e454e9971f..4908a88b72526 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts @@ -6,17 +6,17 @@ */ import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; -import { +import type { FilterOptions, ExceptionsPagination, ExceptionListItemIdentifiers, Filter, } from '../types'; -import { +import type { ExceptionListItemSchema, ExceptionListIdentifiers, Pagination, -} from '../../../../../public/lists_plugin_deps'; +} from '../../../../../public/shared_imports'; export type ViewerModalName = 'addModal' | 'editModal' | null; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index cd19eb5a27d7b..965167f2c945e 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow, ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { mountWithIntl } from '@kbn/test/jest'; import { encodeIpv6 } from '../../lib/helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx index 490392c237e19..da3785648de62 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { MarkdownRenderer } from './renderer'; describe('Markdown', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_delete_button.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_delete_button.test.tsx index 9c30564699311..0e8edbf047528 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/entry_delete_button.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/entry_delete_button.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThreatMappingEntries } from '../../../../common/detection_engine/schemas/types'; +import { ThreatMappingEntries } from '@kbn/securitysolution-io-ts-alerting-types'; import { EntryDeleteButtonComponent } from './entry_delete_button'; diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx index 1d576dc6da32c..cf9407a6bf463 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.test.tsx @@ -20,7 +20,7 @@ import { getFormattedEntry, getUpdatedEntriesOnDelete, } from './helpers'; -import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types'; +import { ThreatMapEntry } from '@kbn/securitysolution-io-ts-alerting-types'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('123'), diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx index ef3e9280e6e6b..232fac4ad8433 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx @@ -6,15 +6,11 @@ */ import uuid from 'uuid'; -import { - ThreatMap, - threatMap, - ThreatMapping, -} from '../../../../common/detection_engine/schemas/types'; +import { addIdToItem } from '@kbn/securitysolution-utils'; +import { ThreatMap, threatMap, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { IndexPattern, IFieldType } from '../../../../../../../src/plugins/data/common'; import { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types'; -import { addIdToItem } from '../../../../common/add_remove_id_to_item'; /** * Formats the entry into one that is easily usable for the UI. diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx index 8bdbe7b1a3db8..3e4f0283145e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/index.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useReducer } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { ThreatMapping } from '../../../../common/detection_engine/schemas/types'; +import { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { ListItemComponent } from './list_item'; import { IndexPattern } from '../../../../../../../src/plugins/data/common'; import { AndOrBadge } from '../and_or_badge'; diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts index 8d1ae27ec6f7e..7c19b61efff02 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/reducer.test.ts @@ -8,7 +8,7 @@ import { ThreatMapEntries } from './types'; import { State, reducer } from './reducer'; import { getDefaultEmptyEntry } from './helpers'; -import { ThreatMapEntry } from '../../../../common/detection_engine/schemas/types'; +import { ThreatMapEntry } from '@kbn/securitysolution-io-ts-alerting-types'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('123'), diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts b/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts index 0ea7727f2282a..3d4fa19ac5794 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ThreatMap, ThreatMapEntry } from '../../../../common/detection_engine/schemas/types'; +import { ThreatMap, ThreatMapEntry } from '@kbn/securitysolution-io-ts-alerting-types'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; export interface FormattedEntry { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 7b9c3f35ef57b..17162a2206fc3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -22,12 +22,12 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations'; -import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types'; import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; -import { Threshold, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 9c40853794743..ab84f1a65cc4b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -10,7 +10,7 @@ import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; -import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types'; +import { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { IIndexPattern, Filter, @@ -39,7 +39,6 @@ import { import { buildMlJobsDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; -import { Threats, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { THREAT_QUERY_LABEL } from './translations'; import { filterEmptyThreats } from '../../../pages/detection_engine/rules/create/helpers'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts index 0e685da078226..dc5e7fea8e771 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts @@ -6,7 +6,7 @@ */ import { ReactNode } from 'react'; -import { Threats } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Threats } from '@kbn/securitysolution-io-ts-alerting-types'; import { IIndexPattern, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts index b867d3d76cc70..6f5cb37d9f91f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ThreatTechnique } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { ThreatTechnique } from '@kbn/securitysolution-io-ts-alerting-types'; import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques'; /** diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx index c39efa1409bc3..25eb7a65e0d1b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/index.tsx @@ -11,7 +11,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { isEqual } from 'lodash'; -import { Threat, Threats } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Threat, Threats } from '@kbn/securitysolution-io-ts-alerting-types'; import { tacticsOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import { FieldHook } from '../../../../shared_imports'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx index d283c19bd13da..7504a5d706f18 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx @@ -17,10 +17,7 @@ import { camelCase } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { - Threats, - ThreatSubtechnique, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Threats, ThreatSubtechnique } from '@kbn/securitysolution-io-ts-alerting-types'; import { subtechniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import { FieldHook } from '../../../../shared_imports'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx index 7f698740f5d3b..5a0fde1a739a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/technique_fields.tsx @@ -17,10 +17,7 @@ import { kebabCase, camelCase } from 'lodash/fp'; import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; -import { - Threats, - ThreatTechnique, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Threats, ThreatTechnique } from '@kbn/securitysolution-io-ts-alerting-types'; import { techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; import { FieldHook } from '../../../../shared_imports'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts index bc4888acc90ff..1d3135b8cb34a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts @@ -9,10 +9,10 @@ import { Position, ScaleType } from '@elastic/charts'; import { EuiSelectOption } from '@elastic/eui'; import { Unit } from '@elastic/datemath'; +import { Type, Language } from '@kbn/securitysolution-io-ts-alerting-types'; import * as i18n from './translations'; import { histogramDateTimeFormatter } from '../../../../common/components/utils'; import { ChartSeriesConfigs } from '../../../../common/components/charts/common'; -import { Type, Language } from '../../../../../common/detection_engine/schemas/common/schemas'; import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; import { FieldValueQueryBar } from '../query_bar'; import { ESQuery } from '../../../../../common/typed_json'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx index 70d292660388d..6342d468f5962 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx @@ -20,11 +20,11 @@ import { } from '@elastic/eui'; import { debounce } from 'lodash/fp'; +import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import * as i18n from './translations'; import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; import { FieldValueQueryBar } from '../query_bar'; -import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { PreviewEqlQueryHistogram } from './eql_histogram'; import { useEqlPreview } from '../../../../common/hooks/eql/'; import { PreviewThresholdQueryHistogram } from './threshold_histogram'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts index 2dff858d61c79..afddca63afcc6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts @@ -8,10 +8,10 @@ import { Unit } from '@elastic/datemath'; import { EuiSelectOption } from '@elastic/eui'; +import { Type, Language } from '@kbn/securitysolution-io-ts-alerting-types'; import * as i18n from './translations'; import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; import { ESQuery } from '../../../../../common/typed_json'; -import { Language, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { FieldValueQueryBar } from '../query_bar'; import { formatDate } from '../../../../common/components/super_date_picker'; import { getInfoFromQueryBar, getTimeframeOptions } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index 79c54a1cc0225..c02f7992a9b92 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -19,13 +19,13 @@ import { import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; +import { RiskScoreMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { RiskScoreMapping } from '../../../../../common/detection_engine/schemas/common/schemas'; const NestedContent = styled.div` margin-left: 24px; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index 5650c2c55488e..712166df2b539 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic/eui'; -import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index d83ad8f4f4d26..8b8c9441e7eae 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -19,6 +19,11 @@ import { import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { + Severity, + SeverityMapping, + SeverityMappingItem, +} from '@kbn/securitysolution-io-ts-alerting-types'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; @@ -29,11 +34,6 @@ import { } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { FieldComponent } from '../../../../common/components/autocomplete/field'; import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; -import { - Severity, - SeverityMapping, - SeverityMappingItem, -} from '../../../../../common/detection_engine/schemas/common/schemas'; const NestedContent = styled.div` margin-left: 24px; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx index 8a01d5b072fb6..264e499d9cf86 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx @@ -10,7 +10,7 @@ import { EuiHealth } from '@elastic/eui'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; -import { Severity } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import * as I18n from './translations'; export interface SeverityOptionItem { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts index 7eb91e259a72f..891e7f47c1b7e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/transforms.ts @@ -6,7 +6,7 @@ */ import { flow } from 'fp-ts/lib/function'; -import { addIdToItem, removeIdFromItem } from '../../../../../common/add_remove_id_to_item'; +import { addIdToItem, removeIdFromItem } from '@kbn/securitysolution-utils'; import { CreateRulesSchema, UpdateRulesSchema, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 2c3d6484aebdd..338124ad5ce65 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -7,28 +7,28 @@ import * as t from 'io-ts'; +import { listArray } from '@kbn/securitysolution-io-ts-list-types'; +import { + risk_score_mapping, + threat_query, + threat_index, + threat_indicator_path, + threat_mapping, + threat_language, + threat_filters, + threats, + type, + severity_mapping, +} from '@kbn/securitysolution-io-ts-alerting-types'; import { SortOrder, author, building_block_type, license, - risk_score_mapping, rule_name_override, - severity_mapping, timestamp_override, threshold, - type, - threats, } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { - listArray, - threat_query, - threat_index, - threat_indicator_path, - threat_mapping, - threat_language, - threat_filters, -} from '../../../../../common/detection_engine/schemas/types'; import { CreateRulesSchema, PatchRulesSchema, @@ -38,6 +38,7 @@ import { /** * Params is an "record", since it is a type of AlertActionParams which is action templates. * @see x-pack/plugins/alerting/common/alert.ts + * @deprecated Use the one from @kbn/security-io-ts-alerting-types */ export const action = t.exact( t.type({ diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx index 1fc6ff607c47f..8807f02774e0e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.tsx @@ -7,8 +7,8 @@ import { useEffect, useState, useRef } from 'react'; +import { List } from '@kbn/securitysolution-io-ts-list-types'; import { HttpStart } from '../../../../../../../../src/core/public'; -import { List } from '../../../../../common/detection_engine/schemas/types/lists'; import { patchRule } from './api'; type Func = (lists: List[]) => void; diff --git a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts index 70c340096f462..a7de7494e1116 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/valid_threat_mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Threats } from '../../../common/detection_engine/schemas/common/schemas'; +import { Threats } from '@kbn/securitysolution-io-ts-alerting-types'; import { mockThreatData } from './mitre_tactics_techniques'; const { tactic, technique, subtechnique } = mockThreatData; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index 98d3dadc7bbcb..f018bc148d626 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { List } from '../../../../../../common/detection_engine/schemas/types'; +import { List } from '@kbn/securitysolution-io-ts-list-types'; import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { Rule } from '../../../../containers/detection_engine/rules'; import { @@ -40,7 +40,7 @@ import { mockActionsStepRule, } from '../all/__mocks__/mock'; import { getThreatMock } from '../../../../../../common/detection_engine/schemas/types/threat.mock'; -import { Threat, Threats } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { Threat, Threats } from '@kbn/securitysolution-io-ts-alerting-types'; describe('helpers', () => { describe('getTimeTypeValue', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 29b63721513d4..5259b95a09ae6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -9,20 +9,22 @@ import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; import deepmerge from 'deepmerge'; -import type { ExceptionListType, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; -import { assertUnreachable } from '../../../../../../common/utility_types'; -import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; -import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; -import { Rule } from '../../../../containers/detection_engine/rules'; +import type { + ExceptionListType, + NamespaceType, + List, +} from '@kbn/securitysolution-io-ts-list-types'; import { Threats, ThreatSubtechnique, ThreatTechnique, Type, -} from '../../../../../../common/detection_engine/schemas/common/schemas'; - +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; +import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, DefineStepRule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 03688264bcf46..25785f6bbcb2d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -12,6 +12,12 @@ import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { EuiFlexItem } from '@elastic/eui'; +import { + Threats, + Type, + SeverityMapping, + Severity, +} from '@kbn/securitysolution-io-ts-alerting-types'; import { ActionVariables } from '../../../../../../triggers_actions_ui/public'; import { normalizeThresholdField } from '../../../../../common/detection_engine/utils'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; @@ -27,12 +33,6 @@ import { ScheduleStepRule, ActionsStepRule, } from './types'; -import { - SeverityMapping, - Type, - Severity, - Threats, -} from '../../../../../common/detection_engine/schemas/common/schemas'; import { severityOptions } from '../../../components/rules/step_about_rule/data'; export interface GetStepsData { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 58994c5a5f556..cf82e7cb7944e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -5,6 +5,16 @@ * 2.0. */ +import { List } from '@kbn/securitysolution-io-ts-list-types'; +import { + RiskScoreMapping, + ThreatIndex, + ThreatMapping, + Threats, + Type, + SeverityMapping, + Severity, +} from '@kbn/securitysolution-io-ts-alerting-types'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { AlertAction } from '../../../../../../alerting/common'; import { Filter } from '../../../../../../../../src/plugins/data/common'; @@ -15,20 +25,10 @@ import { Author, BuildingBlockType, License, - RiskScoreMapping, RuleNameOverride, - SeverityMapping, SortOrder, TimestampOverride, - Type, - Severity, - Threats, } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { - List, - ThreatIndex, - ThreatMapping, -} from '../../../../../common/detection_engine/schemas/types'; export interface EuiBasicTableSortTypes { field: string; diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts deleted file mode 100644 index 498995b538429..0000000000000 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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. - */ - -// DEPRECATED: Do not add exports to this file; please import from shared_imports instead - -export * from './shared_imports'; diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index 9c7a0833b24bb..ef1039bfc92e3 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock/test_providers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index 6be0382e54cde..01065ad5bf15f 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -10,7 +10,7 @@ import { shallow } from 'enzyme'; import { get } from 'lodash/fp'; import React from 'react'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index a13621cfe3a9c..f767e793c8f21 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -8,7 +8,7 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index 63e27bd8fa9c3..7a38c873450ca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index bb2061f744289..802dd74c1892b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 5e6d804b01fde..e2c8b8854504a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -9,7 +9,7 @@ import { get } from 'lodash/fp'; import React from 'react'; import { shallow } from 'enzyme'; -import { removeExternalLinkText } from '../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index b92a4381d837b..56dbc99d47c66 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; -import { removeExternalLinkText } from '../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import '../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../common/ecs'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index bd8c2d10ccfaf..5960f43174b98 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -8,7 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { removeExternalLinkText } from '../../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData } from '../../../../../../common/mock'; import '../../../../../../common/mock/match_media'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 469c8f6c3ec4c..098d6775cfaa4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { removeExternalLinkText } from '../../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index b7857e6bf4585..8e8ce9cb2f988 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { removeExternalLinkText } from '../../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 0f27dccd1aff0..04150163fb4d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { removeExternalLinkText } from '../../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import '../../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 2d20da0c5e6dc..749e450b36ae4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { removeExternalLinkText } from '../../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 4fc2f90c87da4..61155331b1a4b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { removeExternalLinkText } from '../../../../../../../common/test_utils'; +import { removeExternalLinkText } from '@kbn/securitysolution-io-ts-utils'; import '../../../../../../common/mock/match_media'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index f3bc195b5a896..e73e3eb5c56f8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -8,8 +8,8 @@ import { createHash } from 'crypto'; import { deflate } from 'zlib'; import { Entry, EntryNested } from '@kbn/securitysolution-io-ts-list-types'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; -import { validate } from '../../../../common/validate'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports'; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts index 7e1accac37cf0..43d4fc49161bb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -7,7 +7,7 @@ import { flatMap, isEqual } from 'lodash'; import semver from 'semver'; -import { validate } from '../../../../common'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { InternalArtifactSchema, InternalManifestSchema, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index e4704523a16c3..897ffe4ee48cd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -13,11 +13,11 @@ import type { EntryMatchWildcard, EntryNested, NestedEntriesArray, + OsType, } from '@kbn/securitysolution-io-ts-list-types'; import type { ExceptionListItemSchema } from '../../../../../lists/common'; -import type { OsType } from '../../../../../lists/common/schemas'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; import type { CreateExceptionListItemOptions, diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts index 1b1370472f633..3a37bfbe9320c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { operator } from '../../../../../lists/common/schemas'; +import { listOperator as operator } from '@kbn/securitysolution-io-ts-list-types'; export const translatedEntryMatchAnyMatcher = t.keyof({ exact_cased_any: null, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts index e2f6118f4f0a7..d7b05ffa5592e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts @@ -10,11 +10,11 @@ import { SavedObjectsClientContract, SavedObjectsUpdateResponse, } from 'src/core/server'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { manifestSchemaVersion, ManifestSchemaVersion, } from '../../../../common/endpoint/schema/common'; -import { validate } from '../../../../common/validate'; import { ManifestConstants } from '../../lib/artifacts'; import { InternalManifestSchema, InternalManifestCreateSchema } from '../../schemas/artifacts'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/errors/bad_request_error.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/errors/bad_request_error.ts deleted file mode 100644 index c68e7845953f6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/errors/bad_request_error.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export class BadRequestError extends Error {} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts deleted file mode 100644 index 02f8f3f7b36ab..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -// See the reference(s) below on explanations about why -000001 was chosen and -// why the is_write_index is true as well as the bootstrapping step which is needed. -// Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const createBootstrapIndex = async ( - esClient: ElasticsearchClient, - index: string -): Promise => { - return ( - await esClient.transport.request({ - path: `/${index}-000001`, - method: 'PUT', - body: { - aliases: { - [index]: { - is_write_index: true, - }, - }, - }, - }) - ).body; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts deleted file mode 100644 index d76290921fac8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const deleteAllIndex = async ( - esClient: ElasticsearchClient, - pattern: string, - maxAttempts = 5 -): Promise => { - for (let attempt = 1; ; attempt++) { - if (attempt > maxAttempts) { - throw new Error( - `Failed to delete indexes with pattern [${pattern}] after ${maxAttempts} attempts` - ); - } - - // resolve pattern to concrete index names - const { body: resp } = await esClient.indices.getAlias( - { - index: pattern, - }, - { ignore: [404] } - ); - - // @ts-expect-error status doesn't exist on response - if (resp.status === 404) { - return true; - } - - const indices = Object.keys(resp) as string[]; - - // if no indexes exits then we're done with this pattern - if (!indices.length) { - return true; - } - - // delete the concrete indexes we found and try again until this pattern resolves to no indexes - await esClient.indices.delete({ - index: indices, - ignore_unavailable: true, - }); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts deleted file mode 100644 index 924970d304c88..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const deletePolicy = async ( - esClient: ElasticsearchClient, - policy: string -): Promise => { - return ( - await esClient.transport.request({ - path: `/_ilm/policy/${policy}`, - method: 'DELETE', - }) - ).body; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts deleted file mode 100644 index 5466fd03f534c..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const deleteTemplate = async ( - esClient: ElasticsearchClient, - name: string -): Promise => { - return ( - await esClient.indices.deleteTemplate({ - name, - }) - ).body; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts deleted file mode 100644 index abe587ec825c0..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getIndexExists } from './get_index_exists'; - -class StatusCode extends Error { - status: number = -1; - constructor(status: number, message: string) { - super(message); - this.status = status; - } -} - -describe('get_index_exists', () => { - test('it should return a true if you have _shards', async () => { - const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; - esClient.search.mockReturnValue( - // @ts-expect-error not full interface - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) - ); - const indexExists = await getIndexExists(esClient, 'some-index'); - expect(indexExists).toEqual(true); - }); - - test('it should return a false if you do NOT have _shards', async () => { - const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; - esClient.search.mockReturnValue( - // @ts-expect-error not full interface - elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) - ); - const indexExists = await getIndexExists(esClient, 'some-index'); - expect(indexExists).toEqual(false); - }); - - test('it should return a false if it encounters a 404', async () => { - const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; - esClient.search.mockReturnValue( - elasticsearchClientMock.createErrorTransportRequestPromise({ - body: new StatusCode(404, 'I am a 404 error'), - }) - ); - const indexExists = await getIndexExists(esClient, 'some-index'); - expect(indexExists).toEqual(false); - }); - - test('it should reject if it encounters a non 404', async () => { - const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; - esClient.search.mockReturnValue( - elasticsearchClientMock.createErrorTransportRequestPromise( - new StatusCode(500, 'I am a 500 error') - ) - ); - await expect(getIndexExists(esClient, 'some-index')).rejects.toThrow('I am a 500 error'); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts deleted file mode 100644 index 7ca7f9818ba0b..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const getIndexExists = async ( - esClient: ElasticsearchClient, - index: string -): Promise => { - try { - const { body: response } = await esClient.search({ - index, - size: 0, - allow_no_indices: true, - body: { - terminate_after: 1, - }, - }); - return response._shards.total > 0; - } catch (err) { - if (err.body?.status === 404) { - return false; - } else { - throw err.body ? err.body : err; - } - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts deleted file mode 100644 index 6ebdac0d244cb..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const getPolicyExists = async ( - esClient: ElasticsearchClient, - policy: string -): Promise => { - try { - await esClient.transport.request({ - path: `/_ilm/policy/${policy}`, - method: 'GET', - }); - // Return true that there exists a policy which is not 404 or some error - // Since there is not a policy exists API, this is how we create one by calling - // into the API to get it if it exists or rely on it to throw a 404 - return true; - } catch (err) { - if (err.statusCode === 404) { - return false; - } else { - throw err; - } - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts deleted file mode 100644 index af5f874a05688..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const getTemplateExists = async ( - esClient: ElasticsearchClient, - template: string -): Promise => { - return ( - await esClient.indices.existsTemplate({ - name: template, - }) - ).body; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts deleted file mode 100644 index 113b9d368e0d9..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const setPolicy = async ( - esClient: ElasticsearchClient, - policy: string, - body: Record -): Promise => { - return ( - await esClient.transport.request({ - path: `/_ilm/policy/${policy}`, - method: 'PUT', - body, - }) - ).body; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts deleted file mode 100644 index 288377c306325..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { ElasticsearchClient } from 'kibana/server'; - -/** - * @deprecated Use the one from kbn-securitysolution-es-utils - */ -export const setTemplate = async ( - esClient: ElasticsearchClient, - name: string, - body: Record -): Promise => { - return ( - await esClient.indices.putTemplate({ - name, - body, - }) - ).body; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts index 9c1154f19e8dc..3aee9db31bf1b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts @@ -9,7 +9,7 @@ import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsClientContract } from 'src/core/server'; -import { validateTaskEither } from '../../../../common/validate'; +import { validateTaskEither } from '@kbn/securitysolution-io-ts-utils'; import { toError, toPromise } from '../../../../common/fp_utils'; import { signalsMigrationSOClient } from './saved_objects_client'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.test.ts index bdc22f2ff20ce..46de2eb133bac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.test.ts @@ -6,13 +6,13 @@ */ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; -import { getIndexCount } from '../index/get_index_count'; +import { getIndexCount } from '@kbn/securitysolution-es-utils'; import { updateMigrationSavedObject } from './update_migration_saved_object'; import { getSignalsMigrationSavedObjectMock } from './saved_objects_schema.mock'; import { finalizeMigration } from './finalize_migration'; jest.mock('./update_migration_saved_object'); -jest.mock('../index/get_index_count'); +jest.mock('@kbn/securitysolution-es-utils'); describe('finalizeMigration', () => { let esClient: ReturnType; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.ts index 7a52470e58051..ca8ae01ee2d1f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/finalize_migration.ts @@ -6,7 +6,7 @@ */ import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import { getIndexCount } from '../index/get_index_count'; +import { getIndexCount } from '@kbn/securitysolution-es-utils'; import { isMigrationPending } from './helpers'; import { applyMigrationCleanupPolicy } from './migration_cleanup'; import { replaceSignalsIndexAlias } from './replace_signals_index_alias'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/find_migration_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/find_migration_saved_objects.ts index 2a3e25ee57aa2..2cc73c36500f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/find_migration_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/find_migration_saved_objects.ts @@ -9,9 +9,9 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; +import { validateEither } from '@kbn/securitysolution-io-ts-utils'; import { signalsMigrationSOClient } from './saved_objects_client'; import { SignalsMigrationSO, signalsMigrationSOs } from './saved_objects_schema'; -import { validateEither } from '../../../../common/validate'; export const findMigrationSavedObjects = async ({ options, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_migration_saved_objects_by_id.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_migration_saved_objects_by_id.ts index 7d66dca2ef192..66bbc01ef0d68 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_migration_saved_objects_by_id.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/get_migration_saved_objects_by_id.ts @@ -9,7 +9,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsClientContract } from 'src/core/server'; -import { validateEither } from '../../../../common/validate'; +import { validateEither } from '@kbn/securitysolution-io-ts-utils'; import { signalsMigrationSOClient } from './saved_objects_client'; import { SignalsMigrationSO, signalsMigrationSOs } from './saved_objects_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/saved_objects_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/saved_objects_schema.ts index 3ce798ec5a5c1..e81583b825e39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/saved_objects_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/saved_objects_schema.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; -import { IsoDateString, PositiveInteger } from '../../../../common/detection_engine/schemas/types'; +import { IsoDateString, PositiveInteger } from '@kbn/securitysolution-io-ts-types'; import { unionWithNullType } from '../../../../common/utility_types'; const status = t.keyof({ success: null, failure: null, pending: null }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts index 05d7d3e50dca7..5bd8b47c3b74a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts @@ -9,7 +9,7 @@ import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsClientContract, SavedObjectsUpdateResponse } from 'src/core/server'; -import { validateTaskEither } from '../../../../common/validate'; +import { validateTaskEither } from '@kbn/securitysolution-io-ts-utils'; import { toError, toPromise } from '../../../../common/fp_utils'; import { signalsMigrationSOClient } from './saved_objects_client'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index cd1b77862af04..d98cd7cea0f2b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -5,19 +5,22 @@ * 2.0. */ +import { + transformError, + getIndexExists, + getPolicyExists, + setPolicy, + setTemplate, + createBootstrapIndex, +} from '@kbn/securitysolution-es-utils'; import type { AppClient, SecuritySolutionPluginRouter, SecuritySolutionRequestHandlerContext, } from '../../../../types'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; -import { getIndexExists } from '../../index/get_index_exists'; -import { getPolicyExists } from '../../index/get_policy_exists'; -import { setPolicy } from '../../index/set_policy'; -import { setTemplate } from '../../index/set_template'; +import { buildSiemResponse } from '../utils'; import { getSignalsTemplate, SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; -import { createBootstrapIndex } from '../../index/create_bootstrap_index'; import { ensureMigrationCleanupPolicy } from '../../migrations/migration_cleanup'; import signalsPolicy from './signals_policy.json'; import { templateNeedsUpdate } from './check_template_version'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts index 1a4f00a570424..5260c9487de8a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -5,15 +5,18 @@ * 2.0. */ +import { + transformError, + getIndexExists, + getPolicyExists, + deletePolicy, + getTemplateExists, + deleteAllIndex, + deleteTemplate, +} from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; -import { getIndexExists } from '../../index/get_index_exists'; -import { getPolicyExists } from '../../index/get_policy_exists'; -import { deletePolicy } from '../../index/delete_policy'; -import { getTemplateExists } from '../../index/get_template_exists'; -import { deleteAllIndex } from '../../index/delete_all_index'; -import { deleteTemplate } from '../../index/delete_template'; +import { buildSiemResponse } from '../utils'; /** * Deletes all of the indexes, template, ilm policies, and aliases. You can check diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts index b333ef999a6ae..b6f711fc319fb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts @@ -6,8 +6,8 @@ */ import { get } from 'lodash'; +import { readIndex } from '@kbn/securitysolution-es-utils'; import { ElasticsearchClient } from '../../../../../../../../src/core/server'; -import { readIndex } from '../../index/read_index'; export const getIndexVersion = async ( esClient: ElasticsearchClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index 01d07f68aa489..6af4397a4193a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; -import { getIndexExists } from '../../index/get_index_exists'; + +import { buildSiemResponse } from '../utils'; import { SIGNALS_TEMPLATE_VERSION } from './get_signals_template'; import { getIndexVersion } from './get_index_version'; import { isOutdated } from '../../migrations/helpers'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index f006d9250d369..04fd2aeaebb2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -7,9 +7,11 @@ import { merge } from 'lodash/fp'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; -import { buildSiemResponse, transformError } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { readPrivileges } from '../../privileges/read_privileges'; export const readPrivilegesRoute = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 4f9bd7d0cfd6c..03d357ab10bb9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -6,13 +6,14 @@ */ import moment from 'moment'; +import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { AppClient, SecuritySolutionPluginRouter, SecuritySolutionRequestHandlerContext, } from '../../../../types'; -import { validate } from '../../../../../common/validate'; import { PrePackagedRulesAndTimelinesSchema, prePackagedRulesAndTimelinesSchema, @@ -24,7 +25,6 @@ import { ConfigType } from '../../../../config'; import { SetupPlugins } from '../../../../plugin'; import { buildFrameworkRequest } from '../../../timeline/utils/common'; -import { getIndexExists } from '../../index/get_index_exists'; import { getLatestPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { installPrepackagedRules } from '../../rules/install_prepacked_rules'; import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; @@ -33,7 +33,7 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; import { ruleAssetSavedObjectsClientFactory } from '../../rules/rule_asset_saved_objects_client'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; import { AlertsClient } from '../../../../../../alerting/server'; import { FrameworkRequest } from '../../../framework'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index cd0e1883e78f5..500c74e47ea7d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { validate } from '../../../../../common/validate'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { getIndexExists } from '@kbn/securitysolution-es-utils'; import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents'; import { createRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; import { rulesBulkSchema } from '../../../../../common/detection_engine/schemas/response/rules_bulk_schema'; @@ -17,7 +18,6 @@ import { throwHttpError } from '../../../machine_learning/validation'; import { readRules } from '../../rules/read_rules'; import { getDuplicates } from './utils'; import { transformValidateBulkError } from './validate'; -import { getIndexExists } from '../../index/get_index_exists'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 1e34bbbbe4749..9b7e7bb42f423 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; @@ -12,8 +13,8 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { readRules } from '../../rules/read_rules'; -import { getIndexExists } from '../../index/get_index_exists'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { createRulesSchema } from '../../../../../common/detection_engine/schemas/request'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 9739eb7ba9e00..1e7ba976d6915 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { validate } from '../../../../../common/validate'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 3bd7c7f8730b3..76fb9ac0c77e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { queryRulesSchema, @@ -15,7 +16,8 @@ import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; import { getIdError, transform } from './utils'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index 8fe20e4db612c..cb1c1feba5295 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { exportRulesQuerySchema, ExportRulesQuerySchemaDecoded, @@ -18,7 +19,7 @@ import { ConfigType } from '../../../../config'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; export const exportRulesRoute = (router: SecuritySolutionPluginRouter, config: ConfigType) => { router.post( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts index 005266f5c178f..ccf0a59e87c74 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { findRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/find_rules_type_dependents'; import { findRulesSchema, @@ -13,7 +14,8 @@ import { import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index cb436b89af01f..bd6e8fc9e7aad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -5,11 +5,13 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { RuleStatusResponse } from '../../rules/types'; -import { transformError, buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; +import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; + import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; import { findRulesStatusesSchema, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 33f9746fe9245..cd02cc72ba40c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -5,14 +5,16 @@ * 2.0. */ -import { validate } from '../../../../../common/validate'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { PrePackagedRulesAndTimelinesStatusSchema, prePackagedRulesAndTimelinesStatusSchema, } from '../../../../../common/detection_engine/schemas/response/prepackaged_rules_status_schema'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index b37cc41f1439e..8e322405280d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -10,7 +10,8 @@ import { extname } from 'path'; import { schema } from '@kbn/config-schema'; import { createPromiseFromStreams } from '@kbn/utils'; -import { validate } from '../../../../../common/validate'; +import { transformError, getIndexExists } from '@kbn/securitysolution-es-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { importRulesQuerySchema, ImportRulesQuerySchemaDecoded, @@ -29,16 +30,15 @@ import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; -import { getIndexExists } from '../../index/get_index_exists'; import { createBulkErrorObject, ImportRuleResponse, BulkError, isBulkError, isImportRegular, - transformError, buildSiemResponse, } from '../utils'; + import { patchRules } from '../../rules/patch_rules'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 7d7124259af06..7eb01e8b0d402 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { validate } from '../../../../../common/validate'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { patchRulesBulkSchema, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index fd5c33f126fef..780c248183ab9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { patchRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/patch_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -18,7 +19,8 @@ import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; import { patchRules } from '../../rules/patch_rules'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts index b2dd9ea8fb796..ac45e5d2ed3b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { queryRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/query_rules_type_dependents'; import { queryRulesSchema, @@ -14,7 +15,8 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdError, transform } from './utils'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { readRules } from '../../rules/read_rules'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index d088795a118b3..4c59ae2ba442e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { validate } from '../../../../../common/validate'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { updateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/update_rules_bulk_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index b883b7b3462e7..aad0068758f7d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { updateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; import { updateRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/update_rules_type_dependents'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -12,7 +13,8 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { SetupPlugins } from '../../../../plugin'; import { buildMlAuthz } from '../../../machine_learning/authz'; import { throwHttpError } from '../../../machine_learning/validation'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRules } from '../../rules/update_rules'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index b841507bc7a6b..f2788ab1bd4c9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -32,7 +32,7 @@ import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_fro import { RuleAlertType } from '../../rules/types'; import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; -import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request'; import { getMlRuleParams, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index ac9ac960d6f06..d27208de487df 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -7,11 +7,11 @@ import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { FullResponseSchema, fullResponseSchema, } from '../../../../../common/detection_engine/schemas/request'; -import { validateNonExact } from '../../../../../common/validate'; import { RulesSchema, rulesSchema, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts index aed15b66b9bb5..f10907f7c0c02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.test.ts @@ -13,13 +13,19 @@ import { getCreateSignalsMigrationSchemaMock } from '../../../../../common/detec import { getIndexVersionsByIndex } from '../../migrations/get_index_versions_by_index'; import { getSignalVersionsByIndex } from '../../migrations/get_signal_versions_by_index'; import { createMigration } from '../../migrations/create_migration'; -import { getIndexAliases } from '../../index/get_index_aliases'; +import { getIndexAliases } from '@kbn/securitysolution-es-utils'; import { getTemplateVersion } from '../index/check_template_version'; import { createSignalsMigrationRoute } from './create_signals_migration_route'; import { SIGNALS_TEMPLATE_VERSION } from '../index/get_signals_template'; jest.mock('../index/check_template_version'); -jest.mock('../../index/get_index_aliases'); +jest.mock('@kbn/securitysolution-es-utils', () => { + const original = jest.requireActual('@kbn/securitysolution-es-utils'); + return { + ...original, + getIndexAliases: jest.fn(), + }; +}); jest.mock('../../migrations/create_migration'); jest.mock('../../migrations/get_index_versions_by_index'); jest.mock('../../migrations/get_signal_versions_by_index'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts index 99732930234ae..6dd2534870dc2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts @@ -5,16 +5,16 @@ * 2.0. */ +import { transformError, BadRequestError, getIndexAliases } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_MIGRATION_URL } from '../../../../../common/constants'; import { createSignalsMigrationSchema } from '../../../../../common/detection_engine/schemas/request/create_signals_migration_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { buildSiemResponse, transformError } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { getTemplateVersion } from '../index/check_template_version'; import { isOutdated, signalsAreOutdated } from '../../migrations/helpers'; -import { getIndexAliases } from '../../index/get_index_aliases'; -import { BadRequestError } from '../../errors/bad_request_error'; import { signalsMigrationService } from '../../migrations/migration_service'; import { getIndexVersionsByIndex } from '../../migrations/get_index_versions_by_index'; import { getSignalVersionsByIndex } from '../../migrations/get_signal_versions_by_index'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts index 2f0749388522b..65ed42a0a166e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_MIGRATION_URL } from '../../../../../common/constants'; import { deleteSignalsMigrationSchema } from '../../../../../common/detection_engine/schemas/request/delete_signals_migration_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { buildSiemResponse, transformError } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { signalsMigrationService } from '../../migrations/migration_service'; import { getMigrationSavedObjectsById } from '../../migrations/get_migration_saved_objects_by_id'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts index 93567f77d17de..20931a8ba7233 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts @@ -5,15 +5,16 @@ * 2.0. */ +import { transformError, BadRequestError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { SetupPlugins } from '../../../../plugin'; import { DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL } from '../../../../../common/constants'; import { finalizeSignalsMigrationSchema } from '../../../../../common/detection_engine/schemas/request/finalize_signals_migration_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { BadRequestError } from '../../errors/bad_request_error'; import { isMigrationFailed, isMigrationPending } from '../../migrations/helpers'; import { signalsMigrationService } from '../../migrations/migration_service'; -import { buildSiemResponse, transformError } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { getMigrationSavedObjectsById } from '../../migrations/get_migration_saved_objects_by_id'; export const finalizeSignalsMigrationRoute = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/get_signals_migration_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/get_signals_migration_status_route.ts index cd7cd017b8c2d..d800cead20cdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/get_signals_migration_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/get_signals_migration_status_route.ts @@ -5,18 +5,18 @@ * 2.0. */ +import { transformError, getIndexAliases } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL } from '../../../../../common/constants'; import { getSignalsMigrationStatusSchema } from '../../../../../common/detection_engine/schemas/request/get_signals_migration_status_schema'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { getIndexAliases } from '../../index/get_index_aliases'; import { getIndexVersionsByIndex } from '../../migrations/get_index_versions_by_index'; import { getMigrationSavedObjectsByIndex } from '../../migrations/get_migration_saved_objects_by_index'; import { getSignalsIndicesInRange } from '../../migrations/get_signals_indices_in_range'; import { getSignalVersionsByIndex } from '../../migrations/get_signal_versions_by_index'; import { isOutdated, signalsAreOutdated } from '../../migrations/helpers'; import { getTemplateVersion } from '../index/check_template_version'; -import { buildSiemResponse, transformError } from '../utils'; +import { buildSiemResponse } from '../utils'; export const getSignalsMigrationStatusRoute = (router: SecuritySolutionPluginRouter) => { router.get( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index d92d39f91baa4..fd001595fb9c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { setSignalStatusValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/set_signal_status_type_dependents'; import { SetSignalsStatusSchemaDecoded, @@ -12,7 +13,8 @@ import { } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; export const setSignalsStatusRoute = (router: SecuritySolutionPluginRouter) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts index 9e542f6974ffc..91172a277bf54 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -5,9 +5,10 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts index 31ee4f5f42a5d..817e4b95aabce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../utils'; +import { buildSiemResponse } from '../utils'; + import { readTags } from '../../tags/read_tags'; export const readTagsRoute = (router: SecuritySolutionPluginRouter) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts index cca7e871f5b8b..3c3eda5b19e44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.test.ts @@ -6,15 +6,13 @@ */ import Boom from '@hapi/boom'; -import { errors } from '@elastic/elasticsearch'; import { SavedObjectsFindResponse } from 'kibana/server'; import { alertsClientMock } from '../../../../../alerting/server/mocks'; import { IRuleSavedAttributesSavedObjectAttributes, IRuleStatusSOAttributes } from '../rules/types'; -import { BadRequestError } from '../errors/bad_request_error'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { - transformError, transformBulkError, BulkError, createSuccessObject, @@ -35,95 +33,6 @@ import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; let alertsClient: ReturnType; describe('utils', () => { - describe('transformError', () => { - test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { - const boom = new Boom.Boom('some boom message'); - const transformed = transformError(boom); - expect(transformed).toEqual({ - message: 'An internal server error occurred', - statusCode: 500, - }); - }); - - test('returns transformed output if it is some non boom object that has a statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'some message', - statusCode: 403, - }); - }); - - test('returns a transformed message with the message set and statusCode', () => { - const error: Error & { statusCode?: number } = { - statusCode: 403, - name: 'some name', - message: 'some message', - }; - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'some message', - statusCode: 403, - }); - }); - - test('transforms best it can if it is some non boom object but it does not have a status Code.', () => { - const error: Error = { - name: 'some name', - message: 'some message', - }; - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'some message', - statusCode: 500, - }); - }); - - test('it detects a BadRequestError and returns a status code of 400 from that particular error type', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'I have a type error', - statusCode: 400, - }); - }); - - test('it detects a BadRequestError and returns a Boom status of 400', () => { - const error: BadRequestError = new BadRequestError('I have a type error'); - const transformed = transformError(error); - expect(transformed).toEqual({ - message: 'I have a type error', - statusCode: 400, - }); - }); - - it('transforms a ResponseError returned by the elasticsearch client', () => { - const error: errors.ResponseError = { - name: 'ResponseError', - message: 'illegal_argument_exception', - headers: {}, - body: { - error: { - type: 'illegal_argument_exception', - reason: 'detailed explanation', - }, - }, - meta: ({} as unknown) as errors.ResponseError['meta'], - statusCode: 400, - }; - const transformed = transformError(error); - - expect(transformed).toEqual({ - message: 'illegal_argument_exception: detailed explanation', - statusCode: 400, - }); - }); - }); - describe('transformBulkError', () => { test('returns transformed object if it is a boom object', () => { const boom = new Boom.Boom('some boom message', { statusCode: 400 }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts index c2acbf9c5cc0a..130084da21591 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/utils.ts @@ -6,8 +6,8 @@ */ import Boom from '@hapi/boom'; -import { errors } from '@elastic/elasticsearch'; import { has, snakeCase } from 'lodash/fp'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { SanitizedAlert } from '../../../../../alerting/common'; import { @@ -17,53 +17,12 @@ import { SavedObjectsFindResult, } from '../../../../../../../src/core/server'; import { AlertsClient } from '../../../../../alerting/server'; -import { BadRequestError } from '../errors/bad_request_error'; import { RuleStatusResponse, IRuleStatusSOAttributes } from '../rules/types'; export interface OutputError { message: string; statusCode: number; } - -/** - * @deprecated Use kbn-securitysolution-es-utils version - */ -export const transformError = (err: Error & Partial): OutputError => { - if (Boom.isBoom(err)) { - return { - message: err.output.payload.message, - statusCode: err.output.statusCode, - }; - } else { - if (err.statusCode != null) { - if (err.body?.error != null) { - return { - statusCode: err.statusCode, - message: `${err.body.error.type}: ${err.body.error.reason}`, - }; - } else { - return { - statusCode: err.statusCode, - message: err.message, - }; - } - } else if (err instanceof BadRequestError) { - // allows us to throw request validation errors in the absence of Boom - return { - message: err.message, - statusCode: 400, - }; - } else { - // natively return the err and allow the regular framework - // to deal with the error when it is a non Boom - return { - message: err.message ?? '(unknown error message)', - statusCode: 500, - }; - } - } -}; - export interface BulkError { id?: string; rule_id?: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index 248afda7ff5c3..c5b3e98c4c44e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -8,7 +8,7 @@ import { Readable } from 'stream'; import { createPromiseFromStreams } from '@kbn/utils'; import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; -import { BadRequestError } from '../errors/bad_request_error'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { ImportRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/import_rules_schema'; type PromiseFromStreams = ImportRulesSchemaDecoded | Error; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index 5e3c3c8b1cb98..0c2d81c18646b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -11,15 +11,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { createSplitStream, createMapStream, createConcatStream } from '@kbn/utils'; -import { formatErrors } from '../../../../common/format_errors'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { importRuleValidateTypeDependents } from '../../../../common/detection_engine/schemas/request/import_rules_type_dependents'; -import { exactCheck } from '../../../../common/exact_check'; import { importRulesSchema, ImportRulesSchema, ImportRulesSchemaDecoded, } from '../../../../common/detection_engine/schemas/request/import_rules_schema'; -import { BadRequestError } from '../errors/bad_request_error'; import { parseNdjsonStrings, filterExportedCounts, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts index b91557c6d7b1b..f2d28d13fa926 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -8,14 +8,13 @@ import * as t from 'io-ts'; import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { formatErrors } from '../../../../common/format_errors'; -import { exactCheck } from '../../../../common/exact_check'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { addPrepackagedRulesSchema, AddPrepackagedRulesSchema, AddPrepackagedRulesSchemaDecoded, } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; -import { BadRequestError } from '../errors/bad_request_error'; // TODO: convert rules files to TS and add explicit type definitions import { rawRules } from './prepackaged_rules'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index bccd1f2fb73ca..b9a88bc36a812 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -6,7 +6,7 @@ */ import { defaults } from 'lodash/fp'; -import { validate } from '../../../../common/validate'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { PartialAlert } from '../../../../../alerting/server'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PatchRulesOptions } from './types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 2990a0f728027..e670535c26ae5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -14,55 +14,70 @@ import { SavedObjectsFindResponse, SavedObjectsClientContract, } from 'kibana/server'; +import { + MachineLearningJobIdOrUndefined, + From, + FromOrUndefined, + RiskScore, + RiskScoreMapping, + RiskScoreMappingOrUndefined, + RiskScoreOrUndefined, + ThreatIndexOrUndefined, + ThreatQueryOrUndefined, + ThreatMappingOrUndefined, + ThreatFiltersOrUndefined, + ThreatLanguageOrUndefined, + ConcurrentSearchesOrUndefined, + ItemsPerSearchOrUndefined, + ThreatIndicatorPathOrUndefined, + Threats, + ThreatsOrUndefined, + TypeOrUndefined, + Type, + LanguageOrUndefined, + SeverityMapping, + SeverityMappingOrUndefined, + SeverityOrUndefined, + Severity, + MaxSignalsOrUndefined, + MaxSignals, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { VersionOrUndefined, Version } from '@kbn/securitysolution-io-ts-types'; + +import { ListArrayOrUndefined, ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { FalsePositives, - From, RuleId, Immutable, DescriptionOrUndefined, Interval, - MaxSignals, - RiskScore, OutputIndex, Name, - Severity, Tags, - Threats, To, - Type, References, - Version, AnomalyThresholdOrUndefined, QueryOrUndefined, - LanguageOrUndefined, SavedIdOrUndefined, TimelineIdOrUndefined, TimelineTitleOrUndefined, - MachineLearningJobIdOrUndefined, IndexOrUndefined, NoteOrUndefined, MetaOrUndefined, Description, Enabled, - VersionOrUndefined, IdOrUndefined, RuleIdOrUndefined, EnabledOrUndefined, FalsePositivesOrUndefined, - FromOrUndefined, OutputIndexOrUndefined, IntervalOrUndefined, - MaxSignalsOrUndefined, - RiskScoreOrUndefined, NameOrUndefined, - SeverityOrUndefined, TagsOrUndefined, ToOrUndefined, - ThreatsOrUndefined, ThresholdOrUndefined, - TypeOrUndefined, ReferencesOrUndefined, PerPageOrUndefined, PageOrUndefined, @@ -79,31 +94,16 @@ import { Author, AuthorOrUndefined, LicenseOrUndefined, - RiskScoreMapping, - RiskScoreMappingOrUndefined, - SeverityMapping, - SeverityMappingOrUndefined, TimestampOverrideOrUndefined, BuildingBlockTypeOrUndefined, RuleNameOverrideOrUndefined, EventCategoryOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; -import { - ThreatIndexOrUndefined, - ThreatQueryOrUndefined, - ThreatMappingOrUndefined, - ThreatFiltersOrUndefined, - ThreatLanguageOrUndefined, - ConcurrentSearchesOrUndefined, - ItemsPerSearchOrUndefined, - ThreatIndicatorPathOrUndefined, -} from '../../../../common/detection_engine/schemas/types/threat_mapping'; import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; import { Alert, SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { PartialFilter } from '../types'; -import { ListArrayOrUndefined, ListArray } from '../../../../common/detection_engine/schemas/types'; import { RuleParams } from '../schemas/rule_schemas'; export type RuleAlertType = Alert; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 5c8450201d096..a31f9bec2cd58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -6,53 +6,53 @@ */ import { pickBy, isEmpty } from 'lodash/fp'; +import { + FromOrUndefined, + MachineLearningJobIdOrUndefined, + RiskScoreMappingOrUndefined, + RiskScoreOrUndefined, + ConcurrentSearchesOrUndefined, + ItemsPerSearchOrUndefined, + ThreatFiltersOrUndefined, + ThreatIndexOrUndefined, + ThreatLanguageOrUndefined, + ThreatMappingOrUndefined, + ThreatQueryOrUndefined, + ThreatsOrUndefined, + TypeOrUndefined, + LanguageOrUndefined, + SeverityOrUndefined, + SeverityMappingOrUndefined, + MaxSignalsOrUndefined, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { ListArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; +import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; import { DescriptionOrUndefined, AnomalyThresholdOrUndefined, QueryOrUndefined, - LanguageOrUndefined, SavedIdOrUndefined, TimelineIdOrUndefined, TimelineTitleOrUndefined, - MachineLearningJobIdOrUndefined, IndexOrUndefined, NoteOrUndefined, MetaOrUndefined, - VersionOrUndefined, FalsePositivesOrUndefined, - FromOrUndefined, OutputIndexOrUndefined, IntervalOrUndefined, - MaxSignalsOrUndefined, - RiskScoreOrUndefined, NameOrUndefined, - SeverityOrUndefined, TagsOrUndefined, ToOrUndefined, - ThreatsOrUndefined, ThresholdOrUndefined, - TypeOrUndefined, ReferencesOrUndefined, AuthorOrUndefined, BuildingBlockTypeOrUndefined, LicenseOrUndefined, - RiskScoreMappingOrUndefined, RuleNameOverrideOrUndefined, - SeverityMappingOrUndefined, TimestampOverrideOrUndefined, EventCategoryOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { PartialFilter } from '../types'; -import { - ConcurrentSearchesOrUndefined, - ItemsPerSearchOrUndefined, - ListArrayOrUndefined, - ThreatFiltersOrUndefined, - ThreatIndexOrUndefined, - ThreatLanguageOrUndefined, - ThreatMappingOrUndefined, - ThreatQueryOrUndefined, -} from '../../../../common/detection_engine/schemas/types'; export const calculateInterval = ( interval: string | undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts index 79b862d6419c2..2af481b195a07 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts @@ -7,15 +7,26 @@ import * as t from 'io-ts'; -import { listArray } from '../../../../common/detection_engine/schemas/types/lists'; import { + actionsCamel, + from, + machine_learning_job_id_normalized, + risk_score, + risk_score_mapping, threat_mapping, threat_index, threat_query, concurrentSearchesOrUndefined, itemsPerSearchOrUndefined, threatIndicatorPathOrUndefined, -} from '../../../../common/detection_engine/schemas/types/threat_mapping'; + threats, + severity, + severity_mapping, + throttleOrNull, + max_signals, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { listArray } from '@kbn/securitysolution-io-ts-list-types'; +import { version } from '@kbn/securitysolution-io-ts-types'; import { author, buildingBlockTypeOrUndefined, @@ -23,7 +34,6 @@ import { enabled, noteOrUndefined, false_positives, - from, rule_id, immutable, indexOrUndefined, @@ -36,32 +46,23 @@ import { query, queryOrUndefined, filtersOrUndefined, - max_signals, - risk_score, - risk_score_mapping, ruleNameOverrideOrUndefined, - severity, - severity_mapping, tags, timestampOverrideOrUndefined, - threats, to, references, - version, eventCategoryOverrideOrUndefined, savedIdOrUndefined, saved_id, thresholdNormalized, anomaly_threshold, - actionsCamel, - throttleOrNull, createdByOrNull, updatedByOrNull, created_at, updated_at, } from '../../../../common/detection_engine/schemas/common/schemas'; + import { SIGNALS_ID, SERVER_APP_ID } from '../../../../common/constants'; -import { machine_learning_job_id_normalized } from '../../../../common/detection_engine/schemas/types/normalized_ml_job_id'; const nonEqlLanguages = t.keyof({ kuery: null, lucene: null }); export const baseRuleParams = t.exact( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts index e1618d217d0dc..f653fde816c62 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts @@ -7,9 +7,10 @@ import type { estypes } from '@elastic/elasticsearch'; import { Logger } from 'src/core/server'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { ListClient } from '../../../../../../lists/server'; import { BuildRuleMessage } from '../rule_messages'; -import { ExceptionListItemSchema, Type } from '../../../../../../lists/common/schemas'; +import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; export interface FilterEventsAgainstListOptions { listClient: ListClient; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 86940e9b77084..3d6a1f8da7f4d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -5,15 +5,14 @@ * 2.0. */ +import { BadRequestError } from '@kbn/securitysolution-es-utils'; +import { Type, LanguageOrUndefined, Language } from '@kbn/securitysolution-io-ts-alerting-types'; import { assertUnreachable } from '../../../../common/utility_types'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { - LanguageOrUndefined, QueryOrUndefined, - Type, SavedIdOrUndefined, IndexOrUndefined, - Language, } from '../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { @@ -22,7 +21,6 @@ import { AlertServices, } from '../../../../../alerting/server'; import { PartialFilter } from '../types'; -import { BadRequestError } from '../errors/bad_request_error'; import { QueryFilter } from './types'; interface GetFilterArgs { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts index 88ce9de15cff8..82f3ff46b347d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - RiskScore, - RiskScoreMappingOrUndefined, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { RiskScore, RiskScoreMappingOrUndefined } from '@kbn/securitysolution-io-ts-alerting-types'; import { sampleDocRiskScore } from '../__mocks__/es_results'; import { buildRiskScoreFromMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts index 84d45f22b7f44..7253b24e66088 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -5,12 +5,9 @@ * 2.0. */ +import { RiskScore, RiskScoreMappingOrUndefined } from '@kbn/securitysolution-io-ts-alerting-types'; import { get } from 'lodash/fp'; -import { - Meta, - RiskScore, - RiskScoreMappingOrUndefined, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Meta } from '../../../../../common/detection_engine/schemas/common/schemas'; import { SignalSource } from '../types'; export interface BuildRiskScoreFromMappingProps { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts index cfd4b81ae3bc8..4ccf24a307cc4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -5,10 +5,8 @@ * 2.0. */ -import { - Severity, - SeverityMappingOrUndefined, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Severity, SeverityMappingOrUndefined } from '@kbn/securitysolution-io-ts-alerting-types'; + import { sampleDocSeverity } from '../__mocks__/es_results'; import { buildSeverityFromMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts index 44e41fbd0c9c0..652b6b2221900 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -6,13 +6,14 @@ */ import { get } from 'lodash/fp'; + import { - Meta, Severity, SeverityMappingItem, severity as SeverityIOTS, SeverityMappingOrUndefined, -} from '../../../../../common/detection_engine/schemas/common/schemas'; +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { Meta } from '../../../../../common/detection_engine/schemas/common/schemas'; import { SearchTypes } from '../../../../../common/detection_engine/types'; import { SignalSource } from '../types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 6e24e96c6e36d..d00bcc2a9f11e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -12,7 +12,7 @@ import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; import { flow } from 'fp-ts/lib/function'; import * as t from 'io-ts'; -import { validateNonExact } from '../../../../common/validate'; +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { toError, toPromise } from '../../../../common/fp_utils'; import { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts index e39b78b4f4a44..f49e3dec93600 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ import type { estypes } from '@elastic/elasticsearch'; -import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { Filter } from 'src/plugins/data/common'; import { ThreatListDoc, ThreatListItem } from './types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts index 1c0300ee0cc74..a96eb50af3c50 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - ThreatMapping, - ThreatMappingEntries, -} from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { ThreatMapping, ThreatMappingEntries } from '@kbn/securitysolution-io-ts-alerting-types'; import { filterThreatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts index 18204bb678a47..22e21ef40cb3e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts @@ -7,7 +7,7 @@ import get from 'lodash/fp/get'; import { Filter } from 'src/plugins/data/common'; -import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping'; +import { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import { BooleanFilter, BuildEntriesMappingFilterOptions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 360fb118faa84..82fc0dd3abd0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -5,11 +5,6 @@ * 2.0. */ import type { estypes } from '@elastic/elasticsearch'; -import { ListClient } from '../../../../../../lists/server'; -import { - Type, - LanguageOrUndefined, -} from '../../../../../common/detection_engine/schemas/common/schemas'; import { ThreatQuery, ThreatMapping, @@ -19,7 +14,10 @@ import { ConcurrentSearches, ItemsPerSearch, ThreatIndicatorPathOrUndefined, -} from '../../../../../common/detection_engine/schemas/types/threat_mapping'; + LanguageOrUndefined, + Type, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { ListClient } from '../../../../../../lists/server'; import { AlertInstanceContext, AlertInstanceState, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index bd37cf62c74b0..c2e3fe83b8893 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -14,6 +14,7 @@ import { isEmpty, partition } from 'lodash'; import { ApiResponse, Context } from '@elastic/elasticsearch/lib/Transport'; import { SortResults } from '@elastic/elasticsearch/api/types'; +import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { TimestampOverrideOrUndefined, Privilege, @@ -27,7 +28,6 @@ import { } from '../../../../../alerting/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; -import { ListArray } from '../../../../common/detection_engine/schemas/types/lists'; import { BulkResponseErrorAggregation, SignalHit, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 85c8483a0b988..03ec7928115b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -5,54 +5,55 @@ * 2.0. */ +import { + From, + MachineLearningJobIdOrUndefined, + RiskScore, + RiskScoreMappingOrUndefined, + ThreatIndexOrUndefined, + ThreatQueryOrUndefined, + ThreatMappingOrUndefined, + ThreatLanguageOrUndefined, + ConcurrentSearchesOrUndefined, + ItemsPerSearchOrUndefined, + ThreatIndicatorPathOrUndefined, + ThreatsOrUndefined, + Type, + LanguageOrUndefined, + Severity, + SeverityMappingOrUndefined, + MaxSignals, +} from '@kbn/securitysolution-io-ts-alerting-types'; +import { Version } from '@kbn/securitysolution-io-ts-types'; + +import { ListArrayOrUndefined } from '@kbn/securitysolution-io-ts-list-types'; import { AnomalyThresholdOrUndefined, Description, NoteOrUndefined, - ThreatsOrUndefined, ThresholdOrUndefined, FalsePositives, - From, Immutable, IndexOrUndefined, - LanguageOrUndefined, - MaxSignals, - MachineLearningJobIdOrUndefined, - RiskScore, OutputIndex, QueryOrUndefined, References, SavedIdOrUndefined, - Severity, To, TimelineIdOrUndefined, TimelineTitleOrUndefined, - Version, MetaOrUndefined, RuleId, AuthorOrUndefined, BuildingBlockTypeOrUndefined, LicenseOrUndefined, - RiskScoreMappingOrUndefined, RuleNameOverrideOrUndefined, - SeverityMappingOrUndefined, TimestampOverrideOrUndefined, - Type, EventCategoryOverrideOrUndefined, } from '../../../common/detection_engine/schemas/common/schemas'; -import { - ThreatIndexOrUndefined, - ThreatQueryOrUndefined, - ThreatMappingOrUndefined, - ThreatLanguageOrUndefined, - ConcurrentSearchesOrUndefined, - ItemsPerSearchOrUndefined, - ThreatIndicatorPathOrUndefined, -} from '../../../common/detection_engine/schemas/types/threat_mapping'; import { LegacyCallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; -import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types'; import { AlertTypeParams } from '../../../../alerting/common'; export type PartialFilter = Partial; diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts index 85c1eea7a957a..d5e8e951397c2 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; +import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server/'; import { ILicense } from '../../../../licensing/server'; import { MlPluginSetup } from '../../../../ml/server'; @@ -16,8 +17,6 @@ import { hasMlAdminPermissions } from '../../../common/machine_learning/has_ml_a import { isMlRule } from '../../../common/machine_learning/helpers'; import { Validation } from './validation'; import { cache } from './cache'; -import { Type } from '../../../common/detection_engine/schemas/common/schemas'; - export interface MlAuthz { validateRuleType: (type: Type) => Promise; } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts index 113860f369f78..62770408af2fe 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/clean_draft_timelines/index.ts @@ -6,9 +6,11 @@ */ import uuid from 'uuid'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { ConfigType } from '../../../../..'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; + import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; import { buildFrameworkRequest } from '../../../utils/common'; import { SetupPlugins } from '../../../../../plugin'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts index f3f813ace411d..cd7770dcd5b48 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/draft_timelines/get_draft_timelines/index.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { ConfigType } from '../../../../..'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; + import { TIMELINE_DRAFT_URL } from '../../../../../../common/constants'; import { buildFrameworkRequest } from '../../../utils/common'; import { SetupPlugins } from '../../../../../plugin'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts index cb7d984ade40b..32fd87f39620b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { NOTE_URL } from '../../../../../common/constants'; @@ -13,7 +14,7 @@ import { SetupPlugins } from '../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import { ConfigType } from '../../../..'; -import { transformError, buildSiemResponse } from '../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../utils/common'; import { persistNoteSchema } from '../../schemas/notes'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts index 53ac002721c6e..ee407468f0c30 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/pinned_events/persist_pinned_event.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { PINNED_EVENT_URL } from '../../../../../common/constants'; @@ -13,7 +14,7 @@ import { SetupPlugins } from '../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../utils/build_validation/route_validation'; import { ConfigType } from '../../../..'; -import { transformError, buildSiemResponse } from '../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../utils/common'; import { persistPinnedEventSchema } from '../../schemas/pinned_events'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts index bb447948df24a..438ce71edd089 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/prepackaged_timelines/install_prepackaged_timelines/index.ts @@ -5,15 +5,16 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_PREPACKAGED_URL } from '../../../../../../common/constants'; import { SetupPlugins } from '../../../../../plugin'; import { ConfigType } from '../../../../../config'; -import { validate } from '../../../../../../common/validate'; -import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { installPrepackagedTimelines } from './helpers'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts index f35ddf1a76c7d..432a441e61e07 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; @@ -13,7 +14,7 @@ import { ConfigType } from '../../../../..'; import { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { createTimelineSchema } from '../../../schemas/timelines'; import { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts index 7617881b90b7f..13fbc22aba5d3 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/delete_timelines/index.ts @@ -5,13 +5,15 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { ConfigType } from '../../../../..'; import { deleteTimelinesSchema } from '../../../schemas/timelines/delete_timelines_schema'; import { SecuritySolutionPluginRouter } from '../../../../../types'; import { SetupPlugins } from '../../../../../plugin'; import { TIMELINE_URL } from '../../../../../../common/constants'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; + import { buildFrameworkRequest } from '../../../utils/common'; import { deleteTimeline } from '../../../saved_object/timelines'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts index 9e1eabc4450bd..0d60129b1fcd6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/export_timelines/index.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import { TIMELINE_EXPORT_URL } from '../../../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { ConfigType } from '../../../../../config'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { exportTimelinesQuerySchema, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts index 8d94cd2ef2cce..921ae2352a565 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timeline/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; @@ -13,7 +14,7 @@ import { ConfigType } from '../../../../..'; import { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; -import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../utils/common'; import { getTimelineQuerySchema } from '../../../schemas/timelines'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts index 51a02db681b0c..4599916092611 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/get_timelines/index.ts @@ -10,13 +10,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINES_URL } from '../../../../../../common/constants'; import { ConfigType } from '../../../../..'; import { SetupPlugins } from '../../../../../plugin'; -import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { buildFrameworkRequest, escapeHatch, throwErrors } from '../../../utils/common'; import { getAllTimeline } from '../../../saved_object/timelines'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts index aeb7463377b1f..d016fe8a24ff2 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/create_timelines_stream_from_ndjson.ts @@ -11,6 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { createConcatStream, createSplitStream, createMapStream } from '@kbn/utils'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { parseNdjsonStrings, filterExportedCounts, @@ -19,7 +20,6 @@ import { import { ImportTimelineResponse } from './types'; import { ImportTimelinesSchemaRt } from '../../../schemas/timelines/import_timelines_schema'; -import { BadRequestError } from '../../../../detection_engine/errors/bad_request_error'; import { throwErrors } from '../../../utils/common'; type ErrorFactory = (message: string) => Error; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts index a19276652e78b..70d93d7552b1c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts @@ -10,12 +10,12 @@ import { Readable } from 'stream'; import uuid from 'uuid'; import { createPromiseFromStreams } from '@kbn/utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { ImportTimelineResultSchema, importTimelineResultSchema, TimelineStatus, } from '../../../../../../common/types/timeline'; -import { validate } from '../../../../../../common/validate'; import { createBulkErrorObject, BulkError } from '../../../../detection_engine/routes/utils'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts index 603aad16dd9c6..65ffd10c5168b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.ts @@ -8,6 +8,7 @@ import { extname } from 'path'; import { Readable } from 'stream'; +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_IMPORT_URL } from '../../../../../../common/constants'; @@ -15,7 +16,7 @@ import { TIMELINE_IMPORT_URL } from '../../../../../../common/constants'; import { SetupPlugins } from '../../../../../plugin'; import { ConfigType } from '../../../../../config'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; -import { buildSiemResponse, transformError } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { importTimelines } from './helpers'; import { ImportTimelinesPayloadSchemaRt } from '../../../schemas/timelines/import_timelines_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts index b0142625f5e08..e3ad9bc7cb048 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/patch_timelines/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_URL } from '../../../../../../common/constants'; @@ -13,7 +14,7 @@ import { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { ConfigType } from '../../../../..'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { patchTimelineSchema } from '../../../schemas/timelines/patch_timelines_schema'; import { buildFrameworkRequest, TimelineStatusActions } from '../../../utils/common'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts index 2cc3888696248..0de64171e0fb8 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/persist_favorite/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { transformError } from '@kbn/securitysolution-es-utils'; import type { SecuritySolutionPluginRouter } from '../../../../../types'; import { TIMELINE_FAVORITE_URL } from '../../../../../../common/constants'; @@ -13,7 +14,7 @@ import { SetupPlugins } from '../../../../../plugin'; import { buildRouteValidationWithExcess } from '../../../../../utils/build_validation/route_validation'; import { ConfigType } from '../../../../..'; -import { transformError, buildSiemResponse } from '../../../../detection_engine/routes/utils'; +import { buildSiemResponse } from '../../../../detection_engine/routes/utils'; import { buildFrameworkRequest } from '../../../utils/common'; import { persistFavorite } from '../../../saved_object/timelines'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index c4ddefd925b37..be086732ddcd0 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -11,9 +11,9 @@ import fs from 'fs'; import { Readable } from 'stream'; import { createListStream } from '@kbn/utils'; import { schema } from '@kbn/config-schema'; -import { isObject } from 'lodash/fp'; import { KibanaRequest } from 'src/core/server'; +import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { SetupPlugins, StartPlugins } from '../../../plugin'; import type { SecuritySolutionRequestHandlerContext } from '../../../types'; @@ -40,32 +40,6 @@ export const buildFrameworkRequest = async ( export const escapeHatch = schema.object({}, { unknowns: 'allow' }); -/** - * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts - */ -export const formatErrors = (errors: rt.Errors): string[] => { - const err = errors.map((error) => { - if (error.message != null) { - return error.message; - } else { - const keyContext = error.context - .filter( - (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' - ) - .map((entry) => entry.key) - .join(','); - - const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); - const suppliedValue = - keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; - const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; - return `Invalid value "${value}" supplied to "${suppliedValue}"`; - } - }); - - return [...new Set(err)]; -}; - type ErrorFactory = (message: string) => Error; export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { diff --git a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts index caba0eb9f0152..5dfa74c14bf72 100644 --- a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts +++ b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts @@ -8,8 +8,7 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { formatErrors } from '../../../common/format_errors'; -import { exactCheck } from '../../../common/exact_check'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { RouteValidationFunction, RouteValidationResultFactory, diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts index 02749cf093695..b0b70aeb3ea34 100644 --- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts @@ -12,15 +12,14 @@ import { fold } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; import { createMapStream, createFilterStream } from '@kbn/utils'; -import { formatErrors } from '../../../common/format_errors'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; +import { BadRequestError } from '@kbn/securitysolution-es-utils'; import { importRuleValidateTypeDependents } from '../../../common/detection_engine/schemas/request/import_rules_type_dependents'; import { ImportRulesSchemaDecoded, importRulesSchema, ImportRulesSchema, } from '../../../common/detection_engine/schemas/request/import_rules_schema'; -import { exactCheck } from '../../../common/exact_check'; -import { BadRequestError } from '../../lib/detection_engine/errors/bad_request_error'; export interface RulesObjectsExportResultDetails { /** number of successfully exported objects */ diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 0f57728f99d67..d8466f013a110 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -12,8 +12,8 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { Context } from '@elastic/elasticsearch/lib/Transport'; import { SearchResponse } from 'elasticsearch'; +import { NonEmptyEntriesArray } from '@kbn/securitysolution-io-ts-list-types'; import { PrePackagedRulesAndTimelinesStatusSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response'; -import { NonEmptyEntriesArray } from '../../plugins/lists/common/schemas'; import { getCreateExceptionListDetectionSchemaMock } from '../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { CreateRulesSchema, diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 0512cede0a84f..29846a79d6b02 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -9,12 +9,12 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; +import { Type } from '@kbn/securitysolution-io-ts-list-types'; import { getImportListItemAsBuffer } from '../../plugins/lists/common/schemas/request/import_list_item_schema.mock'; import { ListItemSchema, ExceptionListSchema, ExceptionListItemSchema, - Type, } from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; import { LIST_INDEX, LIST_ITEM_URL } from '../../plugins/lists/common/constants'; From df6b002922f47c7f0de7eb92d11bfe5136c1786b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 18 May 2021 01:24:12 -0600 Subject: [PATCH 094/186] Reduce limits of security solutions to max + 15kb (#100247) ## Summary With recent package changes and fixes we are down to 61kb for page load bundle. General rules are max kilobytes + 15kb for buffer so that would mean we should lower it to be 76kb. Resolves #95870 --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 5748984c7bc6e..348ecb7eea7f2 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -68,7 +68,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 95864 securityOss: 30806 - securitySolution: 187863 + securitySolution: 76000 share: 99061 snapshotRestore: 79032 spaces: 57868 From 4227e03f92c84ffdaeb57e95f31158db0f0ce317 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 18 May 2021 10:13:39 +0200 Subject: [PATCH 095/186] [Lens] Debounce axis name inputs mob programming (#100108) --- .../dimension_panel/dimension_editor.tsx | 42 +++------------ .../definitions/ranges/ranges.test.tsx | 11 ++++ .../shared_components/label_input.tsx | 15 ++---- .../indexpattern_datasource/query_input.tsx | 12 ++--- .../lens/public/pie_visualization/toolbar.tsx | 13 ++--- .../shared_components/debounced_value.ts | 52 +++++++++++++++++++ .../lens/public/shared_components/index.ts | 1 + .../axis_settings_popover.tsx | 14 +++-- 8 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 x-pack/plugins/lens/public/shared_components/debounced_value.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index d84d418ff231c..8e26713630281 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -7,7 +7,7 @@ import './dimension_editor.scss'; import _ from 'lodash'; -import React, { useState, useMemo, useEffect, useRef } from 'react'; +import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiListGroup, @@ -44,6 +44,7 @@ import { ReferenceEditor } from './reference_editor'; import { setTimeScaling, TimeScaling } from './time_scaling'; import { defaultFilter, Filtering, setFilter } from './filtering'; import { AdvancedOptions } from './advanced_options'; +import { useDebouncedValue } from '../../shared_components'; const operationPanels = getOperationDisplay(); @@ -53,39 +54,8 @@ export interface DimensionEditorProps extends IndexPatternDimensionEditorProps { currentIndexPattern: IndexPattern; } -/** - * This component shows a debounced input for the label of a dimension. It will update on root state changes - * if no debounced changes are in flight because the user is currently typing into the input. - */ const LabelInput = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => { - const [inputValue, setInputValue] = useState(value); - const unflushedChanges = useRef(false); - - // Save the initial value - const initialValue = useRef(value); - - const onChangeDebounced = useMemo(() => { - const callback = _.debounce((val: string) => { - onChange(val); - unflushedChanges.current = false; - }, 256); - return (val: string) => { - unflushedChanges.current = true; - callback(val); - }; - }, [onChange]); - - useEffect(() => { - if (!unflushedChanges.current && value !== inputValue) { - setInputValue(value); - } - }, [value, inputValue]); - - const handleInputChange = (e: React.ChangeEvent) => { - const val = String(e.target.value); - setInputValue(val); - onChangeDebounced(val || initialValue.current); - }; + const { inputValue, handleInputChange, initialValue } = useDebouncedValue({ onChange, value }); return ( { + handleInputChange(e.target.value); + }} + placeholder={initialValue} /> ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 08bcfcb2e93be..cfbe0f8903daf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -46,6 +46,17 @@ jest.mock('@elastic/eui', () => { }; }); +jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn()); + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ params }) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx index b9870dc8bfec3..81f1b669a53a2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { useState } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; +import React from 'react'; import { EuiFieldText, keys } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { useDebouncedValue } from '../../../../shared_components'; export const LabelInput = ({ value, @@ -27,20 +27,13 @@ export const LabelInput = ({ dataTestSubj?: string; compressed?: boolean; }) => { - const [inputValue, setInputValue] = useState(value); - - useDebounce(() => onChange(inputValue), 256, [inputValue]); - - const handleInputChange = (e: React.ChangeEvent) => { - const val = String(e.target.value); - setInputValue(val); - }; + const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange }); return ( handleInputChange(e.target.value)} fullWidth placeholder={placeholder || ''} inputRef={(node) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx index 50941148342c3..6c2b62f96eaec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useState } from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { IndexPattern } from './types'; import { QueryStringInput, Query } from '../../../../../src/plugins/data/public'; +import { useDebouncedValue } from '../shared_components'; export const QueryInput = ({ value, @@ -26,13 +26,7 @@ export const QueryInput = ({ onSubmit: () => void; disableAutoFocus?: boolean; }) => { - const [inputValue, setInputValue] = useState(value); - - useDebounce(() => onChange(inputValue), 256, [inputValue]); - - const handleInputChange = (input: Query) => { - setInputValue(input); - }; + const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange }); return ( void; }) => { - const [localValue, setLocalValue] = useState(value); - useDebounce(() => setValue(localValue), 256, [localValue]); - + const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange: setValue }); return ( { - setLocalValue(Number(e.currentTarget.value)); + handleInputChange(Number(e.currentTarget.value)); }} /> ); diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts new file mode 100644 index 0000000000000..5447384ce38ea --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts @@ -0,0 +1,52 @@ +/* + * 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 { useState, useMemo, useEffect, useRef } from 'react'; +import _ from 'lodash'; + +/** + * Debounces value changes and updates inputValue on root state changes if no debounced changes + * are in flight because the user is currently modifying the value. + */ + +export const useDebouncedValue = ({ + onChange, + value, +}: { + onChange: (val: T) => void; + value: T; +}) => { + const [inputValue, setInputValue] = useState(value); + const unflushedChanges = useRef(false); + + // Save the initial value + const initialValue = useRef(value); + + const onChangeDebounced = useMemo(() => { + const callback = _.debounce((val: T) => { + onChange(val); + unflushedChanges.current = false; + }, 256); + return (val: T) => { + unflushedChanges.current = true; + callback(val); + }; + }, [onChange]); + + useEffect(() => { + if (!unflushedChanges.current && value !== inputValue) { + setInputValue(value); + } + }, [value, inputValue]); + + const handleInputChange = (val: T) => { + setInputValue(val); + onChangeDebounced(val || initialValue.current); + }; + + return { inputValue, handleInputChange, initialValue: initialValue.current }; +}; diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts index 482571696a4d3..ae57da976a881 100644 --- a/x-pack/plugins/lens/public/shared_components/index.ts +++ b/x-pack/plugins/lens/public/shared_components/index.ts @@ -9,3 +9,4 @@ export * from './empty_placeholder'; export { ToolbarPopoverProps, ToolbarPopover } from './toolbar_popover'; export { LegendSettingsPopover } from './legend_settings_popover'; export { PalettePicker } from './palette_picker'; +export { useDebouncedValue } from './debounced_value'; diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx index d9c60ae666484..d995e1a055e68 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { XYLayerConfig, AxesSettingsConfig } from './types'; -import { ToolbarPopover } from '../shared_components'; +import { ToolbarPopover, useDebouncedValue } from '../shared_components'; import { isHorizontalChart } from './state_helpers'; import { EuiIconAxisBottom } from '../assets/axis_bottom'; import { EuiIconAxisLeft } from '../assets/axis_left'; @@ -149,15 +149,13 @@ export const AxisSettingsPopover: React.FunctionComponent { - const [title, setTitle] = useState(axisTitle); - const isHorizontal = layers?.length ? isHorizontalChart(layers) : false; const config = popoverConfig(axis, isHorizontal); - const onTitleChange = (value: string): void => { - setTitle(value); - updateTitleState(value); - }; + const { inputValue: title, handleInputChange: onTitleChange } = useDebouncedValue({ + value: axisTitle || '', + onChange: updateTitleState, + }); return ( Date: Tue, 18 May 2021 12:33:16 +0200 Subject: [PATCH 096/186] Migrate from Joi to @kbn/config-schema in "home" and "features" plugins (#100201) * add a link for issue to remove circular deps * features: migrate from joi to config-schema * update tests * migrate home tutorials to config-schema * migrate home dataset validation to config schema * remove unnecessary type. we cannot guarantee this is a valid SO * address Pierres comments --- src/plugins/home/server/services/index.ts | 1 - .../lib/sample_dataset_registry_types.ts | 56 +---- .../sample_data/lib/sample_dataset_schema.ts | 92 ++++--- .../services/sample_data/routes/install.ts | 10 +- .../sample_data/sample_data_registry.ts | 9 +- .../home/server/services/tutorials/index.ts | 1 - .../services/tutorials/lib/tutorial_schema.ts | 220 +++++++++-------- .../tutorials/lib/tutorials_registry_types.ts | 90 +------ .../tutorials/tutorials_registry.test.ts | 2 +- .../services/tutorials/tutorials_registry.ts | 9 +- .../usage_collector/get_usage_collector.ts | 4 +- x-pack/plugins/apm/server/plugin.ts | 1 + .../feature_registry.test.ts.snap | 20 +- .../server/feature_privilege_iterator.js | 1 + .../features/server/feature_registry.test.ts | 33 +-- .../plugins/features/server/feature_schema.ts | 226 +++++++++++------- 16 files changed, 390 insertions(+), 385 deletions(-) diff --git a/src/plugins/home/server/services/index.ts b/src/plugins/home/server/services/index.ts index 7f26c886ab4b6..5674a3501f064 100644 --- a/src/plugins/home/server/services/index.ts +++ b/src/plugins/home/server/services/index.ts @@ -15,7 +15,6 @@ export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials export { TutorialsCategory } from './tutorials'; export type { - ParamTypes, InstructionSetSchema, ParamsSchema, InstructionsSchema, diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts index 4d9dc3885e67d..09af7728f74d2 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_registry_types.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { SavedObject } from 'src/core/server'; +import type { SampleDatasetSchema } from './sample_dataset_schema'; +export type { SampleDatasetSchema, AppLinkSchema, DataIndexSchema } from './sample_dataset_schema'; export enum DatasetStatusTypes { NOT_INSTALLED = 'not_installed', @@ -26,57 +27,4 @@ export enum EmbeddableTypes { SEARCH_EMBEDDABLE_TYPE = 'search', VISUALIZE_EMBEDDABLE_TYPE = 'visualization', } -export interface DataIndexSchema { - id: string; - - // path to newline delimented JSON file containing data relative to KIBANA_HOME - dataPath: string; - - // Object defining Elasticsearch field mappings (contents of index.mappings.type.properties) - fields: object; - - // times fields that will be updated relative to now when data is installed - timeFields: string[]; - - // Reference to now in your test data set. - // When data is installed, timestamps are converted to the present time. - // The distance between a timestamp and currentTimeMarker is preserved but the date and time will change. - // For example: - // sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z - // installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z - currentTimeMarker: string; - - // Set to true to move timestamp to current week, preserving day of week and time of day - // Relative distance from timestamp to currentTimeMarker will not remain the same - preserveDayOfWeekTimeOfDay: boolean; -} - -export interface AppLinkSchema { - path: string; - icon: string; - label: string; -} - -export interface SampleDatasetSchema { - id: string; - name: string; - description: string; - previewImagePath: string; - darkPreviewImagePath: string; - - // saved object id of main dashboard for sample data set - overviewDashboard: string; - appLinks: AppLinkSchema[]; - - // saved object id of default index-pattern for sample data set - defaultIndex: string; - - // Kibana saved objects (index patter, visualizations, dashboard, ...) - // Should provide a nice demo of Kibana's functionality with the sample data set - savedObjects: Array>; - dataIndices: DataIndexSchema[]; - status?: string | undefined; - statusMsg?: unknown; -} - export type SampleDatasetProvider = () => SampleDatasetSchema; diff --git a/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts b/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts index eb0b2252774b5..3c1764b2b8df1 100644 --- a/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts +++ b/src/plugins/home/server/services/sample_data/lib/sample_dataset_schema.ts @@ -5,22 +5,27 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import type { Writable } from '@kbn/utility-types'; +import { schema, TypeOf } from '@kbn/config-schema'; -import Joi from 'joi'; - -const dataIndexSchema = Joi.object({ - id: Joi.string() - .regex(/^[a-zA-Z0-9-]+$/) - .required(), +const idRegExp = /^[a-zA-Z0-9-]+$/; +const dataIndexSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!idRegExp.test(value)) { + return `Does not satisfy regexp: ${idRegExp.toString()}`; + } + }, + }), // path to newline delimented JSON file containing data relative to KIBANA_HOME - dataPath: Joi.string().required(), + dataPath: schema.string(), // Object defining Elasticsearch field mappings (contents of index.mappings.type.properties) - fields: Joi.object().required(), + fields: schema.recordOf(schema.string(), schema.any()), // times fields that will be updated relative to now when data is installed - timeFields: Joi.array().items(Joi.string()).required(), + timeFields: schema.arrayOf(schema.string()), // Reference to now in your test data set. // When data is installed, timestamps are converted to the present time. @@ -28,37 +33,66 @@ const dataIndexSchema = Joi.object({ // For example: // sample data set: timestamp: 2018-01-01T00:00:00Z, currentTimeMarker: 2018-01-01T12:00:00Z // installed data set: timestamp: 2018-04-18T20:33:14Z, currentTimeMarker: 2018-04-19T08:33:14Z - currentTimeMarker: Joi.string().isoDate().required(), + currentTimeMarker: schema.string({ + validate(value: string) { + if (isNaN(Date.parse(value))) { + return 'Expected a valid string in iso format'; + } + }, + }), // Set to true to move timestamp to current week, preserving day of week and time of day // Relative distance from timestamp to currentTimeMarker will not remain the same - preserveDayOfWeekTimeOfDay: Joi.boolean().default(false), + preserveDayOfWeekTimeOfDay: schema.boolean({ defaultValue: false }), }); -const appLinkSchema = Joi.object({ - path: Joi.string().required(), - label: Joi.string().required(), - icon: Joi.string().required(), +export type DataIndexSchema = TypeOf; + +const appLinkSchema = schema.object({ + path: schema.string(), + label: schema.string(), + icon: schema.string(), }); +export type AppLinkSchema = TypeOf; -export const sampleDataSchema = { - id: Joi.string() - .regex(/^[a-zA-Z0-9-]+$/) - .required(), - name: Joi.string().required(), - description: Joi.string().required(), - previewImagePath: Joi.string().required(), - darkPreviewImagePath: Joi.string(), +export const sampleDataSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!idRegExp.test(value)) { + return `Does not satisfy regexp: ${idRegExp.toString()}`; + } + }, + }), + name: schema.string(), + description: schema.string(), + previewImagePath: schema.string(), + darkPreviewImagePath: schema.maybe(schema.string()), // saved object id of main dashboard for sample data set - overviewDashboard: Joi.string().required(), - appLinks: Joi.array().items(appLinkSchema).default([]), + overviewDashboard: schema.string(), + appLinks: schema.arrayOf(appLinkSchema, { defaultValue: [] }), // saved object id of default index-pattern for sample data set - defaultIndex: Joi.string().required(), + defaultIndex: schema.string(), // Kibana saved objects (index patter, visualizations, dashboard, ...) // Should provide a nice demo of Kibana's functionality with the sample data set - savedObjects: Joi.array().items(Joi.object()).required(), - dataIndices: Joi.array().items(dataIndexSchema).required(), -}; + savedObjects: schema.arrayOf( + schema.object( + { + id: schema.string(), + type: schema.string(), + attributes: schema.any(), + references: schema.arrayOf(schema.any()), + version: schema.maybe(schema.any()), + }, + { unknowns: 'allow' } + ) + ), + dataIndices: schema.arrayOf(dataIndexSchema), + + status: schema.maybe(schema.string()), + statusMsg: schema.maybe(schema.string()), +}); + +export type SampleDatasetSchema = Writable>; diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index e5ff33d5c199d..d0457f0a6d301 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -7,7 +7,12 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, Logger, IScopedClusterClient } from 'src/core/server'; +import type { + IRouter, + Logger, + IScopedClusterClient, + SavedObjectsBulkCreateObject, +} from 'src/core/server'; import { SampleDatasetSchema } from '../lib/sample_dataset_registry_types'; import { createIndexName } from '../lib/create_index_name'; import { @@ -148,8 +153,9 @@ export function createInstallRoute( const client = getClient({ includedHiddenTypes }); + const savedObjects = sampleDataset.savedObjects as SavedObjectsBulkCreateObject[]; createResults = await client.bulkCreate( - sampleDataset.savedObjects.map(({ version, ...savedObject }) => savedObject), + savedObjects.map(({ version, ...savedObject }) => savedObject), { overwrite: true } ); } catch (err) { diff --git a/src/plugins/home/server/services/sample_data/sample_data_registry.ts b/src/plugins/home/server/services/sample_data/sample_data_registry.ts index ca75d20dc1d3f..dff0d86409974 100644 --- a/src/plugins/home/server/services/sample_data/sample_data_registry.ts +++ b/src/plugins/home/server/services/sample_data/sample_data_registry.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import Joi from 'joi'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { SavedObject } from 'src/core/public'; import { @@ -55,11 +54,13 @@ export class SampleDataRegistry { return { registerSampleDataset: (specProvider: SampleDatasetProvider) => { - const { error, value } = Joi.validate(specProvider(), sampleDataSchema); - - if (error) { + let value: SampleDatasetSchema; + try { + value = sampleDataSchema.validate(specProvider()); + } catch (error) { throw new Error(`Unable to register sample dataset spec because it's invalid. ${error}`); } + const defaultIndexSavedObjectJson = value.savedObjects.find((savedObjectJson: any) => { return ( savedObjectJson.type === 'index-pattern' && savedObjectJson.id === value.defaultIndex diff --git a/src/plugins/home/server/services/tutorials/index.ts b/src/plugins/home/server/services/tutorials/index.ts index 92f6de716185d..f745d0190efd5 100644 --- a/src/plugins/home/server/services/tutorials/index.ts +++ b/src/plugins/home/server/services/tutorials/index.ts @@ -12,7 +12,6 @@ export type { TutorialsRegistrySetup, TutorialsRegistryStart } from './tutorials export { TutorialsCategory } from './lib/tutorials_registry_types'; export type { - ParamTypes, InstructionSetSchema, ParamsSchema, InstructionsSchema, diff --git a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts index 0f06b6c3257c2..5efbe067f6ece 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts @@ -5,121 +5,153 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { schema, TypeOf } from '@kbn/config-schema'; -import Joi from 'joi'; - -const PARAM_TYPES = { - NUMBER: 'number', - STRING: 'string', -}; - -const TUTORIAL_CATEGORY = { - LOGGING: 'logging', - SECURITY_SOLUTION: 'security solution', - METRICS: 'metrics', - OTHER: 'other', -}; - -const dashboardSchema = Joi.object({ - id: Joi.string().required(), // Dashboard saved object id - linkLabel: Joi.string().when('isOverview', { - is: true, - then: Joi.required(), - }), +const dashboardSchema = schema.object({ + // Dashboard saved object id + id: schema.string(), // Is this an Overview / Entry Point dashboard? - isOverview: Joi.boolean().required(), + isOverview: schema.boolean(), + linkLabel: schema.conditional( + schema.siblingRef('isOverview'), + true, + schema.string(), + schema.maybe(schema.string()) + ), }); +export type DashboardSchema = TypeOf; -const artifactsSchema = Joi.object({ +const artifactsSchema = schema.object({ // Fields present in Elasticsearch documents created by this product. - exportedFields: Joi.object({ - documentationUrl: Joi.string().required(), - }), + exportedFields: schema.maybe( + schema.object({ + documentationUrl: schema.string(), + }) + ), // Kibana dashboards created by this product. - dashboards: Joi.array().items(dashboardSchema).required(), - application: Joi.object({ - path: Joi.string().required(), - label: Joi.string().required(), - }), + dashboards: schema.arrayOf(dashboardSchema), + application: schema.maybe( + schema.object({ + path: schema.string(), + label: schema.string(), + }) + ), }); - -const statusCheckSchema = Joi.object({ - title: Joi.string(), - text: Joi.string(), - btnLabel: Joi.string(), - success: Joi.string(), - error: Joi.string(), - esHitsCheck: Joi.object({ - index: Joi.alternatives().try(Joi.string(), Joi.array().items(Joi.string())).required(), - query: Joi.object().required(), - }).required(), +export type ArtifactsSchema = TypeOf; + +const statusCheckSchema = schema.object({ + title: schema.maybe(schema.string()), + text: schema.maybe(schema.string()), + btnLabel: schema.maybe(schema.string()), + success: schema.maybe(schema.string()), + error: schema.maybe(schema.string()), + esHitsCheck: schema.object({ + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + query: schema.recordOf(schema.string(), schema.any()), + }), }); -const instructionSchema = Joi.object({ - title: Joi.string(), - textPre: Joi.string(), - commands: Joi.array().items(Joi.string().allow('')), - textPost: Joi.string(), +const instructionSchema = schema.object({ + title: schema.maybe(schema.string()), + textPre: schema.maybe(schema.string()), + commands: schema.maybe(schema.arrayOf(schema.string())), + textPost: schema.maybe(schema.string()), }); +export type Instruction = TypeOf; -const instructionVariantSchema = Joi.object({ - id: Joi.string().required(), - instructions: Joi.array().items(instructionSchema).required(), +const instructionVariantSchema = schema.object({ + id: schema.string(), + instructions: schema.arrayOf(instructionSchema), }); -const instructionSetSchema = Joi.object({ - title: Joi.string(), - callOut: Joi.object({ - title: Joi.string().required(), - message: Joi.string(), - iconType: Joi.string(), - }), +export type InstructionVariant = TypeOf; + +const instructionSetSchema = schema.object({ + title: schema.maybe(schema.string()), + callOut: schema.maybe( + schema.object({ + title: schema.string(), + message: schema.maybe(schema.string()), + iconType: schema.maybe(schema.string()), + }) + ), // Variants (OSes, languages, etc.) for which tutorial instructions are specified. - instructionVariants: Joi.array().items(instructionVariantSchema).required(), - statusCheck: statusCheckSchema, + instructionVariants: schema.arrayOf(instructionVariantSchema), + statusCheck: schema.maybe(statusCheckSchema), }); - -const paramSchema = Joi.object({ - defaultValue: Joi.required(), - id: Joi.string() - .regex(/^[a-zA-Z_]+$/) - .required(), - label: Joi.string().required(), - type: Joi.string().valid(Object.values(PARAM_TYPES)).required(), +export type InstructionSetSchema = TypeOf; + +const idRegExp = /^[a-zA-Z_]+$/; +const paramSchema = schema.object({ + defaultValue: schema.any(), + id: schema.string({ + validate(value: string) { + if (!idRegExp.test(value)) { + return `Does not satisfy regexp ${idRegExp.toString()}`; + } + }, + }), + label: schema.string(), + type: schema.oneOf([schema.literal('number'), schema.literal('string')]), }); +export type ParamsSchema = TypeOf; -const instructionsSchema = Joi.object({ - instructionSets: Joi.array().items(instructionSetSchema).required(), - params: Joi.array().items(paramSchema), +const instructionsSchema = schema.object({ + instructionSets: schema.arrayOf(instructionSetSchema), + params: schema.maybe(schema.arrayOf(paramSchema)), }); - -export const tutorialSchema = { - id: Joi.string() - .regex(/^[a-zA-Z0-9-]+$/) - .required(), - category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(), - name: Joi.string().required(), - moduleName: Joi.string(), - isBeta: Joi.boolean().default(false), - shortDescription: Joi.string().required(), - euiIconType: Joi.string(), // EUI icon type string, one of https://elastic.github.io/eui/#/icons - longDescription: Joi.string().required(), - completionTimeMinutes: Joi.number().integer(), - previewImagePath: Joi.string(), - +export type InstructionsSchema = TypeOf; + +const tutorialIdRegExp = /^[a-zA-Z0-9-]+$/; +export const tutorialSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!tutorialIdRegExp.test(value)) { + return `Does not satisfy regexp ${tutorialIdRegExp.toString()}`; + } + }, + }), + category: schema.oneOf([ + schema.literal('logging'), + schema.literal('security'), + schema.literal('metrics'), + schema.literal('other'), + ]), + name: schema.string({ + validate(value: string) { + if (value === '') { + return 'is not allowed to be empty'; + } + }, + }), + moduleName: schema.maybe(schema.string()), + isBeta: schema.maybe(schema.boolean()), + shortDescription: schema.string(), + // EUI icon type string, one of https://elastic.github.io/eui/#/icons + euiIconType: schema.maybe(schema.string()), + longDescription: schema.string(), + completionTimeMinutes: schema.maybe( + schema.number({ + validate(value: number) { + if (!Number.isInteger(value)) { + return 'Expected to be a valid integer number'; + } + }, + }) + ), + previewImagePath: schema.maybe(schema.string()), // kibana and elastic cluster running on prem - onPrem: instructionsSchema.required(), - + onPrem: instructionsSchema, // kibana and elastic cluster running in elastic's cloud - elasticCloud: instructionsSchema, - + elasticCloud: schema.maybe(instructionsSchema), // kibana running on prem and elastic cluster running in elastic's cloud - onPremElasticCloud: instructionsSchema, - + onPremElasticCloud: schema.maybe(instructionsSchema), // Elastic stack artifacts produced by product when it is setup and run. - artifacts: artifactsSchema, + artifacts: schema.maybe(artifactsSchema), // saved objects used by data module. - savedObjects: Joi.array().items(), - savedObjectsInstallMsg: Joi.string(), -}; + savedObjects: schema.maybe(schema.arrayOf(schema.any())), + savedObjectsInstallMsg: schema.maybe(schema.string()), +}); + +export type TutorialSchema = TypeOf; diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index b0837a99d65ad..4c80c8858a475 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -6,8 +6,18 @@ * Side Public License, v 1. */ -import { IconType } from '@elastic/eui'; -import { KibanaRequest } from 'src/core/server'; +import type { KibanaRequest } from 'src/core/server'; +import type { TutorialSchema } from './tutorial_schema'; +export type { + TutorialSchema, + ArtifactsSchema, + DashboardSchema, + InstructionsSchema, + ParamsSchema, + InstructionSetSchema, + InstructionVariant, + Instruction, +} from './tutorial_schema'; /** @public */ export enum TutorialsCategory { @@ -18,82 +28,6 @@ export enum TutorialsCategory { } export type Platform = 'WINDOWS' | 'OSX' | 'DEB' | 'RPM'; -export interface ParamTypes { - NUMBER: string; - STRING: string; -} -export interface Instruction { - title?: string; - textPre?: string; - commands?: string[]; - textPost?: string; -} -export interface InstructionVariant { - id: string; - instructions: Instruction[]; -} -export interface InstructionSetSchema { - readonly title?: string; - readonly callOut?: { - title: string; - message?: string; - iconType?: IconType; - }; - instructionVariants: InstructionVariant[]; -} -export interface ParamsSchema { - defaultValue: any; - id: string; - label: string; - type: ParamTypes; -} -export interface InstructionsSchema { - readonly instructionSets: InstructionSetSchema[]; - readonly params?: ParamsSchema[]; -} -export interface DashboardSchema { - id: string; - linkLabel?: string; - isOverview: boolean; -} -export interface ArtifactsSchema { - exportedFields?: { - documentationUrl: string; - }; - dashboards: DashboardSchema[]; - application?: { - path: string; - label: string; - }; -} -export interface TutorialSchema { - id: string; - category: TutorialsCategory; - name: string; - moduleName?: string; - isBeta?: boolean; - shortDescription: string; - euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/display/icons; - longDescription: string; - completionTimeMinutes?: number; - previewImagePath?: string; - - // kibana and elastic cluster running on prem - onPrem: InstructionsSchema; - - // kibana and elastic cluster running in elastic's cloud - elasticCloud?: InstructionsSchema; - - // kibana running on prem and elastic cluster running in elastic's cloud - onPremElasticCloud?: InstructionsSchema; - - // Elastic stack artifacts produced by product when it is setup and run. - artifacts?: ArtifactsSchema; - - // saved objects used by data module. - savedObjects?: any[]; - savedObjectsInstallMsg?: string; -} export interface TutorialContext { [key: string]: unknown; } diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index 94f5d65610083..a82699c231ad4 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -92,7 +92,7 @@ describe('TutorialsRegistry', () => { const setup = new TutorialsRegistry().setup(mockCoreSetup); testProvider = ({}) => invalidTutorialProvider; expect(() => setup.registerTutorial(testProvider)).toThrowErrorMatchingInlineSnapshot( - `"Unable to register tutorial spec because its invalid. ValidationError: child \\"name\\" fails because [\\"name\\" is not allowed to be empty]"` + `"Unable to register tutorial spec because its invalid. Error: [name]: is not allowed to be empty"` ); }); diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.ts index f21f2ccd719c5..05f5600af307a 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import Joi from 'joi'; import { CoreSetup } from 'src/core/server'; import { TutorialProvider, @@ -42,10 +41,10 @@ export class TutorialsRegistry { ); return { registerTutorial: (specProvider: TutorialProvider) => { - const emptyContext = {}; - const { error } = Joi.validate(specProvider(emptyContext), tutorialSchema); - - if (error) { + try { + const emptyContext = {}; + tutorialSchema.validate(specProvider(emptyContext)); + } catch (error) { throw new Error(`Unable to register tutorial spec because its invalid. ${error}`); } diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts index d5f8d978d5252..310486bfdfffd 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts @@ -7,7 +7,7 @@ */ import { parse } from 'hjson'; -import { ElasticsearchClient, SavedObject } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; @@ -27,7 +27,7 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home']) const sampleDataSets = home?.sampleData.getSampleDatasets() ?? []; sampleDataSets.forEach((sampleDataSet) => - sampleDataSet.savedObjects.forEach((savedObject: SavedObject) => { + sampleDataSet.savedObjects.forEach((savedObject) => { try { if (savedObject.type === 'visualization') { const visState = JSON.parse(savedObject.attributes?.visState); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 44334889128c4..cf5be4369f79e 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -108,6 +108,7 @@ export class APMPlugin plugins.home?.tutorials.registerTutorial(() => { const ossPart = ossTutorialProvider({}); if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) { + // @ts-expect-error ossPart.artifacts.application is readonly ossPart.artifacts.application = { path: '/app/apm', label: i18n.translate( diff --git a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap index 3043de27534f1..c91a891d4825f 100644 --- a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap @@ -1,21 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "" 1`] = `"[catalogue.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains space" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains space" 1`] = `"[catalogue.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"[catalogue.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "" 1`] = `"[management.kibana.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains space" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains space" 1`] = `"[management.kibana.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"[management.kibana.0]: Does not satisfy regexp /^[a-zA-Z0-9:_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"[id]: [catalogue] is not allowed"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"[id]: Does not satisfy regexp /^[a-zA-Z0-9_-]+$/"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "management" 1`] = `"[id]: [management] is not allowed"`; -exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "navLinks" 1`] = `"[id]: [navLinks] is not allowed"`; diff --git a/x-pack/plugins/features/server/feature_privilege_iterator.js b/x-pack/plugins/features/server/feature_privilege_iterator.js index f813eeebc9550..842c30d643b67 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator.js +++ b/x-pack/plugins/features/server/feature_privilege_iterator.js @@ -6,5 +6,6 @@ */ // the file created to remove TS cicular dependency between features and security pluin +// https://github.com/elastic/kibana/issues/87388 // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { featurePrivilegeIterator } from '../../security/server/authorization'; diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 827773c7d7c5a..8e7ed45f33f50 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -158,7 +158,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerKibanaFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"category\\" fails because [\\"category\\" is required]"` + `"[category.id]: expected value of type [string] but got [undefined]"` ); }); @@ -175,7 +175,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerKibanaFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"category\\" fails because [child \\"id\\" fails because [\\"id\\" is required]]"` + `"[category.id]: expected value of type [string] but got [undefined]"` ); }); @@ -192,7 +192,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerKibanaFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"category\\" fails because [child \\"label\\" fails because [\\"label\\" is required]]"` + `"[category.label]: expected value of type [string] but got [undefined]"` ); }); }); @@ -209,7 +209,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerKibanaFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` + `"[privileges]: expected at least one defined value but got [undefined]"` ); }); @@ -248,7 +248,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerKibanaFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"` + `"[subFeatures]: array size is [1], but cannot be greater than [0]"` ); }); @@ -488,11 +488,12 @@ describe('FeatureRegistry', () => { }; const featureRegistry = new FeatureRegistry(); - expect(() => - featureRegistry.registerKibanaFeature(feature) - ).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` - ); + expect(() => featureRegistry.registerKibanaFeature(feature)) + .toThrowErrorMatchingInlineSnapshot(` + "[privileges]: types that failed validation: + - [privileges.0]: expected value to equal [null] + - [privileges.1.foo]: definition for this key is missing" + `); }); it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { @@ -1504,7 +1505,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerKibanaFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` + `"[reserved.privileges.0.id]: Does not satisfy regexp /^(?!reserved_)[a-zA-Z0-9_-]+$/"` ); }); @@ -1620,9 +1621,11 @@ describe('FeatureRegistry', () => { const featureRegistry = new FeatureRegistry(); expect(() => { featureRegistry.registerKibanaFeature(feature1); - }).toThrowErrorMatchingInlineSnapshot( - `"child \\"subFeatures\\" fails because [\\"subFeatures\\" at position 0 fails because [child \\"privilegeGroups\\" fails because [\\"privilegeGroups\\" at position 0 fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"minimumLicense\\" fails because [\\"minimumLicense\\" is not allowed]]]]]]]"` - ); + }).toThrowErrorMatchingInlineSnapshot(` + "[subFeatures.0.privilegeGroups.0]: types that failed validation: + - [subFeatures.0.privilegeGroups.0.0.privileges.0.minimumLicense]: a value wasn't expected to be present + - [subFeatures.0.privilegeGroups.0.1.groupType]: expected value to equal [independent]" + `); }); it('cannot register feature after getAll has been called', () => { @@ -1801,7 +1804,7 @@ describe('FeatureRegistry', () => { expect(() => featureRegistry.registerElasticsearchFeature(feature) ).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` + `"[privileges]: expected value of type [array] but got [undefined]"` ); }); diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 4cb6c779d2465..276d3405d7da5 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -5,7 +5,7 @@ * 2.0. */ -import Joi from 'joi'; +import { schema } from '@kbn/config-schema'; import { difference } from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; @@ -14,7 +14,11 @@ import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.'; // Each feature gets its own property on the UICapabilities object, // but that object has a few built-in properties which should not be overwritten. -const prohibitedFeatureIds: Array = ['catalogue', 'management', 'navLinks']; +const prohibitedFeatureIds: Set = new Set([ + 'catalogue', + 'management', + 'navLinks', +]); const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; const subFeaturePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; @@ -22,14 +26,40 @@ const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/; export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/; -const validLicenses = ['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial']; +const validLicenseSchema = schema.oneOf([ + schema.literal('basic'), + schema.literal('standard'), + schema.literal('gold'), + schema.literal('platinum'), + schema.literal('enterprise'), + schema.literal('trial'), +]); // sub-feature privileges are only available with a `gold` license or better, so restricting sub-feature privileges // for `gold` or below doesn't make a whole lot of sense. -const validSubFeaturePrivilegeLicenses = ['platinum', 'enterprise', 'trial']; - -const managementSchema = Joi.object().pattern( - managementSectionIdRegex, - Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)) +const validSubFeaturePrivilegeLicensesSchema = schema.oneOf([ + schema.literal('platinum'), + schema.literal('enterprise'), + schema.literal('trial'), +]); + +const listOfCapabilitiesSchema = schema.arrayOf( + schema.string({ + validate(key: string) { + if (!uiCapabilitiesRegex.test(key)) { + return `Does not satisfy regexp ${uiCapabilitiesRegex.toString()}`; + } + }, + }) +); +const managementSchema = schema.recordOf( + schema.string({ + validate(key: string) { + if (!managementSectionIdRegex.test(key)) { + return `Does not satisfy regexp ${managementSectionIdRegex.toString()}`; + } + }, + }), + listOfCapabilitiesSchema ); const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const alertingSchema = Joi.array().items(Joi.string()); @@ -58,11 +88,7 @@ const kibanaPrivilegeSchema = Joi.object({ read: alertingSchema, }, }), - savedObject: Joi.object({ - all: Joi.array().items(Joi.string()).required(), - read: Joi.array().items(Joi.string()).required(), - }).required(), - ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), + ui: listOfCapabilitiesSchema, }); const kibanaIndependentSubFeaturePrivilegeSchema = Joi.object({ @@ -82,96 +108,121 @@ const kibanaIndependentSubFeaturePrivilegeSchema = Joi.object({ read: alertingSchema, }, }), - api: Joi.array().items(Joi.string()), - app: Joi.array().items(Joi.string()), - savedObject: Joi.object({ - all: Joi.array().items(Joi.string()).required(), - read: Joi.array().items(Joi.string()).required(), - }).required(), - ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), + ui: listOfCapabilitiesSchema, }); -const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema = kibanaIndependentSubFeaturePrivilegeSchema.keys( +const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema = kibanaIndependentSubFeaturePrivilegeSchema.extends( { - minimumLicense: Joi.forbidden(), + minimumLicense: schema.never(), } ); -const kibanaSubFeatureSchema = Joi.object({ - name: Joi.string().required(), - privilegeGroups: Joi.array().items( - Joi.object({ - groupType: Joi.string().valid('mutually_exclusive', 'independent').required(), - privileges: Joi.when('groupType', { - is: 'mutually_exclusive', - then: Joi.array().items(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema).min(1), - otherwise: Joi.array().items(kibanaIndependentSubFeaturePrivilegeSchema).min(1), - }), - }) +const kibanaSubFeatureSchema = schema.object({ + name: schema.string(), + privilegeGroups: schema.maybe( + schema.arrayOf( + schema.oneOf([ + schema.object({ + groupType: schema.literal('mutually_exclusive'), + privileges: schema.maybe( + schema.arrayOf(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema, { minSize: 1 }) + ), + }), + schema.object({ + groupType: schema.literal('independent'), + privileges: schema.maybe( + schema.arrayOf(kibanaIndependentSubFeaturePrivilegeSchema, { minSize: 1 }) + ), + }), + ]) + ) ), }); -const kibanaFeatureSchema = Joi.object({ - id: Joi.string() - .regex(featurePrivilegePartRegex) - .invalid(...prohibitedFeatureIds) - .required(), - name: Joi.string().required(), - category: appCategorySchema, - order: Joi.number(), - excludeFromBasePrivileges: Joi.boolean(), - minimumLicense: Joi.string().valid(...validLicenses), - app: Joi.array().items(Joi.string()).required(), - management: managementSchema, - catalogue: catalogueSchema, - alerting: alertingSchema, - privileges: Joi.object({ - all: kibanaPrivilegeSchema, - read: kibanaPrivilegeSchema, - }) - .allow(null) - .required(), - subFeatures: Joi.when('privileges', { - is: null, - then: Joi.array().items(kibanaSubFeatureSchema).max(0), - otherwise: Joi.array().items(kibanaSubFeatureSchema), +const kibanaFeatureSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!featurePrivilegePartRegex.test(value)) { + return `Does not satisfy regexp ${featurePrivilegePartRegex.toString()}`; + } + if (prohibitedFeatureIds.has(value)) { + return `[${value}] is not allowed`; + } + }, }), - privilegesTooltip: Joi.string(), - reserved: Joi.object({ - description: Joi.string().required(), - privileges: Joi.array() - .items( - Joi.object({ - id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(), - privilege: kibanaPrivilegeSchema.required(), + name: schema.string(), + category: appCategorySchema, + order: schema.maybe(schema.number()), + excludeFromBasePrivileges: schema.maybe(schema.boolean()), + minimumLicense: schema.maybe(validLicenseSchema), + app: schema.arrayOf(schema.string()), + management: schema.maybe(managementSchema), + catalogue: schema.maybe(catalogueSchema), + alerting: schema.maybe(alertingSchema), + privileges: schema.oneOf([ + schema.literal(null), + schema.object({ + all: schema.maybe(kibanaPrivilegeSchema), + read: schema.maybe(kibanaPrivilegeSchema), + }), + ]), + subFeatures: schema.maybe( + schema.conditional( + schema.siblingRef('privileges'), + null, + // allows an empty array only + schema.arrayOf(schema.never(), { maxSize: 0 }), + schema.arrayOf(kibanaSubFeatureSchema) + ) + ), + privilegesTooltip: schema.maybe(schema.string()), + reserved: schema.maybe( + schema.object({ + description: schema.string(), + privileges: schema.arrayOf( + schema.object({ + id: schema.string({ + validate(value: string) { + if (!reservedFeaturePrrivilegePartRegex.test(value)) { + return `Does not satisfy regexp ${reservedFeaturePrrivilegePartRegex.toString()}`; + } + }, + }), + privilege: kibanaPrivilegeSchema, }) - ) - .required(), - }), + ), + }) + ), }); -const elasticsearchPrivilegeSchema = Joi.object({ - ui: Joi.array().items(Joi.string()).required(), - requiredClusterPrivileges: Joi.array().items(Joi.string()), - requiredIndexPrivileges: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), - requiredRoles: Joi.array().items(Joi.string()), +const elasticsearchPrivilegeSchema = schema.object({ + ui: schema.arrayOf(schema.string()), + requiredClusterPrivileges: schema.maybe(schema.arrayOf(schema.string())), + requiredIndexPrivileges: schema.maybe( + schema.recordOf(schema.string(), schema.arrayOf(schema.string())) + ), + requiredRoles: schema.maybe(schema.arrayOf(schema.string())), }); -const elasticsearchFeatureSchema = Joi.object({ - id: Joi.string() - .regex(featurePrivilegePartRegex) - .invalid(...prohibitedFeatureIds) - .required(), - management: managementSchema, - catalogue: catalogueSchema, - privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(), +const elasticsearchFeatureSchema = schema.object({ + id: schema.string({ + validate(value: string) { + if (!featurePrivilegePartRegex.test(value)) { + return `Does not satisfy regexp ${featurePrivilegePartRegex.toString()}`; + } + if (prohibitedFeatureIds.has(value)) { + return `[${value}] is not allowed`; + } + }, + }), + management: schema.maybe(managementSchema), + catalogue: schema.maybe(catalogueSchema), + privileges: schema.arrayOf(elasticsearchPrivilegeSchema), }); export function validateKibanaFeature(feature: KibanaFeatureConfig) { - const validateResult = Joi.validate(feature, kibanaFeatureSchema); - if (validateResult.error) { - throw validateResult.error; - } + kibanaFeatureSchema.validate(feature); + // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. const { app = [], management = {}, catalogue = [], alerting = [] } = feature; @@ -355,10 +406,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { } export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) { - const validateResult = Joi.validate(feature, elasticsearchFeatureSchema); - if (validateResult.error) { - throw validateResult.error; - } + elasticsearchFeatureSchema.validate(feature); // the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition const { privileges } = feature; privileges.forEach((privilege, index) => { From 60fa2597dfefee3e42a907f9c34305a7159dbb86 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 18 May 2021 12:36:35 +0200 Subject: [PATCH 097/186] [Lens] Specify Y axis extent (#99203) --- .../__snapshots__/expression.test.tsx.snap | 49 ++++ .../__snapshots__/to_expression.test.ts.snap | 40 +++ .../xy_visualization/axes_configuration.ts | 18 +- .../axis_settings_popover.test.tsx | 23 ++ .../axis_settings_popover.tsx | 227 +++++++++++++++++- .../xy_visualization/expression.test.tsx | 140 +++++++++++ .../public/xy_visualization/expression.tsx | 82 +++++-- .../lens/public/xy_visualization/index.ts | 2 + .../xy_visualization/to_expression.test.ts | 5 + .../public/xy_visualization/to_expression.ts | 44 ++++ .../lens/public/xy_visualization/types.ts | 52 ++++ .../xy_visualization/xy_config_panel.test.tsx | 84 +++++++ .../xy_visualization/xy_config_panel.tsx | 92 ++++++- .../public/xy_visualization/xy_suggestions.ts | 3 + 14 files changed, 834 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index aa22bbb0c15c6..f9b4e33072c81 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -52,6 +52,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` title="c" /> 0) || + (extent.upperBound !== undefined && extent.upperBound) < 0); + const boundaryError = + extent && + extent.lowerBound !== undefined && + extent.upperBound !== undefined && + extent.upperBound <= extent.lowerBound; + return { inclusiveZeroError, boundaryError }; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx index 48cc45dfacdca..047c95846cd27 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx @@ -32,6 +32,7 @@ describe('Axes Settings', () => { toggleAxisTitleVisibility: jest.fn(), toggleTickLabelsVisibility: jest.fn(), toggleGridlinesVisibility: jest.fn(), + hasBarOrAreaOnAxis: false, }; }); @@ -91,4 +92,26 @@ describe('Axes Settings', () => { ); expect(component.find('[data-test-subj="lnsshowEndzones"]').prop('checked')).toBe(true); }); + + describe('axis extent', () => { + it('hides the extent section if no extent is passed in', () => { + const component = shallow(); + expect(component.find('[data-test-subj="lnsXY_axisBounds_groups"]').length).toBe(0); + }); + + it('renders bound inputs if mode is custom', () => { + const setSpy = jest.fn(); + const component = shallow( + + ); + const lower = component.find('[data-test-subj="lnsXY_axisExtent_lowerBound"]'); + const upper = component.find('[data-test-subj="lnsXY_axisExtent_upperBound"]'); + expect(lower.prop('value')).toEqual(123); + expect(upper.prop('value')).toEqual(456); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx index d995e1a055e68..43ebc91f533a4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -14,9 +14,13 @@ import { EuiSpacer, EuiFieldText, IconType, + EuiFormRow, + EuiButtonGroup, + htmlIdGenerator, + EuiFieldNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { XYLayerConfig, AxesSettingsConfig } from './types'; +import { XYLayerConfig, AxesSettingsConfig, AxisExtentConfig } from './types'; import { ToolbarPopover, useDebouncedValue } from '../shared_components'; import { isHorizontalChart } from './state_helpers'; import { EuiIconAxisBottom } from '../assets/axis_bottom'; @@ -24,6 +28,7 @@ import { EuiIconAxisLeft } from '../assets/axis_left'; import { EuiIconAxisRight } from '../assets/axis_right'; import { EuiIconAxisTop } from '../assets/axis_top'; import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/public'; +import { validateExtent } from './axes_configuration'; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; export interface AxisSettingsPopoverProps { @@ -79,6 +84,16 @@ export interface AxisSettingsPopoverProps { * Flag whether endzones are visible */ endzonesVisible?: boolean; + /** + * axis extent + */ + extent?: AxisExtentConfig; + /** + * set axis extent + */ + setExtent?: (extent: AxisExtentConfig | undefined) => void; + hasBarOrAreaOnAxis: boolean; + dataBounds?: { min: number; max: number }; } const popoverConfig = ( axis: AxesSettingsConfigKeys, @@ -134,6 +149,8 @@ const popoverConfig = ( } }; +const noop = () => {}; +const idPrefix = htmlIdGenerator()(); export const AxisSettingsPopover: React.FunctionComponent = ({ layers, axis, @@ -148,10 +165,45 @@ export const AxisSettingsPopover: React.FunctionComponent { const isHorizontal = layers?.length ? isHorizontalChart(layers) : false; const config = popoverConfig(axis, isHorizontal); + const { inputValue: debouncedExtent, handleInputChange: setDebouncedExtent } = useDebouncedValue< + AxisExtentConfig | undefined + >({ + value: extent, + onChange: setExtent || noop, + }); + + const [localExtent, setLocalExtent] = useState(debouncedExtent); + + const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrAreaOnAxis, localExtent); + + useEffect(() => { + // set global extent if local extent is not invalid + if ( + setExtent && + !inclusiveZeroError && + !boundaryError && + localExtent && + localExtent !== debouncedExtent + ) { + setDebouncedExtent(localExtent); + } + }, [ + localExtent, + inclusiveZeroError, + boundaryError, + setDebouncedExtent, + debouncedExtent, + setExtent, + ]); + const { inputValue: title, handleInputChange: onTitleChange } = useDebouncedValue({ value: axisTitle || '', onChange: updateTitleState, @@ -234,6 +286,177 @@ export const AxisSettingsPopover: React.FunctionComponent )} + {localExtent && setExtent && ( + <> + + + { + const newMode = id.replace(idPrefix, '') as AxisExtentConfig['mode']; + setLocalExtent({ + ...localExtent, + mode: newMode, + lowerBound: + newMode === 'custom' && dataBounds ? Math.min(0, dataBounds.min) : undefined, + upperBound: newMode === 'custom' && dataBounds ? dataBounds.max : undefined, + }); + }} + /> + + {localExtent.mode === 'custom' && ( + <> + + + + + { + const val = Number(e.target.value); + if (e.target.value === '' || Number.isNaN(Number(val))) { + setLocalExtent({ + ...localExtent, + lowerBound: undefined, + }); + } else { + setLocalExtent({ + ...localExtent, + lowerBound: val, + }); + } + }} + onBlur={() => { + if (localExtent.lowerBound === undefined && dataBounds) { + setLocalExtent({ + ...localExtent, + lowerBound: Math.min(0, dataBounds.min), + }); + } + }} + /> + + + + + { + const val = Number(e.target.value); + if (e.target.value === '' || Number.isNaN(Number(val))) { + setLocalExtent({ + ...localExtent, + upperBound: undefined, + }); + } else { + setLocalExtent({ + ...localExtent, + upperBound: val, + }); + } + }} + onBlur={() => { + if (localExtent.upperBound === undefined && dataBounds) { + setLocalExtent({ + ...localExtent, + upperBound: dataBounds.max, + }); + } + }} + /> + + + + + )} + + )} ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index fe0513caa08a8..3fab88248d4a5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -283,6 +283,14 @@ const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({ yLeft: false, yRight: false, }, + yLeftExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, layers, }); @@ -681,6 +689,114 @@ describe('xy_expression', () => { }); }); + describe('y axis extents', () => { + test('it passes custom y axis extents to elastic-charts axis spec', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: 123, + max: 456, + }); + }); + + test('it passes fit to bounds y axis extents to elastic-charts axis spec', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: true, + min: undefined, + max: undefined, + }); + }); + + test('it does not allow fit for area chart', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + }); + }); + + test('it does not allow positive lower bound for bar', () => { + const { data, args } = sampleArgs(); + + const component = shallow( + + ); + expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ + fit: false, + min: undefined, + max: undefined, + }); + }); + }); + test('it has xDomain undefined if the x is not a time scale or a histogram', () => { const { data, args } = sampleArgs(); @@ -1761,6 +1877,14 @@ describe('xy_expression', () => { yLeft: false, yRight: false, }, + yLeftExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, layers: [ { layerId: 'first', @@ -1835,6 +1959,14 @@ describe('xy_expression', () => { yLeft: false, yRight: false, }, + yLeftExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, layers: [ { layerId: 'first', @@ -1895,6 +2027,14 @@ describe('xy_expression', () => { yLeft: false, yRight: false, }, + yLeftExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, + yRightExtent: { + mode: 'full', + type: 'lens_xy_axisExtentConfig', + }, layers: [ { layerId: 'first', diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 5416c8eda0aa9..006727b05b905 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -55,7 +55,7 @@ import { import { EmptyPlaceholder } from '../shared_components'; import { desanitizeFilterContext } from '../utils'; import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions'; -import { getAxesConfiguration } from './axes_configuration'; +import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration'; import { getColorAssignments } from './color_assignment'; import { getXDomain, XyEndzones } from './x_domain'; @@ -135,6 +135,18 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Y right axis title', }), }, + yLeftExtent: { + types: ['lens_xy_axisExtentConfig'], + help: i18n.translate('xpack.lens.xyChart.yLeftExtent.help', { + defaultMessage: 'Y left axis extents', + }), + }, + yRightExtent: { + types: ['lens_xy_axisExtentConfig'], + help: i18n.translate('xpack.lens.xyChart.yRightExtent.help', { + defaultMessage: 'Y right axis extents', + }), + }, legend: { types: ['lens_xy_legendConfig'], help: i18n.translate('xpack.lens.xyChart.legend.help', { @@ -345,6 +357,8 @@ export function XYChart({ gridlinesVisibilitySettings, valueLabels, hideEndzones, + yLeftExtent, + yRightExtent, } = args; const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); @@ -445,6 +459,33 @@ export function XYChart({ return style; }; + const getYAxisDomain = (axis: GroupsConfiguration[number]) => { + const extent = axis.groupId === 'left' ? yLeftExtent : yRightExtent; + const hasBarOrArea = Boolean( + axis.series.some((series) => { + const seriesType = filteredLayers.find((l) => l.layerId === series.layer)?.seriesType; + return seriesType?.includes('bar') || seriesType?.includes('area'); + }) + ); + const fit = !hasBarOrArea && extent.mode === 'dataBounds'; + let min: undefined | number; + let max: undefined | number; + + if (extent.mode === 'custom') { + const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent); + if (!inclusiveZeroError && !boundaryError) { + min = extent.lowerBound; + max = extent.upperBound; + } + } + + return { + fit, + min, + max, + }; + }; + const shouldShowValueLabels = // No stacked bar charts filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) && @@ -597,24 +638,27 @@ export function XYChart({ }} /> - {yAxesConfiguration.map((axis) => ( - axis.formatter?.convert(d) || ''} - style={getYAxesStyle(axis.groupId)} - /> - ))} + {yAxesConfiguration.map((axis) => { + return ( + axis.formatter?.convert(d) || ''} + style={getYAxesStyle(axis.groupId)} + domain={getYAxisDomain(axis)} + /> + ); + })} {!hideEndzones && ( legendConfig); expressions.registerFunction(() => yAxisConfig); expressions.registerFunction(() => tickLabelsConfig); + expressions.registerFunction(() => axisExtentConfig); expressions.registerFunction(() => gridlinesConfig); expressions.registerFunction(() => axisTitlesVisibilityConfig); expressions.registerFunction(() => layerConfig); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 89dca6e8a3944..b88d38e18329c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -52,6 +52,11 @@ describe('#toExpression', () => { tickLabelsVisibilitySettings: { x: false, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: false, yLeft: true, yRight: true }, hideEndzones: true, + yRightExtent: { + mode: 'custom', + lowerBound: 123, + upperBound: 456, + }, layers: [ { layerId: 'first', diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 02c5f3773d813..dea6b1a7be0c5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -149,6 +149,50 @@ export const buildExpression = ( ], fittingFunction: [state.fittingFunction || 'None'], curveType: [state.curveType || 'LINEAR'], + yLeftExtent: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_axisExtentConfig', + arguments: { + mode: [state?.yLeftExtent?.mode || 'full'], + lowerBound: + state?.yLeftExtent?.lowerBound !== undefined + ? [state?.yLeftExtent?.lowerBound] + : [], + upperBound: + state?.yLeftExtent?.upperBound !== undefined + ? [state?.yLeftExtent?.upperBound] + : [], + }, + }, + ], + }, + ], + yRightExtent: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_axisExtentConfig', + arguments: { + mode: [state?.yRightExtent?.mode || 'full'], + lowerBound: + state?.yRightExtent?.lowerBound !== undefined + ? [state?.yRightExtent?.lowerBound] + : [], + upperBound: + state?.yRightExtent?.upperBound !== undefined + ? [state?.yRightExtent?.upperBound] + : [], + }, + }, + ], + }, + ], axisTitlesVisibilitySettings: [ { type: 'expression', diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 0622f1c43f1c3..ea28b492477c1 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -211,6 +211,54 @@ export const axisTitlesVisibilityConfig: ExpressionFunctionDefinition< }, }; +export interface AxisExtentConfig { + mode: 'full' | 'dataBounds' | 'custom'; + lowerBound?: number; + upperBound?: number; +} + +export const axisExtentConfig: ExpressionFunctionDefinition< + 'lens_xy_axisExtentConfig', + null, + AxisExtentConfig, + AxisExtentConfigResult +> = { + name: 'lens_xy_axisExtentConfig', + aliases: [], + type: 'lens_xy_axisExtentConfig', + help: `Configure the xy chart's axis extents`, + inputTypes: ['null'], + args: { + mode: { + types: ['string'], + options: ['full', 'dataBounds', 'custom'], + help: i18n.translate('xpack.lens.xyChart.extentMode.help', { + defaultMessage: 'The extent mode', + }), + }, + lowerBound: { + types: ['number'], + help: i18n.translate('xpack.lens.xyChart.extentMode.help', { + defaultMessage: 'The extent mode', + }), + }, + upperBound: { + types: ['number'], + help: i18n.translate('xpack.lens.xyChart.extentMode.help', { + defaultMessage: 'The extent mode', + }), + }, + }, + fn: function fn(input: unknown, args: AxisExtentConfig) { + return { + type: 'lens_xy_axisExtentConfig', + ...args, + }; + }, +}; + +export type AxisExtentConfigResult = AxisExtentConfig & { type: 'lens_xy_axisExtentConfig' }; + interface AxisConfig { title: string; hide?: boolean; @@ -404,6 +452,8 @@ export interface XYArgs { xTitle: string; yTitle: string; yRightTitle: string; + yLeftExtent: AxisExtentConfigResult; + yRightExtent: AxisExtentConfigResult; legend: LegendConfig & { type: 'lens_xy_legendConfig' }; valueLabels: ValueLabelConfig; layers: LayerArgs[]; @@ -425,6 +475,8 @@ export interface XYState { legend: LegendConfig; valueLabels?: ValueLabelConfig; fittingFunction?: FittingFunction; + yLeftExtent?: AxisExtentConfig; + yRightExtent?: AxisExtentConfig; layers: XYLayerConfig[]; xTitle?: string; yTitle?: string; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index e3e8c6e93e3aa..0bafbead7d543 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -161,6 +161,90 @@ describe('XY Config panels', () => { expect(component.find(AxisSettingsPopover).at(1).prop('endzonesVisible')).toBe(false); expect(component.find(AxisSettingsPopover).at(2).prop('setEndzoneVisibility')).toBeFalsy(); }); + + it('should pass in information about current data bounds', () => { + const state = testState(); + frame.activeData = { + first: { + type: 'datatable', + rows: [{ bar: -5 }, { bar: 50 }], + columns: [ + { + id: 'baz', + meta: { + type: 'number', + }, + name: 'baz', + }, + { + id: 'foo', + meta: { + type: 'number', + }, + name: 'foo', + }, + { + id: 'bar', + meta: { + type: 'number', + }, + name: 'bar', + }, + ], + }, + }; + const component = shallow( + + ); + + expect(component.find(AxisSettingsPopover).at(0).prop('dataBounds')).toEqual({ + min: -5, + max: 50, + }); + }); + + it('should pass in extent information', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(AxisSettingsPopover).at(0).prop('extent')).toEqual({ + mode: 'custom', + lowerBound: 123, + upperBound: 456, + }); + expect(component.find(AxisSettingsPopover).at(0).prop('setExtent')).toBeTruthy(); + expect(component.find(AxisSettingsPopover).at(1).prop('extent')).toBeFalsy(); + expect(component.find(AxisSettingsPopover).at(1).prop('setExtent')).toBeFalsy(); + // default extent + expect(component.find(AxisSettingsPopover).at(2).prop('extent')).toEqual({ + mode: 'full', + }); + expect(component.find(AxisSettingsPopover).at(2).prop('setExtent')).toBeTruthy(); + }); }); describe('Dimension Editor', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index eccf4d9b64345..3037513ccd56e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -6,7 +6,7 @@ */ import './xy_config_panel.scss'; -import React, { useMemo, useState, memo } from 'react'; +import React, { useMemo, useState, memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { Position, ScaleType } from '@elastic/charts'; import { debounce } from 'lodash'; @@ -27,14 +27,22 @@ import { VisualizationToolbarProps, VisualizationDimensionEditorProps, FormatFactory, + FramePublicAPI, } from '../types'; -import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } from './types'; +import { + State, + SeriesType, + visualizationTypes, + YAxisMode, + AxesSettingsConfig, + AxisExtentConfig, +} from './types'; import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers'; import { trackUiEvent } from '../lens_ui_telemetry'; import { LegendSettingsPopover } from '../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; import { TooltipWrapper } from './tooltip_wrapper'; -import { getAxesConfiguration } from './axes_configuration'; +import { getAxesConfiguration, GroupsConfiguration } from './axes_configuration'; import { PalettePicker } from '../shared_components'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; import { getScaleType, getSortedAccessors } from './to_expression'; @@ -123,11 +131,44 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { ); } +const getDataBounds = function ( + activeData: FramePublicAPI['activeData'], + axes: GroupsConfiguration +) { + const groups: Partial> = {}; + axes.forEach((axis) => { + let min = Number.MAX_VALUE; + let max = Number.MIN_VALUE; + axis.series.forEach((series) => { + activeData?.[series.layer].rows.forEach((row) => { + const value = row[series.accessor]; + if (!Number.isNaN(value)) { + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } + } + }); + }); + if (min !== Number.MAX_VALUE && max !== Number.MIN_VALUE) { + groups[axis.groupId] = { + min: Math.round((min + Number.EPSILON) * 100) / 100, + max: Math.round((max + Number.EPSILON) * 100) / 100, + }; + } + }); + + return groups; +}; + export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps) { const { state, setState, frame } = props; const shouldRotate = state?.layers.length ? isHorizontalChart(state.layers) : false; - const axisGroups = getAxesConfiguration(state?.layers, shouldRotate); + const axisGroups = getAxesConfiguration(state?.layers, shouldRotate, frame.activeData); + const dataBounds = getDataBounds(frame.activeData, axisGroups); const tickLabelsVisibilitySettings = { x: state?.tickLabelsVisibilitySettings?.x ?? true, @@ -210,6 +251,40 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp : !state?.legend.isVisible ? 'hide' : 'show'; + const hasBarOrAreaOnLeftAxis = Boolean( + axisGroups + .find((group) => group.groupId === 'left') + ?.series?.some((series) => { + const seriesType = state.layers.find((l) => l.layerId === series.layer)?.seriesType; + return seriesType?.includes('bar') || seriesType?.includes('area'); + }) + ); + const setLeftExtent = useCallback( + (extent: AxisExtentConfig | undefined) => { + setState({ + ...state, + yLeftExtent: extent, + }); + }, + [setState, state] + ); + const hasBarOrAreaOnRightAxis = Boolean( + axisGroups + .find((group) => group.groupId === 'left') + ?.series?.some((series) => { + const seriesType = state.layers.find((l) => l.layerId === series.layer)?.seriesType; + return seriesType?.includes('bar') || seriesType?.includes('area'); + }) + ); + const setRightExtent = useCallback( + (extent: AxisExtentConfig | undefined) => { + setState({ + ...state, + yRightExtent: extent, + }); + }, + [setState, state] + ); return ( @@ -282,6 +357,10 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp } isAxisTitleVisible={axisTitlesVisibilitySettings.yLeft} toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange} + extent={state?.yLeftExtent || { mode: 'full' }} + setExtent={setLeftExtent} + hasBarOrAreaOnAxis={hasBarOrAreaOnLeftAxis} + dataBounds={dataBounds.left} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index d5120cfae973c..4554c34b97c55 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -527,6 +527,9 @@ function buildSuggestion({ xTitle: currentState?.xTitle, yTitle: currentState?.yTitle, yRightTitle: currentState?.yRightTitle, + hideEndzones: currentState?.hideEndzones, + yLeftExtent: currentState?.yLeftExtent, + yRightExtent: currentState?.yRightExtent, axisTitlesVisibilitySettings: currentState?.axisTitlesVisibilitySettings || { x: true, yLeft: true, From c2743d622472fef944e42ad3eae9f0de02b631ec Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Tue, 18 May 2021 11:37:18 +0100 Subject: [PATCH 098/186] Simplify deleting spaces (#99960) * Simplify deleting spaces * Fixed i18n * Fix functional tests * Update x-pack/plugins/spaces/public/management/spaces_management_app.tsx Co-authored-by: Larry Gregory * Fix snapshots Co-authored-by: Larry Gregory Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../confirm_delete_modal.test.tsx.snap | 93 ----- .../confirm_delete_modal.scss | 3 - .../confirm_delete_modal.test.tsx | 83 +++-- .../confirm_delete_modal.tsx | 317 ++++++------------ .../edit_space/delete_spaces_button.tsx | 43 +-- ...sx.snap => spaces_grid_page.test.tsx.snap} | 0 ...ges.test.tsx => spaces_grid_page.test.tsx} | 0 .../spaces_grid/spaces_grid_page.tsx | 49 +-- .../management/spaces_management_app.tsx | 52 +-- .../translations/translations/ja-JP.json | 12 - .../translations/translations/zh-CN.json | 13 - x-pack/test/accessibility/apps/spaces.ts | 4 - .../page_objects/space_selector_page.ts | 4 - 13 files changed, 201 insertions(+), 472 deletions(-) delete mode 100644 x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap delete mode 100644 x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.scss rename x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/{spaces_grid_pages.test.tsx.snap => spaces_grid_page.test.tsx.snap} (100%) rename x-pack/plugins/spaces/public/management/spaces_grid/{spaces_grid_pages.test.tsx => spaces_grid_page.test.tsx} (100%) diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap deleted file mode 100644 index 5bf93a1021c05..0000000000000 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ /dev/null @@ -1,93 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ConfirmDeleteModal renders as expected 1`] = ` - - - - - - - - -

- - - , - } - } - /> -

- - - -
-
- - - - - - - - -
-`; diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.scss b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.scss deleted file mode 100644 index 887495a231485..0000000000000 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.scss +++ /dev/null @@ -1,3 +0,0 @@ -.spcConfirmDeleteModal { - max-width: $euiFormMaxWidth + ($euiSizeL * 2); -} diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx index 36d282a1cd653..59c7dde71aedb 100644 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx +++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.test.tsx @@ -6,10 +6,10 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import type { SpacesManager } from '../../../spaces_manager'; import { spacesManagerMock } from '../../../spaces_manager/mocks'; import { ConfirmDeleteModal } from './confirm_delete_modal'; @@ -22,25 +22,53 @@ describe('ConfirmDeleteModal', () => { }; const spacesManager = spacesManagerMock.create(); - spacesManager.getActiveSpace.mockResolvedValue(space); - const onCancel = jest.fn(); - const onConfirm = jest.fn(); expect( shallowWithIntl( - + ) - ).toMatchSnapshot(); + ).toMatchInlineSnapshot(` + + +

+ + + , + } + } + /> +

+

+ +

+
+
+ `); }); - it(`requires the space name to be typed before confirming`, () => { + it('deletes the space when confirmed', async () => { const space = { id: 'my-space', name: 'My Space', @@ -48,34 +76,23 @@ describe('ConfirmDeleteModal', () => { }; const spacesManager = spacesManagerMock.create(); - spacesManager.getActiveSpace.mockResolvedValue(space); - const onCancel = jest.fn(); - const onConfirm = jest.fn(); + const onSuccess = jest.fn(); const wrapper = mountWithIntl( - ); - const input = wrapper.find('input'); - expect(input).toHaveLength(1); - - input.simulate('change', { target: { value: 'My Invalid Space Name ' } }); - - const confirmButton = wrapper.find('button[data-test-subj="confirmModalConfirmButton"]'); - confirmButton.simulate('click'); - - expect(onConfirm).not.toHaveBeenCalled(); - - input.simulate('change', { target: { value: 'My Space' } }); - confirmButton.simulate('click'); + await act(async () => { + wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + await spacesManager.deleteSpace.mock.results[0]; + }); - expect(onConfirm).toHaveBeenCalledTimes(1); + expect(spacesManager.deleteSpace).toHaveBeenLastCalledWith(space); }); }); diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 100b5b6493e30..f3ed578a94962 100644 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -5,224 +5,127 @@ * 2.0. */ -import './confirm_delete_modal.scss'; - -import type { CommonProps, EuiModalProps } from '@elastic/eui'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFieldText, - EuiFormRow, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import type { ChangeEvent } from 'react'; -import React, { Component } from 'react'; - -import type { InjectedIntl } from '@kbn/i18n/react'; -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import type { Space } from 'src/plugins/spaces_oss/common'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import type { SpacesManager } from '../../../spaces_manager'; interface Props { space: Space; spacesManager: SpacesManager; - onCancel: () => void; - onConfirm: () => void; - intl: InjectedIntl; -} - -interface State { - confirmSpaceName: string; - error: boolean | null; - deleteInProgress: boolean; - isDeletingCurrentSpace: boolean; + onCancel(): void; + onSuccess?(): void; } -class ConfirmDeleteModalUI extends Component { - public state = { - confirmSpaceName: '', - error: null, - deleteInProgress: false, - isDeletingCurrentSpace: false, - }; - - public componentDidMount() { - isCurrentSpace(this.props.space, this.props.spacesManager).then((result) => { - this.setState({ - isDeletingCurrentSpace: result, - }); - }); - } - - public render() { - const { space, onCancel, intl } = this.props; - const { isDeletingCurrentSpace } = this.state; - - let warning = null; - if (isDeletingCurrentSpace) { - const name = ( - - ({space.name}) - - ); - warning = ( - <> - - - - - - - +export const ConfirmDeleteModal: FunctionComponent = ({ + space, + onSuccess, + onCancel, + spacesManager, +}) => { + const { services } = useKibana(); + + const { value: isCurrentSpace } = useAsync( + async () => space.id === (await spacesManager.getActiveSpace()).id, + [space.id] + ); + + const [state, deleteSpace] = useAsyncFn(async () => { + try { + await spacesManager.deleteSpace(space); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.spaces.management.confirmDeleteModal.successMessage', { + defaultMessage: "Deleted space '{name}'", + values: { name: space.name }, + }) ); - } - - // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability - // to disable the buttons since this could be a long-running operation - - const modalProps: Omit & CommonProps = { - onClose: onCancel, - className: 'spcConfirmDeleteModal', - initialFocus: 'input[name="confirmDeleteSpaceInput"]', - }; - - return ( - - - - - - - - -

- - - - ), - }} - /> -

- - - - - - {warning} -
-
- - - - - - - - - -
- ); - } - - private onSpaceNameChange = (e: ChangeEvent) => { - if (typeof this.state.error === 'boolean') { - this.setState({ - confirmSpaceName: e.target.value, - error: e.target.value !== this.props.space.name, - }); - } else { - this.setState({ - confirmSpaceName: e.target.value, - }); - } - }; - - private onConfirm = async () => { - if (this.state.confirmSpaceName === this.props.space.name) { - const needsRedirect = this.state.isDeletingCurrentSpace; - const spacesManager = this.props.spacesManager; - - this.setState({ - deleteInProgress: true, - }); - - await this.props.onConfirm(); - - this.setState({ - deleteInProgress: false, - }); - - if (needsRedirect) { + if (isCurrentSpace) { spacesManager.redirectToSpaceSelector(); + } else { + onSuccess?.(); } - } else { - this.setState({ - error: true, + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.spaces.management.confirmDeleteModal.errorMessage', { + defaultMessage: "Could not delete space '{name}'", + values: { name: space.name }, + }), + text: (error as any).body?.message || error.message, }); } - }; -} - -async function isCurrentSpace(space: Space, spacesManager: SpacesManager) { - return space.id === (await spacesManager.getActiveSpace()).id; -} - -export const ConfirmDeleteModal = injectI18n(ConfirmDeleteModalUI); + }, [isCurrentSpace]); + + return ( + + {isCurrentSpace && ( + <> + + + + + + )} + +

+ + + + ), + }} + /> +

+

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx index d03b878cb19ab..92b68426d172e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx @@ -95,44 +95,13 @@ export class DeleteSpacesButton extends Component { showConfirmDeleteModal: false, }); }} - onConfirm={this.deleteSpaces} + onSuccess={() => { + this.setState({ + showConfirmDeleteModal: false, + }); + this.props.onDelete?.(); + }} /> ); }; - - public deleteSpaces = async () => { - const { spacesManager, space } = this.props; - - this.setState({ - showConfirmDeleteModal: false, - }); - - try { - await spacesManager.deleteSpace(space); - } catch (error) { - const { message: errorMessage = '' } = error.data || error.body || {}; - - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle', { - defaultMessage: 'Error deleting space: {errorMessage}', - values: { errorMessage }, - }) - ); - return; - } - - const message = i18n.translate( - 'xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage', - { - defaultMessage: 'Deleted {spaceName} space.', - values: { spaceName: space.name }, - } - ); - - this.props.notifications.toasts.addSuccess(message); - - if (this.props.onDelete) { - this.props.onDelete(); - } - }; } diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_page.test.tsx.snap similarity index 100% rename from x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap rename to x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_page.test.tsx.snap diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx similarity index 100% rename from x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx rename to x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.test.tsx diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index ac57a566e2a00..a4f797e441ab5 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -180,53 +180,16 @@ export class SpacesGridPage extends Component { showConfirmDeleteModal: false, }); }} - onConfirm={this.deleteSpace} + onSuccess={() => { + this.setState({ + showConfirmDeleteModal: false, + }); + this.loadGrid(); + }} /> ); }; - public deleteSpace = async () => { - const { spacesManager } = this.props; - - const space = this.state.selectedSpace; - - if (!space) { - return; - } - - this.setState({ - showConfirmDeleteModal: false, - }); - - try { - await spacesManager.deleteSpace(space); - } catch (error) { - const { message: errorMessage = '' } = error.data || error.body || {}; - - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage', { - defaultMessage: 'Error deleting space: {errorMessage}', - values: { - errorMessage, - }, - }) - ); - return; - } - - this.loadGrid(); - - const message = i18n.translate( - 'xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage', - { - defaultMessage: 'Deleted "{spaceName}" space.', - values: { spaceName: space.name }, - } - ); - - this.props.notifications.toasts.addSuccess(message); - }; - public loadGrid = async () => { const { spacesManager, getFeatures, notifications } = this.props; diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index da0f9157f310d..c97ec5fbcc227 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -14,7 +14,10 @@ import type { StartServicesAccessor } from 'src/core/public'; import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; import type { Space } from 'src/plugins/spaces_oss/common'; -import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + RedirectAppLinks, +} from '../../../../../src/plugins/kibana_react/public'; import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; @@ -36,22 +39,23 @@ export const spacesManagementApp = Object.freeze({ title, async mount({ element, setBreadcrumbs, history }) { - const [startServices, { SpacesGridPage }, { ManageSpacePage }] = await Promise.all([ + const [ + [coreStart, { features }], + { SpacesGridPage }, + { ManageSpacePage }, + ] = await Promise.all([ getStartServices(), import('./spaces_grid'), import('./edit_space'), ]); - const [ - { notifications, i18n: i18nStart, application, chrome }, - { features }, - ] = startServices; const spacesBreadcrumbs = [ { text: title, href: `/`, }, ]; + const { notifications, i18n: i18nStart, application, chrome } = coreStart; chrome.docTitle.change(title); @@ -119,23 +123,25 @@ export const spacesManagementApp = Object.freeze({ }; render( - - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + + + + , element ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c46b59bce2f8e..a2615ae9713a2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22650,13 +22650,6 @@ "xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "このスペースで表示される機能を更新しました。保存後にページが更新されます。", "xpack.spaces.management.confirmAlterActiveSpaceModal.title": "スペースの更新の確認", "xpack.spaces.management.confirmAlterActiveSpaceModal.updateSpaceButton": "スペースを更新", - "xpack.spaces.management.confirmDeleteModal.allContentsText": "すべてのコンテンツ", - "xpack.spaces.management.confirmDeleteModal.cancelButtonLabel": "キャンセル", - "xpack.spaces.management.confirmDeleteModal.confirmDeleteSpaceButtonLabel": "スペース {spaceName} を削除", - "xpack.spaces.management.confirmDeleteModal.confirmSpaceNameFormRowLabel": "削除するスペース名の確定", - "xpack.spaces.management.confirmDeleteModal.deleteSpaceAndAllContentsButtonLabel": " スペースとすべてのコンテンツを削除", - "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "現在のスペース {name} を削除しようとしています。続行すると、別のスペースを選択する画面に移動します。", - "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "スペース名が一致していません。", "xpack.spaces.management.copyToSpace.actionDescription": "1つ以上のスペースでこの保存されたオブジェクトのコピーを作成します", "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", "xpack.spaces.management.copyToSpace.cancelButton": "キャンセル", @@ -22721,8 +22714,6 @@ "xpack.spaces.management.customizeSpaceAvatar.selectImageUrl": "画像ファイルを選択", "xpack.spaces.management.deleteSpacesButton.deleteSpaceAriaLabel": "スペースを削除", "xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "スペースを削除", - "xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle": "スペースの削除中にエラーが発生:{errorMessage}", - "xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage": "{spaceName} スペースが削除されました。", "xpack.spaces.management.deselectAllFeaturesLink": "すべて選択解除", "xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "カテゴリ切り替え", "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": " (表示されているすべての機能) ", @@ -22738,7 +22729,6 @@ "xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "アバター", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "素晴らしいスペース", "xpack.spaces.management.manageSpacePage.cancelSpaceButton": "キャンセル", - "xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "クリックしてこのスペースのアバターをカスタマイズします", "xpack.spaces.management.manageSpacePage.createSpaceButton": "スペースを作成", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースの作成", "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "スペースに名前を付けてアバターをカスタマイズします。", @@ -22773,7 +22763,6 @@ "xpack.spaces.management.spacesGridPage.deleteActionName": "{spaceName} を削除。", "xpack.spaces.management.spacesGridPage.descriptionColumnName": "説明", "xpack.spaces.management.spacesGridPage.editSpaceActionName": "{spaceName} を編集。", - "xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage": "スペースの削除中にエラーが発生:{errorMessage}", "xpack.spaces.management.spacesGridPage.errorTitle": "スペースの読み込みエラー", "xpack.spaces.management.spacesGridPage.featuresColumnName": "機能", "xpack.spaces.management.spacesGridPage.identifierColumnName": "識別子", @@ -22783,7 +22772,6 @@ "xpack.spaces.management.spacesGridPage.someFeaturesEnabled": "{totalFeatureCount} 件中 {enabledFeatureCount} 件の機能を表示中", "xpack.spaces.management.spacesGridPage.spaceColumnName": "スペース", "xpack.spaces.management.spacesGridPage.spacesTitle": "スペース", - "xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage": "「{spaceName}」スペースが削除されました。", "xpack.spaces.management.spacesGridPage.tableCaption": "Kibana スペース", "xpack.spaces.management.toggleAllFeaturesLink": " (すべて変更) ", "xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "スペースを管理するアクセス権がありません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90aa517c68232..a6cf8e5ddb865 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23009,14 +23009,6 @@ "xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "您已更新此工作区中的可见功能。保存后,您的页面将重新加载。", "xpack.spaces.management.confirmAlterActiveSpaceModal.title": "确认更新工作区", "xpack.spaces.management.confirmAlterActiveSpaceModal.updateSpaceButton": "更新工作区", - "xpack.spaces.management.confirmDeleteModal.allContentsText": "所有内容", - "xpack.spaces.management.confirmDeleteModal.cancelButtonLabel": "取消", - "xpack.spaces.management.confirmDeleteModal.confirmDeleteSpaceButtonLabel": "删除空间 {spaceName}", - "xpack.spaces.management.confirmDeleteModal.confirmSpaceNameFormRowLabel": "确认要删除的工作区名称", - "xpack.spaces.management.confirmDeleteModal.deleteSpaceAndAllContentsButtonLabel": " 删除空间及其所有内容", - "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "删除空间会永久删除空间及{allContents}。此操作无法撤消。", - "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "您即将删除当前空间 {name}。如果继续,您将会被重定向以选择其他空间的位置。", - "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "空间名称不匹配。", "xpack.spaces.management.copyToSpace.actionDescription": "在一个或多个工作区中创建此已保存对象的副本", "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", "xpack.spaces.management.copyToSpace.cancelButton": "取消", @@ -23082,8 +23074,6 @@ "xpack.spaces.management.customizeSpaceAvatar.selectImageUrl": "选择图像文件", "xpack.spaces.management.deleteSpacesButton.deleteSpaceAriaLabel": "删除此空间", "xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "删除空间", - "xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle": "删除空间时出错:{errorMessage}", - "xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage": "已删除 {spaceName} 空间。", "xpack.spaces.management.deselectAllFeaturesLink": "取消全选", "xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel": "类别切换", "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": " (所有可见功能) ", @@ -23099,7 +23089,6 @@ "xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "头像", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间", "xpack.spaces.management.manageSpacePage.cancelSpaceButton": "取消", - "xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "单击可定制此工作区头像", "xpack.spaces.management.manageSpacePage.createSpaceButton": "创建工作区", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间", "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "命名您的工作区并定制其头像。", @@ -23134,7 +23123,6 @@ "xpack.spaces.management.spacesGridPage.deleteActionName": "删除 {spaceName}。", "xpack.spaces.management.spacesGridPage.descriptionColumnName": "描述", "xpack.spaces.management.spacesGridPage.editSpaceActionName": "编辑 {spaceName}。", - "xpack.spaces.management.spacesGridPage.errorDeletingSpaceErrorMessage": "删除空间时出错:{errorMessage}", "xpack.spaces.management.spacesGridPage.errorTitle": "加载工作区时出错", "xpack.spaces.management.spacesGridPage.featuresColumnName": "功能", "xpack.spaces.management.spacesGridPage.identifierColumnName": "标识符", @@ -23144,7 +23132,6 @@ "xpack.spaces.management.spacesGridPage.someFeaturesEnabled": "{enabledFeatureCount} / {totalFeatureCount} 个功能可见", "xpack.spaces.management.spacesGridPage.spaceColumnName": "工作区", "xpack.spaces.management.spacesGridPage.spacesTitle": "工作区", - "xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage": "已删除“{spaceName}”工作区。", "xpack.spaces.management.spacesGridPage.tableCaption": "Kibana 工作区", "xpack.spaces.management.toggleAllFeaturesLink": " (全部更改) ", "xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "您无权管理工作区。", diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts index a08ae474497e5..6242896c263ba 100644 --- a/x-pack/test/accessibility/apps/spaces.ts +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -111,14 +111,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.spaceSelector.clickManageSpaces(); await PageObjects.spaceSelector.clickOnDeleteSpaceButton('space_b'); await a11y.testAppSnapshot(); - // a11y test for no space name in confirm dialogue box - await PageObjects.spaceSelector.confirmDeletingSpace(); - await a11y.testAppSnapshot(); }); // test starts with deleting space b so we can get the space selection page instead of logging out in the test it('a11y test for space selection page', async () => { - await PageObjects.spaceSelector.setSpaceNameTobeDeleted('space_b'); await PageObjects.spaceSelector.confirmDeletingSpace(); await a11y.testAppSnapshot(); await PageObjects.spaceSelector.clickSpaceCard('default'); diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index 57ca0c97c63da..0a41e4b90287f 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -167,10 +167,6 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro await testSubjects.click(`${spaceName}-deleteSpace`); } - async setSpaceNameTobeDeleted(spaceName: string) { - await testSubjects.setValue('deleteSpaceInput', spaceName); - } - async cancelDeletingSpace() { await testSubjects.click('confirmModalCancelButton'); } From 035456a30d135a5d3a678c17dbc2d86d735e68a7 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 18 May 2021 15:28:10 +0300 Subject: [PATCH 099/186] [XY axis] Improve expression with explicit params (#98897) * Removed visconfig and using explicit params instead in xy_plugin * Fix CI * Fix i18n * Fix unit test * Fix some remarks * move expressions into separate chunk * fix CI * Update label.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alexey Antonov --- .../public/__snapshots__/to_ast.test.ts.snap | 2 +- src/plugins/vis_type_xy/common/index.ts | 13 +- .../public/__snapshots__/to_ast.test.ts.snap | 63 +++- .../public/editor/common_config.tsx | 6 +- .../expression_functions/category_axis.ts | 116 ++++++++ .../public/expression_functions/index.ts | 18 ++ .../public/expression_functions/label.ts | 89 ++++++ .../expression_functions/series_param.ts | 128 ++++++++ .../expression_functions/threshold_line.ts | 86 ++++++ .../expression_functions/time_marker.ts | 82 ++++++ .../public/expression_functions/value_axis.ts | 79 +++++ .../public/expression_functions/vis_scale.ts | 98 +++++++ .../expression_functions/xy_dimension.ts | 85 ++++++ .../public/expression_functions/xy_vis_fn.ts | 273 ++++++++++++++++++ src/plugins/vis_type_xy/public/plugin.ts | 17 +- .../public/sample_vis.test.mocks.ts | 6 + src/plugins/vis_type_xy/public/to_ast.test.ts | 2 +- src/plugins/vis_type_xy/public/to_ast.ts | 148 +++++++++- .../vis_type_xy/public/types/config.ts | 2 +- .../vis_type_xy/public/types/constants.ts | 89 +++--- src/plugins/vis_type_xy/public/types/index.ts | 2 +- src/plugins/vis_type_xy/public/types/param.ts | 65 ++++- .../utils/render_all_series.test.mocks.ts | 8 +- .../vis_type_xy/public/vis_renderer.tsx | 9 +- src/plugins/vis_type_xy/public/xy_vis_fn.ts | 77 ----- 25 files changed, 1389 insertions(+), 174 deletions(-) create mode 100644 src/plugins/vis_type_xy/public/expression_functions/category_axis.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/index.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/label.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/series_param.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/time_marker.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/value_axis.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts create mode 100644 src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts delete mode 100644 src/plugins/vis_type_xy/public/xy_vis_fn.ts diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap index c3ffc0dd08412..3ca2834a54fca 100644 --- a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap @@ -8,7 +8,7 @@ Object { "area", ], "visConfig": Array [ - "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"palette\\":{\\"name\\":\\"default\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", ], }, "getArgument": [Function], diff --git a/src/plugins/vis_type_xy/common/index.ts b/src/plugins/vis_type_xy/common/index.ts index 903adb53eb403..a80946f7c62fa 100644 --- a/src/plugins/vis_type_xy/common/index.ts +++ b/src/plugins/vis_type_xy/common/index.ts @@ -6,17 +6,14 @@ * Side Public License, v 1. */ -import { $Values } from '@kbn/utility-types'; - /** * Type of charts able to render */ -export const ChartType = Object.freeze({ - Line: 'line' as const, - Area: 'area' as const, - Histogram: 'histogram' as const, -}); -export type ChartType = $Values; +export enum ChartType { + Line = 'line', + Area = 'area', + Histogram = 'histogram', +} /** * Type of xy visualizations diff --git a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap index e6665c26a2815..7c21e699216bc 100644 --- a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap @@ -4,11 +4,70 @@ exports[`xy vis toExpressionAst function should match basic snapshot 1`] = ` Object { "addArgument": [Function], "arguments": Object { + "addLegend": Array [ + true, + ], + "addTimeMarker": Array [ + false, + ], + "addTooltip": Array [ + true, + ], + "categoryAxes": Array [ + Object { + "toAst": [Function], + }, + ], + "chartType": Array [ + "area", + ], + "gridCategoryLines": Array [ + false, + ], + "labels": Array [ + Object { + "toAst": [Function], + }, + ], + "legendPosition": Array [ + "top", + ], + "palette": Array [ + "default", + ], + "seriesDimension": Array [ + Object { + "toAst": [Function], + }, + ], + "seriesParams": Array [ + Object { + "toAst": [Function], + }, + ], + "thresholdLine": Array [ + Object { + "toAst": [Function], + }, + ], + "times": Array [], "type": Array [ "area", ], - "visConfig": Array [ - "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "valueAxes": Array [ + Object { + "toAst": [Function], + }, + ], + "xDimension": Array [ + Object { + "toAst": [Function], + }, + ], + "yDimension": Array [ + Object { + "toAst": [Function], + }, ], }, "getArgument": [Function], diff --git a/src/plugins/vis_type_xy/public/editor/common_config.tsx b/src/plugins/vis_type_xy/public/editor/common_config.tsx index 1e4ac7df0082c..1815d9cfc429d 100644 --- a/src/plugins/vis_type_xy/public/editor/common_config.tsx +++ b/src/plugins/vis_type_xy/public/editor/common_config.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VisEditorOptionsProps } from '../../../visualizations/public'; +import type { VisEditorOptionsProps } from '../../../visualizations/public'; -import { VisParams } from '../types'; +import type { VisParams } from '../types'; import { MetricsAxisOptions, PointSeriesOptions } from './components/options'; -import { ValidationWrapper } from './components/common'; +import { ValidationWrapper } from './components/common/validation_wrapper'; export function getOptionTabs(showElasticChartsOptions = false) { return [ diff --git a/src/plugins/vis_type_xy/public/expression_functions/category_axis.ts b/src/plugins/vis_type_xy/public/expression_functions/category_axis.ts new file mode 100644 index 0000000000000..30215d8feb8a3 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/category_axis.ts @@ -0,0 +1,116 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { CategoryAxis } from '../types'; +import type { ExpressionValueScale } from './vis_scale'; +import type { ExpressionValueLabel } from './label'; + +export interface Arguments extends Omit { + title?: string; + scale: ExpressionValueScale; + labels: ExpressionValueLabel; +} + +export type ExpressionValueCategoryAxis = ExpressionValueBoxed< + 'category_axis', + { + id: CategoryAxis['id']; + show: CategoryAxis['show']; + position: CategoryAxis['position']; + axisType: CategoryAxis['type']; + title: { + text?: string; + }; + labels: CategoryAxis['labels']; + scale: CategoryAxis['scale']; + } +>; + +export const categoryAxis = (): ExpressionFunctionDefinition< + 'categoryaxis', + Datatable | null, + Arguments, + ExpressionValueCategoryAxis +> => ({ + name: 'categoryaxis', + help: i18n.translate('visTypeXy.function.categoryAxis.help', { + defaultMessage: 'Generates category axis object', + }), + type: 'category_axis', + args: { + id: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.id.help', { + defaultMessage: 'Id of category axis', + }), + required: true, + }, + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.categoryAxis.show.help', { + defaultMessage: 'Show the category axis', + }), + required: true, + }, + position: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.position.help', { + defaultMessage: 'Position of the category axis', + }), + required: true, + }, + type: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.type.help', { + defaultMessage: 'Type of the category axis. Can be category or value', + }), + required: true, + }, + title: { + types: ['string'], + help: i18n.translate('visTypeXy.function.categoryAxis.title.help', { + defaultMessage: 'Title of the category axis', + }), + }, + scale: { + types: ['vis_scale'], + help: i18n.translate('visTypeXy.function.categoryAxis.scale.help', { + defaultMessage: 'Scale config', + }), + }, + labels: { + types: ['label'], + help: i18n.translate('visTypeXy.function.categoryAxis.labels.help', { + defaultMessage: 'Axis label config', + }), + }, + }, + fn: (context, args) => { + return { + type: 'category_axis', + id: args.id, + show: args.show, + position: args.position, + axisType: args.type, + title: { + text: args.title, + }, + scale: { + ...args.scale, + type: args.scale.scaleType, + }, + labels: args.labels, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/index.ts b/src/plugins/vis_type_xy/public/expression_functions/index.ts new file mode 100644 index 0000000000000..4e7db57ee65d7 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/index.ts @@ -0,0 +1,18 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { visTypeXyVisFn } from './xy_vis_fn'; + +export { categoryAxis, ExpressionValueCategoryAxis } from './category_axis'; +export { timeMarker, ExpressionValueTimeMarker } from './time_marker'; +export { valueAxis, ExpressionValueValueAxis } from './value_axis'; +export { seriesParam, ExpressionValueSeriesParam } from './series_param'; +export { thresholdLine, ExpressionValueThresholdLine } from './threshold_line'; +export { label, ExpressionValueLabel } from './label'; +export { visScale, ExpressionValueScale } from './vis_scale'; +export { xyDimension, ExpressionValueXYDimension } from './xy_dimension'; diff --git a/src/plugins/vis_type_xy/public/expression_functions/label.ts b/src/plugins/vis_type_xy/public/expression_functions/label.ts new file mode 100644 index 0000000000000..934278d13cff0 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/label.ts @@ -0,0 +1,89 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { Labels } from '../../../charts/public'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; + +export type ExpressionValueLabel = ExpressionValueBoxed< + 'label', + { + color?: Labels['color']; + filter?: Labels['filter']; + overwriteColor?: Labels['overwriteColor']; + rotate?: Labels['rotate']; + show?: Labels['show']; + truncate?: Labels['truncate']; + } +>; + +export const label = (): ExpressionFunctionDefinition< + 'label', + Datatable | null, + Labels, + ExpressionValueLabel +> => ({ + name: 'label', + help: i18n.translate('visTypeXy.function.label.help', { + defaultMessage: 'Generates label object', + }), + type: 'label', + args: { + color: { + types: ['string'], + help: i18n.translate('visTypeXy.function.label.color.help', { + defaultMessage: 'Color of label', + }), + }, + filter: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.label.filter.help', { + defaultMessage: 'Hides overlapping labels and duplicates on axis', + }), + }, + overwriteColor: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.label.overwriteColor.help', { + defaultMessage: 'Overwrite color', + }), + }, + rotate: { + types: ['number'], + help: i18n.translate('visTypeXy.function.label.rotate.help', { + defaultMessage: 'Rotate angle', + }), + }, + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.label.show.help', { + defaultMessage: 'Show label', + }), + }, + truncate: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.label.truncate.help', { + defaultMessage: 'The number of symbols before truncating', + }), + }, + }, + fn: (context, args) => { + return { + type: 'label', + color: args.color, + filter: args.hasOwnProperty('filter') ? args.filter : undefined, + overwriteColor: args.overwriteColor, + rotate: args.rotate, + show: args.show, + truncate: args.truncate, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/series_param.ts b/src/plugins/vis_type_xy/public/expression_functions/series_param.ts new file mode 100644 index 0000000000000..402187cea6586 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/series_param.ts @@ -0,0 +1,128 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { SeriesParam } from '../types'; + +export interface Arguments extends Omit { + label: string; + id: string; +} + +export type ExpressionValueSeriesParam = ExpressionValueBoxed< + 'series_param', + { + data: { label: string; id: string }; + drawLinesBetweenPoints?: boolean; + interpolate?: SeriesParam['interpolate']; + lineWidth?: number; + mode: SeriesParam['mode']; + show: boolean; + showCircles: boolean; + seriesParamType: SeriesParam['type']; + valueAxis: string; + } +>; + +export const seriesParam = (): ExpressionFunctionDefinition< + 'seriesparam', + Datatable, + Arguments, + ExpressionValueSeriesParam +> => ({ + name: 'seriesparam', + help: i18n.translate('visTypeXy.function.seriesparam.help', { + defaultMessage: 'Generates series param object', + }), + type: 'series_param', + inputTypes: ['datatable'], + args: { + label: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.label.help', { + defaultMessage: 'Name of series param', + }), + required: true, + }, + id: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.id.help', { + defaultMessage: 'Id of series param', + }), + required: true, + }, + drawLinesBetweenPoints: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.seriesParam.drawLinesBetweenPoints.help', { + defaultMessage: 'Draw lines between points', + }), + }, + interpolate: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.interpolate.help', { + defaultMessage: 'Interpolate mode. Can be linear, cardinal or step-after', + }), + }, + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.seriesParam.show.help', { + defaultMessage: 'Show param', + }), + required: true, + }, + lineWidth: { + types: ['number'], + help: i18n.translate('visTypeXy.function.seriesParam.lineWidth.help', { + defaultMessage: 'Width of line', + }), + }, + mode: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.mode.help', { + defaultMessage: 'Chart mode. Can be stacked or percentage', + }), + }, + showCircles: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.seriesParam.showCircles.help', { + defaultMessage: 'Show circles', + }), + }, + type: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.type.help', { + defaultMessage: 'Chart type. Can be line, area or histogram', + }), + }, + valueAxis: { + types: ['string'], + help: i18n.translate('visTypeXy.function.seriesParam.valueAxis.help', { + defaultMessage: 'Name of value axis', + }), + }, + }, + fn: (context, args) => { + return { + type: 'series_param', + data: { label: args.label, id: args.id }, + drawLinesBetweenPoints: args.drawLinesBetweenPoints, + interpolate: args.interpolate, + lineWidth: args.lineWidth, + mode: args.mode, + show: args.show, + showCircles: args.showCircles, + seriesParamType: args.type, + valueAxis: args.valueAxis, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts b/src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts new file mode 100644 index 0000000000000..8c01e37503985 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/threshold_line.ts @@ -0,0 +1,86 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { ThresholdLine } from '../types'; + +export type ExpressionValueThresholdLine = ExpressionValueBoxed< + 'threshold_line', + { + show: ThresholdLine['show']; + value: ThresholdLine['value']; + width: ThresholdLine['width']; + style: ThresholdLine['style']; + color: ThresholdLine['color']; + } +>; + +export const thresholdLine = (): ExpressionFunctionDefinition< + 'thresholdline', + Datatable | null, + ThresholdLine, + ExpressionValueThresholdLine +> => ({ + name: 'thresholdline', + help: i18n.translate('visTypeXy.function.thresholdLine.help', { + defaultMessage: 'Generates threshold line object', + }), + type: 'threshold_line', + args: { + show: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.thresholdLine.show.help', { + defaultMessage: 'Show threshould line', + }), + required: true, + }, + value: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.thresholdLine.value.help', { + defaultMessage: 'Threshold value', + }), + required: true, + }, + width: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.thresholdLine.width.help', { + defaultMessage: 'Width of threshold line', + }), + required: true, + }, + style: { + types: ['string'], + help: i18n.translate('visTypeXy.function.thresholdLine.style.help', { + defaultMessage: 'Style of threshold line. Can be full, dashed or dot-dashed', + }), + required: true, + }, + color: { + types: ['string'], + help: i18n.translate('visTypeXy.function.thresholdLine.color.help', { + defaultMessage: 'Color of threshold line', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'threshold_line', + show: args.show, + value: args.value, + width: args.width, + style: args.style, + color: args.color, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/time_marker.ts b/src/plugins/vis_type_xy/public/expression_functions/time_marker.ts new file mode 100644 index 0000000000000..3d9f609292c00 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/time_marker.ts @@ -0,0 +1,82 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { TimeMarker } from '../types'; + +export type ExpressionValueTimeMarker = ExpressionValueBoxed< + 'time_marker', + { + time: string; + class?: string; + color?: string; + opacity?: number; + width?: number; + } +>; + +export const timeMarker = (): ExpressionFunctionDefinition< + 'timemarker', + Datatable | null, + TimeMarker, + ExpressionValueTimeMarker +> => ({ + name: 'timemarker', + help: i18n.translate('visTypeXy.function.timemarker.help', { + defaultMessage: 'Generates time marker object', + }), + type: 'time_marker', + args: { + time: { + types: ['string'], + help: i18n.translate('visTypeXy.function.timeMarker.time.help', { + defaultMessage: 'Exact Time', + }), + required: true, + }, + class: { + types: ['string'], + help: i18n.translate('visTypeXy.function.timeMarker.class.help', { + defaultMessage: 'Css class name', + }), + }, + color: { + types: ['string'], + help: i18n.translate('visTypeXy.function.timeMarker.color.help', { + defaultMessage: 'Color of time marker', + }), + }, + opacity: { + types: ['number'], + help: i18n.translate('visTypeXy.function.timeMarker.opacity.help', { + defaultMessage: 'Opacity of time marker', + }), + }, + width: { + types: ['number'], + help: i18n.translate('visTypeXy.function.timeMarker.width.help', { + defaultMessage: 'Width of time marker', + }), + }, + }, + fn: (context, args) => { + return { + type: 'time_marker', + time: args.time, + class: args.class, + color: args.color, + opacity: args.opacity, + width: args.width, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/value_axis.ts b/src/plugins/vis_type_xy/public/expression_functions/value_axis.ts new file mode 100644 index 0000000000000..510ec9bc605d2 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/value_axis.ts @@ -0,0 +1,79 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionValueCategoryAxis } from './category_axis'; +import type { CategoryAxis } from '../types'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; + +interface Arguments { + name: string; + axisParams: ExpressionValueCategoryAxis; +} + +export type ExpressionValueValueAxis = ExpressionValueBoxed< + 'value_axis', + { + name: string; + id: string; + show: boolean; + position: CategoryAxis['position']; + axisType: CategoryAxis['type']; + title: { + text?: string; + }; + labels: CategoryAxis['labels']; + scale: CategoryAxis['scale']; + } +>; + +export const valueAxis = (): ExpressionFunctionDefinition< + 'valueaxis', + Datatable | null, + Arguments, + ExpressionValueValueAxis +> => ({ + name: 'valueaxis', + help: i18n.translate('visTypeXy.function.valueaxis.help', { + defaultMessage: 'Generates value axis object', + }), + type: 'value_axis', + args: { + name: { + types: ['string'], + help: i18n.translate('visTypeXy.function.valueAxis.name.help', { + defaultMessage: 'Name of value axis', + }), + required: true, + }, + axisParams: { + types: ['category_axis'], + help: i18n.translate('visTypeXy.function.valueAxis.axisParams.help', { + defaultMessage: 'Value axis params', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'value_axis', + name: args.name, + id: args.axisParams.id, + show: args.axisParams.show, + position: args.axisParams.position, + axisType: args.axisParams.axisType, + title: args.axisParams.title, + scale: args.axisParams.scale, + labels: args.axisParams.labels, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts b/src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts new file mode 100644 index 0000000000000..fadf3d80a6e81 --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/vis_scale.ts @@ -0,0 +1,98 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { Scale } from '../types'; + +export type ExpressionValueScale = ExpressionValueBoxed< + 'vis_scale', + { + boundsMargin?: Scale['boundsMargin']; + defaultYExtents?: Scale['defaultYExtents']; + max?: Scale['max']; + min?: Scale['min']; + mode?: Scale['mode']; + setYExtents?: Scale['setYExtents']; + scaleType: Scale['type']; + } +>; + +export const visScale = (): ExpressionFunctionDefinition< + 'visscale', + Datatable | null, + Scale, + ExpressionValueScale +> => ({ + name: 'visscale', + help: i18n.translate('visTypeXy.function.scale.help', { + defaultMessage: 'Generates scale object', + }), + type: 'vis_scale', + args: { + boundsMargin: { + types: ['number', 'string'], + help: i18n.translate('visTypeXy.function.scale.boundsMargin.help', { + defaultMessage: 'Margin of bounds', + }), + }, + defaultYExtents: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.scale.defaultYExtents.help', { + defaultMessage: 'Flag which allows to scale to data bounds', + }), + }, + setYExtents: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.scale.setYExtents.help', { + defaultMessage: 'Flag which allows to set your own extents', + }), + }, + max: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.scale.max.help', { + defaultMessage: 'Max value', + }), + }, + min: { + types: ['number', 'null'], + help: i18n.translate('visTypeXy.function.scale.min.help', { + defaultMessage: 'Min value', + }), + }, + mode: { + types: ['string'], + help: i18n.translate('visTypeXy.function.scale.mode.help', { + defaultMessage: 'Scale mode. Can be normal, percentage, wiggle or silhouette', + }), + }, + type: { + types: ['string'], + help: i18n.translate('visTypeXy.function.scale.type.help', { + defaultMessage: 'Scale type. Can be linear, log or square root', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'vis_scale', + boundsMargin: args.boundsMargin, + defaultYExtents: args.defaultYExtents, + setYExtents: args.setYExtents, + max: args.max, + min: args.min, + mode: args.mode, + scaleType: args.type, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts b/src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts new file mode 100644 index 0000000000000..ecbc3640c035b --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/xy_dimension.ts @@ -0,0 +1,85 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionValueVisDimension } from '../../../visualizations/public'; +import type { + ExpressionFunctionDefinition, + Datatable, + ExpressionValueBoxed, +} from '../../../expressions/public'; +import type { Dimension } from '../types'; + +interface Arguments { + visDimension: ExpressionValueVisDimension; + params: string; + aggType: string; + label: string; +} + +export type ExpressionValueXYDimension = ExpressionValueBoxed< + 'xy_dimension', + { + label: string; + aggType: string; + params: Dimension['params']; + accessor: number; + format: Dimension['format']; + } +>; + +export const xyDimension = (): ExpressionFunctionDefinition< + 'xydimension', + Datatable | null, + Arguments, + ExpressionValueXYDimension +> => ({ + name: 'xydimension', + help: i18n.translate('visTypeXy.function.xydimension.help', { + defaultMessage: 'Generates xy dimension object', + }), + type: 'xy_dimension', + args: { + visDimension: { + types: ['vis_dimension'], + help: i18n.translate('visTypeXy.function.xyDimension.visDimension.help', { + defaultMessage: 'Dimension object config', + }), + required: true, + }, + label: { + types: ['string'], + help: i18n.translate('visTypeXy.function.xyDimension.label.help', { + defaultMessage: 'Label', + }), + }, + aggType: { + types: ['string'], + help: i18n.translate('visTypeXy.function.xyDimension.aggType.help', { + defaultMessage: 'Aggregation type', + }), + }, + params: { + types: ['string'], + default: '"{}"', + help: i18n.translate('visTypeXy.function.xyDimension.params.help', { + defaultMessage: 'Params', + }), + }, + }, + fn: (context, args) => { + return { + type: 'xy_dimension', + label: args.label, + aggType: args.aggType, + params: JSON.parse(args.params!), + accessor: args.visDimension.accessor as number, + format: args.visDimension.format, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts new file mode 100644 index 0000000000000..b8b8c0e8b8cca --- /dev/null +++ b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts @@ -0,0 +1,273 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import type { ExpressionFunctionDefinition, Datatable, Render } from '../../../expressions/public'; +import type { ChartType } from '../../common'; +import type { VisParams, XYVisConfig } from '../types'; + +export const visName = 'xy_vis'; +export interface RenderValue { + visData: Datatable; + visType: ChartType; + visConfig: VisParams; + syncColors: boolean; +} + +export type VisTypeXyExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof visName, + Datatable, + XYVisConfig, + Render +>; + +export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ + name: visName, + type: 'render', + context: { + types: ['datatable'], + }, + help: i18n.translate('visTypeXy.functions.help', { + defaultMessage: 'XY visualization', + }), + args: { + type: { + types: ['string'], + default: '""', + help: 'xy vis type', + }, + chartType: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.args.chartType.help', { + defaultMessage: 'Type of a chart. Can be line, area or histogram', + }), + }, + addTimeMarker: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.addTimeMarker.help', { + defaultMessage: 'Show time marker', + }), + }, + addLegend: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.addLegend.help', { + defaultMessage: 'Show chart legend', + }), + }, + addTooltip: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.addTooltip.help', { + defaultMessage: 'Show tooltip on hover', + }), + }, + legendPosition: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.legendPosition.help', { + defaultMessage: 'Position the legend on top, bottom, left, right of the chart', + }), + }, + categoryAxes: { + types: ['category_axis'], + help: i18n.translate('visTypeXy.function.args.categoryAxes.help', { + defaultMessage: 'Category axis config', + }), + multi: true, + }, + thresholdLine: { + types: ['threshold_line'], + help: i18n.translate('visTypeXy.function.args.thresholdLine.help', { + defaultMessage: 'Threshold line config', + }), + }, + labels: { + types: ['label'], + help: i18n.translate('visTypeXy.function.args.labels.help', { + defaultMessage: 'Chart labels config', + }), + }, + orderBucketsBySum: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.orderBucketsBySum.help', { + defaultMessage: 'Order buckets by sum', + }), + }, + seriesParams: { + types: ['series_param'], + help: i18n.translate('visTypeXy.function.args.seriesParams.help', { + defaultMessage: 'Series param config', + }), + multi: true, + }, + valueAxes: { + types: ['value_axis'], + help: i18n.translate('visTypeXy.function.args.valueAxes.help', { + defaultMessage: 'Value axis config', + }), + multi: true, + }, + radiusRatio: { + types: ['number'], + help: i18n.translate('visTypeXy.function.args.radiusRatio.help', { + defaultMessage: 'Dot size ratio', + }), + }, + gridCategoryLines: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.gridCategoryLines.help', { + defaultMessage: 'Show grid category lines in chart', + }), + }, + gridValueAxis: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.gridValueAxis.help', { + defaultMessage: 'Name of value axis for which we show grid', + }), + }, + isVislibVis: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.isVislibVis.help', { + defaultMessage: + 'Flag to indicate old vislib visualizations. Used for backwards compatibility including colors', + }), + }, + detailedTooltip: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.detailedTooltip.help', { + defaultMessage: 'Show detailed tooltip', + }), + }, + fittingFunction: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.fittingFunction.help', { + defaultMessage: 'Name of fitting function', + }), + }, + times: { + types: ['time_marker'], + help: i18n.translate('visTypeXy.function.args.times.help', { + defaultMessage: 'Time marker config', + }), + multi: true, + }, + palette: { + types: ['string'], + help: i18n.translate('visTypeXy.function.args.palette.help', { + defaultMessage: 'Defines the chart palette name', + }), + }, + xDimension: { + types: ['xy_dimension', 'null'], + help: i18n.translate('visTypeXy.function.args.xDimension.help', { + defaultMessage: 'X axis dimension config', + }), + }, + yDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.yDimension.help', { + defaultMessage: 'Y axis dimension config', + }), + multi: true, + }, + zDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.zDimension.help', { + defaultMessage: 'Z axis dimension config', + }), + multi: true, + }, + widthDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.widthDimension.help', { + defaultMessage: 'Width dimension config', + }), + multi: true, + }, + seriesDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.seriesDimension.help', { + defaultMessage: 'Series dimension config', + }), + multi: true, + }, + splitRowDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.splitRowDimension.help', { + defaultMessage: 'Split by row dimension config', + }), + multi: true, + }, + splitColumnDimension: { + types: ['xy_dimension'], + help: i18n.translate('visTypeXy.function.args.splitColumnDimension.help', { + defaultMessage: 'Split by column dimension config', + }), + multi: true, + }, + }, + fn(context, args, handlers) { + const visType = args.chartType; + const visConfig = { + type: args.chartType, + addLegend: args.addLegend, + addTooltip: args.addTooltip, + legendPosition: args.legendPosition, + addTimeMarker: args.addTimeMarker, + categoryAxes: args.categoryAxes.map((categoryAxis) => ({ + ...categoryAxis, + type: categoryAxis.axisType, + })), + orderBucketsBySum: args.orderBucketsBySum, + labels: args.labels, + thresholdLine: args.thresholdLine, + valueAxes: args.valueAxes.map((valueAxis) => ({ ...valueAxis, type: valueAxis.axisType })), + grid: { + categoryLines: args.gridCategoryLines, + valueAxis: args.gridValueAxis, + }, + seriesParams: args.seriesParams.map((seriesParam) => ({ + ...seriesParam, + type: seriesParam.seriesParamType, + })), + radiusRatio: args.radiusRatio, + times: args.times, + isVislibVis: args.isVislibVis, + detailedTooltip: args.detailedTooltip, + palette: { + type: 'palette', + name: args.palette, + }, + fittingFunction: args.fittingFunction, + dimensions: { + x: args.xDimension, + y: args.yDimension, + z: args.zDimension, + width: args.widthDimension, + series: args.seriesDimension, + splitRow: args.splitRowDimension, + splitColumn: args.splitColumnDimension, + }, + } as VisParams; + + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.logDatatable('default', context); + } + + return { + type: 'render', + as: visName, + value: { + context, + visType, + visConfig, + visData: context, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, + }, + }; + }, +}); diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index d414da8f6dc97..7bdb4f78bc631 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -12,8 +12,6 @@ import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/p import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; - -import { createVisTypeXyVisFn } from './xy_vis_fn'; import { setDataActions, setFormatService, @@ -23,10 +21,13 @@ import { setPalettesService, setTrackUiMetric, } from './services'; + import { visTypesDefinitions } from './vis_types'; import { LEGACY_CHARTS_LIBRARY } from '../common'; import { xyVisRenderer } from './vis_renderer'; +import * as expressionFunctions from './expression_functions'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface VisTypeXyPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -66,8 +67,18 @@ export class VisTypeXyPlugin setUISettings(core.uiSettings); setThemeService(charts.theme); setPalettesService(charts.palettes); - [createVisTypeXyVisFn].forEach(expressions.registerFunction); + expressions.registerRenderer(xyVisRenderer); + expressions.registerFunction(expressionFunctions.visTypeXyVisFn); + expressions.registerFunction(expressionFunctions.categoryAxis); + expressions.registerFunction(expressionFunctions.timeMarker); + expressions.registerFunction(expressionFunctions.valueAxis); + expressions.registerFunction(expressionFunctions.seriesParam); + expressions.registerFunction(expressionFunctions.thresholdLine); + expressions.registerFunction(expressionFunctions.label); + expressions.registerFunction(expressionFunctions.visScale); + expressions.registerFunction(expressionFunctions.xyDimension); + visTypesDefinitions.forEach(visualizations.createBaseVisualization); } diff --git a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts index c425eb71117e8..e15f9c4207702 100644 --- a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts @@ -1414,6 +1414,9 @@ export const sampleAreaVis = { color: '#E7664C', }, labels: {}, + palette: { + name: 'default', + }, }, }, editorConfig: { @@ -1575,6 +1578,9 @@ export const sampleAreaVis = { style: 'full', color: '#E7664C', }, + palette: { + name: 'default', + }, labels: {}, dimensions: { x: { diff --git a/src/plugins/vis_type_xy/public/to_ast.test.ts b/src/plugins/vis_type_xy/public/to_ast.test.ts index 22e2d5f1cd9cc..4437986eff5f7 100644 --- a/src/plugins/vis_type_xy/public/to_ast.test.ts +++ b/src/plugins/vis_type_xy/public/to_ast.test.ts @@ -42,7 +42,7 @@ describe('xy vis toExpressionAst function', () => { it('should match basic snapshot', () => { toExpressionAst(vis, params); - const [, builtExpression] = (buildExpression as jest.Mock).mock.calls[0][0]; + const [, builtExpression] = (buildExpression as jest.Mock).mock.calls.pop()[0]; expect(builtExpression).toMatchSnapshot(); }); diff --git a/src/plugins/vis_type_xy/public/to_ast.ts b/src/plugins/vis_type_xy/public/to_ast.ts index 84331af3a5329..c0a0ee566a445 100644 --- a/src/plugins/vis_type_xy/public/to_ast.ts +++ b/src/plugins/vis_type_xy/public/to_ast.ts @@ -11,13 +11,122 @@ import moment from 'moment'; import { VisToExpressionAst, getVisSchemas } from '../../visualizations/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { BUCKET_TYPES } from '../../data/public'; +import { Labels } from '../../charts/public'; -import { DateHistogramParams, Dimensions, HistogramParams, VisParams } from './types'; -import { visName, VisTypeXyExpressionFunctionDefinition } from './xy_vis_fn'; +import { + DateHistogramParams, + Dimensions, + Dimension, + HistogramParams, + VisParams, + CategoryAxis, + SeriesParam, + ThresholdLine, + ValueAxis, + Scale, + TimeMarker, +} from './types'; +import { visName, VisTypeXyExpressionFunctionDefinition } from './expression_functions/xy_vis_fn'; import { XyVisType } from '../common'; import { getEsaggsFn } from './to_ast_esaggs'; import { TimeRangeBounds } from '../../data/common'; +const prepareLabel = (data: Labels) => { + const label = buildExpressionFunction('label', { + ...data, + }); + + return buildExpression([label]); +}; + +const prepareScale = (data: Scale) => { + const scale = buildExpressionFunction('visscale', { + ...data, + }); + + return buildExpression([scale]); +}; + +const prepareThresholdLine = (data: ThresholdLine) => { + const thresholdLine = buildExpressionFunction('thresholdline', { + ...data, + }); + + return buildExpression([thresholdLine]); +}; + +const prepareTimeMarker = (data: TimeMarker) => { + const timeMarker = buildExpressionFunction('timemarker', { + ...data, + }); + + return buildExpression([timeMarker]); +}; + +const prepareCategoryAxis = (data: CategoryAxis) => { + const categoryAxis = buildExpressionFunction('categoryaxis', { + id: data.id, + show: data.show, + position: data.position, + type: data.type, + title: data.title.text, + scale: prepareScale(data.scale), + labels: prepareLabel(data.labels), + }); + + return buildExpression([categoryAxis]); +}; + +const prepareValueAxis = (data: ValueAxis) => { + const categoryAxis = buildExpressionFunction('valueaxis', { + name: data.name, + axisParams: prepareCategoryAxis({ + ...data, + }), + }); + + return buildExpression([categoryAxis]); +}; + +const prepareSeriesParam = (data: SeriesParam) => { + const seriesParam = buildExpressionFunction('seriesparam', { + label: data.data.label, + id: data.data.id, + drawLinesBetweenPoints: data.drawLinesBetweenPoints, + interpolate: data.interpolate, + lineWidth: data.lineWidth, + mode: data.mode, + show: data.show, + showCircles: data.showCircles, + type: data.type, + valueAxis: data.valueAxis, + }); + + return buildExpression([seriesParam]); +}; + +const prepareVisDimension = (data: Dimension) => { + const visDimension = buildExpressionFunction('visdimension', { accessor: data.accessor }); + + if (data.format) { + visDimension.addArgument('format', data.format.id); + visDimension.addArgument('formatParams', JSON.stringify(data.format.params)); + } + + return buildExpression([visDimension]); +}; + +const prepareXYDimension = (data: Dimension) => { + const xyDimension = buildExpressionFunction('xydimension', { + params: JSON.stringify(data.params), + aggType: data.aggType, + label: data.label, + visDimension: prepareVisDimension(data), + }); + + return buildExpression([xyDimension]); +}; + export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const schemas = getVisSchemas(vis, params); const dimensions: Dimensions = { @@ -62,15 +171,13 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params } } - const visConfig = { ...vis.params }; - (dimensions.y || []).forEach((yDimension) => { const yAgg = responseAggs[yDimension.accessor]; - const seriesParam = (visConfig.seriesParams || []).find( + const seriesParam = (vis.params.seriesParams || []).find( (param: any) => param.data.id === yAgg.id ); if (seriesParam) { - const usedValueAxis = (visConfig.valueAxes || []).find( + const usedValueAxis = (vis.params.valueAxes || []).find( (valueAxis: any) => valueAxis.id === seriesParam.valueAxis ); if (usedValueAxis?.scale.mode === 'percentage') { @@ -79,11 +186,34 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params } }); - visConfig.dimensions = dimensions; - const visTypeXy = buildExpressionFunction(visName, { type: vis.type.name as XyVisType, - visConfig: JSON.stringify(visConfig), + chartType: vis.params.type, + addTimeMarker: vis.params.addTimeMarker, + addLegend: vis.params.addLegend, + addTooltip: vis.params.addTooltip, + legendPosition: vis.params.legendPosition, + orderBucketsBySum: vis.params.orderBucketsBySum, + categoryAxes: vis.params.categoryAxes.map(prepareCategoryAxis), + valueAxes: vis.params.valueAxes.map(prepareValueAxis), + seriesParams: vis.params.seriesParams.map(prepareSeriesParam), + labels: prepareLabel(vis.params.labels), + thresholdLine: prepareThresholdLine(vis.params.thresholdLine), + gridCategoryLines: vis.params.grid.categoryLines, + gridValueAxis: vis.params.grid.valueAxis, + radiusRatio: vis.params.radiusRatio, + isVislibVis: vis.params.isVislibVis, + detailedTooltip: vis.params.detailedTooltip, + fittingFunction: vis.params.fittingFunction, + times: vis.params.times.map(prepareTimeMarker), + palette: vis.params.palette.name, + xDimension: dimensions.x ? prepareXYDimension(dimensions.x) : null, + yDimension: dimensions.y.map(prepareXYDimension), + zDimension: dimensions.z?.map(prepareXYDimension), + widthDimension: dimensions.width?.map(prepareXYDimension), + seriesDimension: dimensions.series?.map(prepareXYDimension), + splitRowDimension: dimensions.splitRow?.map(prepareXYDimension), + splitColumnDimension: dimensions.splitColumn?.map(prepareXYDimension), }); const ast = buildExpression([getEsaggsFn(vis), visTypeXy]); diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts index d5c5bfe004191..f025a36a82410 100644 --- a/src/plugins/vis_type_xy/public/types/config.ts +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -20,7 +20,7 @@ import { YDomainRange, } from '@elastic/charts'; -import { Dimension, Scale, ThresholdLine } from './param'; +import type { Dimension, Scale, ThresholdLine } from './param'; export interface Column { id: string | null; diff --git a/src/plugins/vis_type_xy/public/types/constants.ts b/src/plugins/vis_type_xy/public/types/constants.ts index 5c2f23b76aa96..05ed0783d4c68 100644 --- a/src/plugins/vis_type_xy/public/types/constants.ts +++ b/src/plugins/vis_type_xy/public/types/constants.ts @@ -6,52 +6,43 @@ * Side Public License, v 1. */ -import { $Values } from '@kbn/utility-types'; - -export const ChartMode = Object.freeze({ - Normal: 'normal' as const, - Stacked: 'stacked' as const, -}); -export type ChartMode = $Values; - -export const InterpolationMode = Object.freeze({ - Linear: 'linear' as const, - Cardinal: 'cardinal' as const, - StepAfter: 'step-after' as const, -}); -export type InterpolationMode = $Values; - -export const AxisType = Object.freeze({ - Category: 'category' as const, - Value: 'value' as const, -}); -export type AxisType = $Values; - -export const ScaleType = Object.freeze({ - Linear: 'linear' as const, - Log: 'log' as const, - SquareRoot: 'square root' as const, -}); -export type ScaleType = $Values; - -export const AxisMode = Object.freeze({ - Normal: 'normal' as const, - Percentage: 'percentage' as const, - Wiggle: 'wiggle' as const, - Silhouette: 'silhouette' as const, -}); -export type AxisMode = $Values; - -export const ThresholdLineStyle = Object.freeze({ - Full: 'full' as const, - Dashed: 'dashed' as const, - DotDashed: 'dot-dashed' as const, -}); -export type ThresholdLineStyle = $Values; - -export const ColorMode = Object.freeze({ - Background: 'Background' as const, - Labels: 'Labels' as const, - None: 'None' as const, -}); -export type ColorMode = $Values; +export enum ChartMode { + Normal = 'normal', + Stacked = 'stacked', +} + +export enum InterpolationMode { + Linear = 'linear', + Cardinal = 'cardinal', + StepAfter = 'step-after', +} + +export enum AxisType { + Category = 'category', + Value = 'value', +} + +export enum ScaleType { + Linear = 'linear', + Log = 'log', + SquareRoot = 'square root', +} + +export enum AxisMode { + Normal = 'normal', + Percentage = 'percentage', + Wiggle = 'wiggle', + Silhouette = 'silhouette', +} + +export enum ThresholdLineStyle { + Full = 'full', + Dashed = 'dashed', + DotDashed = 'dot-dashed', +} + +export enum ColorMode { + Background = 'Background', + Labels = 'Labels', + None = 'None', +} diff --git a/src/plugins/vis_type_xy/public/types/index.ts b/src/plugins/vis_type_xy/public/types/index.ts index d612e9fcf5f6f..6abbdfabaa956 100644 --- a/src/plugins/vis_type_xy/public/types/index.ts +++ b/src/plugins/vis_type_xy/public/types/index.ts @@ -9,4 +9,4 @@ export * from './constants'; export * from './config'; export * from './param'; -export * from './vis_type'; +export type { VisTypeNames, XyVisTypeDefinition } from './vis_type'; diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts index 69b6daf077a32..f90899620126a 100644 --- a/src/plugins/vis_type_xy/public/types/param.ts +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -6,13 +6,21 @@ * Side Public License, v 1. */ -import { Fit, Position } from '@elastic/charts'; - -import { Style, Labels, PaletteOutput } from '../../../charts/public'; -import { SchemaConfig } from '../../../visualizations/public'; - -import { ChartType } from '../../common'; -import { +import type { Fit, Position } from '@elastic/charts'; +import type { Style, Labels, PaletteOutput } from '../../../charts/public'; +import type { SchemaConfig } from '../../../visualizations/public'; +import type { ChartType, XyVisType } from '../../common'; +import type { + ExpressionValueCategoryAxis, + ExpressionValueSeriesParam, + ExpressionValueValueAxis, + ExpressionValueLabel, + ExpressionValueThresholdLine, + ExpressionValueTimeMarker, + ExpressionValueXYDimension, +} from '../expression_functions'; + +import type { ChartMode, AxisMode, AxisType, @@ -47,7 +55,7 @@ export interface CategoryAxis { * remove with vis_type_vislib * https://github.com/elastic/kibana/issues/56143 */ - style: Partial