From 552212cb11d66fee2912a7a881763761779d7ec9 Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Tue, 19 Nov 2024 10:43:59 +0100 Subject: [PATCH 01/42] [Security Solution] Remove a blog post callout from rule management page (#200650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Resolves: https://github.com/elastic/kibana/issues/197024** ## Summary PR https://github.com/elastic/kibana/pull/195943 added a callout banner to the 8.x branch. This banner was intended to be displayed only in ESS v8.16. We are now removing it to ensure it does not appear in v8.17.0. ## Screenshots **Before** Scherm­afbeelding 2024-11-18 om 21 14 22 **After** Scherm­afbeelding 2024-11-18 om 21 14 41 --- .../rule_management_ui/pages/rule_management/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 10dcf56d33a2c..17c5368a4b1c5 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -22,7 +22,6 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { MissingPrivilegesCallOut } from '../../../../detections/components/callouts/missing_privileges_callout'; import { MlJobCompatibilityCallout } from '../../../../detections/components/callouts/ml_job_compatibility_callout'; import { NeedAdminForUpdateRulesCallOut } from '../../../../detections/components/callouts/need_admin_for_update_callout'; -import { BlogPostDetectionEngineeringCallout } from '../../../../detections/components/callouts/blog_post_detection_engineering_callout'; import { AddElasticRulesButton } from '../../../../detections/components/rules/pre_packaged_rules/add_elastic_rules_button'; import { ValueListsFlyout } from '../../../../detections/components/value_lists_management_flyout'; import { useUserData } from '../../../../detections/components/user_info'; @@ -173,7 +172,6 @@ const RulesPageComponent: React.FC = () => { kibanaServices={kibanaServices} categories={[DEFAULT_APP_CATEGORIES.security.id]} /> - From cffa72b0a361876a56f89a60961bf902cc7db691 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:46:46 +1100 Subject: [PATCH 02/42] [8.x] [Fields Metadata] Restrict access to integration fields by privileges (#199774) (#200676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Fields Metadata] Restrict access to integration fields by privileges (#199774)](https://github.com/elastic/kibana/pull/199774) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Marco Antonio Ghiani --- .../plugins/fields_metadata/server/plugin.ts | 4 ++-- .../fields_metadata/find_fields_metadata.ts | 7 ++++--- .../fields_metadata_client.test.ts | 18 +++++++++++++++++- .../fields_metadata/fields_metadata_client.ts | 19 +++++++++++++++++-- .../fields_metadata_service.mock.ts | 5 ++++- .../fields_metadata_service.ts | 11 ++++++++--- .../server/services/fields_metadata/types.ts | 3 ++- 7 files changed, 54 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/fields_metadata/server/plugin.ts b/x-pack/plugins/fields_metadata/server/plugin.ts index 70ccb7b5d30a3..144b03d6f6786 100644 --- a/x-pack/plugins/fields_metadata/server/plugin.ts +++ b/x-pack/plugins/fields_metadata/server/plugin.ts @@ -56,8 +56,8 @@ export class FieldsMetadataPlugin }; } - public start(_core: CoreStart, _plugins: FieldsMetadataServerPluginStartDeps) { - const fieldsMetadata = this.fieldsMetadataService.start(); + public start(core: CoreStart, _plugins: FieldsMetadataServerPluginStartDeps) { + const fieldsMetadata = this.fieldsMetadataService.start(core); return { getClient: fieldsMetadata.getClient }; } diff --git a/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts b/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts index 422c16a726843..9d186b57dd8dc 100644 --- a/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts +++ b/x-pack/plugins/fields_metadata/server/routes/fields_metadata/find_fields_metadata.ts @@ -27,7 +27,8 @@ export const initFindFieldsMetadataRoute = ({ security: { authz: { enabled: false, - reason: 'This route is opted out from authorization', + reason: + 'This route is opted out from authorization to keep available the access to static fields metadata such as ECS fields. For other sources (fleet integrations), appropriate checks are performed at the API level.', }, }, validate: { @@ -38,9 +39,9 @@ export const initFindFieldsMetadataRoute = ({ }, async (_requestContext, request, response) => { const { attributes, fieldNames, integration, dataset } = request.query; - const [_core, _startDeps, startContract] = await getStartServices(); - const fieldsMetadataClient = startContract.getClient(); + + const fieldsMetadataClient = await startContract.getClient(request); try { const fieldsDictionary = await fieldsMetadataClient.find({ diff --git a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.test.ts b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.test.ts index 4ef2e3c693fb5..f59380b948e2d 100644 --- a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.test.ts +++ b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.test.ts @@ -103,10 +103,11 @@ describe('FieldsMetadataClient class', () => { integrationListExtractor, }); fieldsMetadataClient = FieldsMetadataClient.create({ + capabilities: { fleet: { read: true }, fleetv2: { read: true } }, + logger, ecsFieldsRepository, integrationFieldsRepository, metadataFieldsRepository, - logger, }); }); @@ -184,6 +185,21 @@ describe('FieldsMetadataClient class', () => { expect(integrationFieldsExtractor).not.toHaveBeenCalled(); expect(unknownFieldInstance).toBeUndefined(); }); + + it('should not resolve the field from an integration if the user has not the fleet privileges to access it', async () => { + const clientWithouthPrivileges = FieldsMetadataClient.create({ + capabilities: { fleet: { read: false }, fleetv2: { read: false } }, + logger, + ecsFieldsRepository, + integrationFieldsRepository, + metadataFieldsRepository, + }); + + const fieldInstance = await clientWithouthPrivileges.getByName('mysql.slowlog.filesort'); + + expect(integrationFieldsExtractor).not.toHaveBeenCalled(); + expect(fieldInstance).toBeUndefined(); + }); }); describe('#find', () => { diff --git a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.ts b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.ts index baaac903a7b3a..4aa0d8c1a4c71 100644 --- a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.ts +++ b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger } from '@kbn/core/server'; +import { Capabilities, Logger } from '@kbn/core/server'; import { FieldName, FieldMetadata, FieldsMetadataDictionary } from '../../../common'; import { EcsFieldsRepository } from './repositories/ecs_fields_repository'; import { IntegrationFieldsRepository } from './repositories/integration_fields_repository'; @@ -13,7 +13,13 @@ import { MetadataFieldsRepository } from './repositories/metadata_fields_reposit import { IntegrationFieldsSearchParams } from './repositories/types'; import { FindFieldsMetadataOptions, IFieldsMetadataClient } from './types'; +interface FleetCapabilities { + fleet: Capabilities[string]; + fleetv2: Capabilities[string]; +} + interface FieldsMetadataClientDeps { + capabilities: FleetCapabilities; logger: Logger; ecsFieldsRepository: EcsFieldsRepository; metadataFieldsRepository: MetadataFieldsRepository; @@ -22,6 +28,7 @@ interface FieldsMetadataClientDeps { export class FieldsMetadataClient implements IFieldsMetadataClient { private constructor( + private readonly capabilities: FleetCapabilities, private readonly logger: Logger, private readonly ecsFieldsRepository: EcsFieldsRepository, private readonly metadataFieldsRepository: MetadataFieldsRepository, @@ -43,7 +50,7 @@ export class FieldsMetadataClient implements IFieldsMetadataClient { } // 2. Try searching for the fiels in the Elastic Package Registry - if (!field) { + if (!field && this.hasFleetPermissions(this.capabilities)) { field = await this.integrationFieldsRepository.getByName(fieldName, { integration, dataset }); } @@ -74,13 +81,21 @@ export class FieldsMetadataClient implements IFieldsMetadataClient { return FieldsMetadataDictionary.create(fields); } + private hasFleetPermissions(capabilities: FleetCapabilities) { + const { fleet, fleetv2 } = capabilities; + + return fleet.read && fleetv2.read; + } + public static create({ + capabilities, logger, ecsFieldsRepository, metadataFieldsRepository, integrationFieldsRepository, }: FieldsMetadataClientDeps) { return new FieldsMetadataClient( + capabilities, logger, ecsFieldsRepository, metadataFieldsRepository, diff --git a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.mock.ts b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.mock.ts index b6395d4c96f6b..62ffc231fe837 100644 --- a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.mock.ts +++ b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { KibanaRequest } from '@kbn/core-http-server'; import { createFieldsMetadataClientMock } from './fields_metadata_client.mock'; import { FieldsMetadataServiceSetup, FieldsMetadataServiceStart } from './types'; @@ -16,5 +17,7 @@ export const createFieldsMetadataServiceSetupMock = export const createFieldsMetadataServiceStartMock = (): jest.Mocked => ({ - getClient: jest.fn(() => createFieldsMetadataClientMock()), + getClient: jest.fn((_request: KibanaRequest) => + Promise.resolve(createFieldsMetadataClientMock()) + ), }); diff --git a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.ts b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.ts index dc8aa976e34be..6e00572c21070 100644 --- a/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.ts +++ b/x-pack/plugins/fields_metadata/server/services/fields_metadata/fields_metadata_service.ts @@ -6,7 +6,7 @@ */ import { EcsFlat as ecsFields } from '@elastic/ecs'; -import { Logger } from '@kbn/core/server'; +import { CoreStart, Logger } from '@kbn/core/server'; import { FieldsMetadataClient } from './fields_metadata_client'; import { EcsFieldsRepository } from './repositories/ecs_fields_repository'; import { IntegrationFieldsRepository } from './repositories/integration_fields_repository'; @@ -32,7 +32,7 @@ export class FieldsMetadataService { }; } - public start(): FieldsMetadataServiceStart { + public start(core: CoreStart): FieldsMetadataServiceStart { const { logger, integrationFieldsExtractor, integrationListExtractor } = this; const ecsFieldsRepository = EcsFieldsRepository.create({ ecsFields }); @@ -43,8 +43,13 @@ export class FieldsMetadataService { }); return { - getClient() { + getClient: async (request) => { + const { fleet, fleetv2 } = await core.capabilities.resolveCapabilities(request, { + capabilityPath: '*', + }); + return FieldsMetadataClient.create({ + capabilities: { fleet, fleetv2 }, logger, ecsFieldsRepository, metadataFieldsRepository, diff --git a/x-pack/plugins/fields_metadata/server/services/fields_metadata/types.ts b/x-pack/plugins/fields_metadata/server/services/fields_metadata/types.ts index 533b4fd0bb2c2..7e094fbbbf8b5 100644 --- a/x-pack/plugins/fields_metadata/server/services/fields_metadata/types.ts +++ b/x-pack/plugins/fields_metadata/server/services/fields_metadata/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { KibanaRequest } from '@kbn/core/server'; import { FieldName, FieldMetadata, FieldsMetadataDictionary } from '../../../common'; import { IntegrationFieldsExtractor, @@ -23,7 +24,7 @@ export interface FieldsMetadataServiceSetup { } export interface FieldsMetadataServiceStart { - getClient(): IFieldsMetadataClient; + getClient(request: KibanaRequest): Promise; } export interface FindFieldsMetadataOptions extends Partial { From 14d85755d92fdff5bc794b70bac29e23f0a2f4c5 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:59:53 +1100 Subject: [PATCH 03/42] [8.x] [ML][ES|QL] Adds query guardrails and technical preview badge to ES|QL data visualizer (#200325) (#200677) # Backport This will backport the following commits from `main` to `8.x`: - [[ML][ES|QL] Adds query guardrails and technical preview badge to ES|QL data visualizer (#200325)](https://github.com/elastic/kibana/pull/200325) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Quynh Nguyen (Quinn) <43350163+qn895@users.noreply.github.com> --- .../expanded_row/index_based_expanded_row.tsx | 12 ++- .../data_visualizer_stats_table.tsx | 5 +- .../stats_table/use_table_settings.ts | 7 +- .../index_data_visualizer_esql.tsx | 1 + .../search_panel/esql/limit_size.tsx | 8 -- .../constants/esql_constants.ts | 2 +- .../embeddable_esql_field_stats_table.tsx | 1 + .../esql/use_data_visualizer_esql_data.tsx | 42 ++++++++- .../hooks/esql/use_esql_overall_stats_data.ts | 1 - .../index_data_visualizer.tsx | 40 +++------ x-pack/plugins/data_visualizer/tsconfig.json | 1 - .../datavisualizer_selector.tsx | 15 +++- .../index_based/index_data_visualizer.tsx | 7 +- .../data_visualizer/esql_data_visualizer.ts | 26 +++--- .../functional/services/ml/data_visualizer.ts | 4 +- .../services/ml/data_visualizer_table.ts | 88 ++++++++++--------- 16 files changed, 155 insertions(+), 105 deletions(-) diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx index e0c83f36399e9..2982df103bded 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/expanded_row/index_based_expanded_row.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { useExpandedRowCss } from './use_expanded_row_css'; import { GeoPointContentWithMap } from './geo_point_content_with_map'; @@ -34,6 +34,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({ totalDocuments, timeFieldName, typeAccessor = 'type', + onVisibilityChange, }: { item: FieldVisConfig; dataView: DataView | undefined; @@ -46,6 +47,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({ */ onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void; timeFieldName?: string; + onVisibilityChange?: (visible: boolean, item: FieldVisConfig) => void; }) => { const config = { ...item, stats: { ...item.stats, totalDocuments } }; const { loading, existsInDocs, fieldName } = config; @@ -98,6 +100,14 @@ export const IndexBasedDataVisualizerExpandedRow = ({ } } + useEffect(() => { + onVisibilityChange?.(true, item); + + return () => { + onVisibilityChange?.(false, item); + }; + }, [item, onVisibilityChange]); + return (
{loading === true ? : getCardContent()} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index 3b591a85ff472..e0e43efb694f2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -63,6 +63,7 @@ interface DataVisualizerTableProps { overallStatsRunning: boolean; renderFieldName?: FieldStatisticTableEmbeddableProps['renderFieldName']; error?: Error | string; + isEsql?: boolean; } const UnmemoizedDataVisualizerTable = ({ @@ -78,6 +79,7 @@ const UnmemoizedDataVisualizerTable = ({ overallStatsRunning, renderFieldName, error, + isEsql = false, }: DataVisualizerTableProps) => { const { euiTheme } = useEuiTheme(); @@ -87,7 +89,8 @@ const UnmemoizedDataVisualizerTable = ({ const { onTableChange, pagination, sorting } = useTableSettings( items, pageState, - updatePageState + updatePageState, + isEsql ); const [showDistributions, setShowDistributions] = useState(showPreviewByDefault ?? true); const [dimensions, setDimensions] = useState(calculateTableColumnsDimensions()); diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts index b2292970230c0..be427a6ebccae 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/use_table_settings.ts @@ -27,7 +27,8 @@ interface UseTableSettingsReturnValue { export function useTableSettings( items: TypeOfItem[], pageState: DataVisualizerTableState, - updatePageState: (update: DataVisualizerTableState) => void + updatePageState: (update: DataVisualizerTableState) => void, + isEsql: boolean = false ): UseTableSettingsReturnValue { const { pageIndex, pageSize, sortField, sortDirection } = pageState; @@ -50,9 +51,9 @@ export function useTableSettings( pageIndex, pageSize, totalItemCount: items.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, + pageSizeOptions: isEsql ? [10, 25] : PAGE_SIZE_OPTIONS, }), - [items, pageIndex, pageSize] + [items, pageIndex, pageSize, isEsql] ); const sorting = useMemo( diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx index c6190c87bcae5..5953144e715fb 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -332,6 +332,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi + isEsql={true} items={configs} pageState={dataVisualizerListState} updatePageState={onTableChange} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx index 2323b231d67f7..26b8ff3cf24d7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_data_visualizer_esql_data.tsx @@ -53,14 +53,14 @@ const defaultSearchQuery = { }; const FALLBACK_ESQL_QUERY: ESQLQuery = { esql: '' }; -const DEFAULT_LIMIT_SIZE = '10000'; +const DEFAULT_LIMIT_SIZE = '5000'; const defaults = getDefaultPageState(); export const getDefaultESQLDataVisualizerListState = ( overrides?: Partial ): Required => ({ pageIndex: 0, - pageSize: 25, + pageSize: 10, sortField: 'fieldName', sortDirection: 'asc', visibleFieldTypes: [], @@ -70,7 +70,7 @@ export const getDefaultESQLDataVisualizerListState = ( searchQuery: defaultSearchQuery, searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, filters: [], - showDistributions: true, + showDistributions: false, showAllFields: false, showEmptyFields: false, probability: null, @@ -229,6 +229,21 @@ export const useESQLDataVisualizerData = ( } as QueryDslQueryContainer; } } + + // Ensure that we don't query frozen data + if (filter.bool === undefined) { + filter.bool = Object.create(null); + } + + if (filter.bool && filter.bool.must_not === undefined) { + filter.bool.must_not = []; + } + + if (filter.bool && Array.isArray(filter?.bool?.must_not)) { + filter.bool.must_not!.push({ + term: { _tier: 'data_frozen' }, + }); + } return { id: input.id, earliest, @@ -332,9 +347,25 @@ export const useESQLDataVisualizerData = ( const visibleFieldTypes = dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes; + const [expandedRows, setExpandedRows] = useState([]); + + const onVisibilityChange = useCallback((visible: boolean, item: FieldVisConfig) => { + if (visible) { + setExpandedRows((prev) => [...prev, item.fieldName]); + } else { + setExpandedRows((prev) => prev.filter((fieldName) => fieldName !== item.fieldName)); + } + }, []); + + const hasExpandedRows = useMemo(() => expandedRows.length > 0, [expandedRows]); useEffect( function updateFieldStatFieldsToFetch() { + if (dataVisualizerListState?.showDistributions === false && !hasExpandedRows) { + setFieldStatFieldsToFetch(undefined); + return; + } + const { sortField, sortDirection } = dataVisualizerListState; // Otherwise, sort the list of fields by the initial sort field and sort direction @@ -376,6 +407,8 @@ export const useESQLDataVisualizerData = ( dataVisualizerListState.sortDirection, nonMetricConfigs, metricConfigs, + dataVisualizerListState?.showDistributions, + hasExpandedRows, ] ); @@ -618,6 +651,7 @@ export const useESQLDataVisualizerData = ( typeAccessor="secondaryType" timeFieldName={timeFieldName} onAddFilter={input.onAddFilter} + onVisibilityChange={onVisibilityChange} /> ); } @@ -625,7 +659,7 @@ export const useESQLDataVisualizerData = ( }, {} as ItemIdToExpandedRowMap); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [currentDataView, totalCount, query.esql, timeFieldName] + [currentDataView, totalCount, query.esql, timeFieldName, onVisibilityChange] ); const combinedProgress = useMemo( diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts index 3d023a6fc3811..7ea012d2d4b28 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts @@ -313,7 +313,6 @@ export const useESQLOverallStatsData = ( { strategy: ESQL_ASYNC_SEARCH_STRATEGY } )) as ESQLResponse | undefined; setQueryHistoryStatus(false); - const columnInfo = columnsResp?.rawResponse ? columnsResp.rawResponse.all_columns ?? columnsResp.rawResponse.columns : []; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx index 745e03da10d09..8a5e34f58a10f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx @@ -16,7 +16,6 @@ import { i18n } from '@kbn/i18n'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; -import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { StorageContextProvider } from '@kbn/ml-local-storage'; import type { DataView } from '@kbn/data-views-plugin/public'; import { getNestedProperty } from '@kbn/ml-nested-property'; @@ -49,8 +48,6 @@ import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_ import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants'; import { DV_STORAGE_KEYS } from './types/storage'; -const XXL_BREAKPOINT = 1400; - const localStorage = new Storage(window.localStorage); export interface DataVisualizerStateContextProviderProps { @@ -341,29 +338,20 @@ export const IndexDataVisualizer: FC = ({ return ( - - - - - {!esql ? ( - - ) : ( - - )} - - - - + + + + {!esql ? ( + + ) : ( + + )} + + + ); }; diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 970526cdf464e..9e1c19c84067b 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -76,7 +76,6 @@ "@kbn/ml-time-buckets", "@kbn/aiops-log-rate-analysis", "@kbn/react-kibana-context-render", - "@kbn/react-kibana-context-theme", "@kbn/presentation-publishing", "@kbn/shared-ux-utility", "@kbn/search-types", diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 41b2ac3a47d37..41291e3ac5057 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -18,6 +18,7 @@ import { EuiLink, EuiSpacer, EuiText, + EuiBetaBadge, EuiTextAlign, } from '@elastic/eui'; @@ -64,7 +65,6 @@ export const DatavisualizerSelector: FC = () => { }, } = useMlKibana(); const isEsqlEnabled = useMemo(() => uiSettings.get(ENABLE_ESQL), [uiSettings]); - const helpLink = docLinks.links.ml.guide; const navigateToPath = useNavigateToPath(); @@ -172,6 +172,19 @@ export const DatavisualizerSelector: FC = () => { {' '} + + } + tooltipPosition={'right'} /> diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx index e85da14ddb808..cd06783ddc17b 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/index_data_visualizer.tsx @@ -16,7 +16,7 @@ import type { GetAdditionalLinksParams, } from '@kbn/data-visualizer-plugin/public'; import { useTimefilter } from '@kbn/ml-date-picker'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import useMountedState from 'react-use/lib/useMountedState'; import { useMlApi, useMlKibana, useMlLocator } from '../../contexts/kibana'; import { HelpMenu } from '../../components/help_menu'; @@ -26,6 +26,7 @@ import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_ import { checkPermission } from '../../capabilities/check_capabilities'; import { MlPageHeader } from '../../components/page_header'; import { useEnabledFeatures } from '../../contexts/ml'; +import { TechnicalPreviewBadge } from '../../components/technical_preview_badge'; export const IndexDataVisualizerPage: FC<{ esql: boolean }> = ({ esql = false }) => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { @@ -188,6 +189,7 @@ export const IndexDataVisualizerPage: FC<{ esql: boolean }> = ({ esql = false }) // eslint-disable-next-line react-hooks/exhaustive-deps [mlLocator, mlFeaturesDisabled] ); + const { euiTheme } = useEuiTheme(); return IndexDataVisualizer ? ( {IndexDataVisualizer !== null ? ( @@ -203,6 +205,9 @@ export const IndexDataVisualizerPage: FC<{ esql: boolean }> = ({ esql = false }) + + + ) : null} diff --git a/x-pack/test/functional/apps/ml/data_visualizer/esql_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/esql_data_visualizer.ts index f6fe276ac33b7..96e01c67ff91c 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/esql_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/esql_data_visualizer.ts @@ -39,7 +39,7 @@ const esqlFarequoteData = { sourceIndexOrSavedSearch: 'ft_farequote', expected: { hasDocCountChart: true, - initialLimitSize: '10,000 (100%)', + initialLimitSize: '5,000 (100%)', totalDocCountFormatted: '86,274', metricFields: [ { @@ -48,7 +48,7 @@ const esqlFarequoteData = { existsInDocs: true, aggregatable: true, loading: false, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', statsMaxDecimalPlaces: 3, topValuesCount: 11, viewableInLens: false, @@ -61,7 +61,7 @@ const esqlFarequoteData = { existsInDocs: true, aggregatable: true, loading: false, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', exampleCount: 2, viewableInLens: false, }, @@ -72,7 +72,7 @@ const esqlFarequoteData = { aggregatable: false, loading: false, exampleCount: 1, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', viewableInLens: false, }, { @@ -82,7 +82,7 @@ const esqlFarequoteData = { aggregatable: true, loading: false, exampleCount: 1, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', viewableInLens: false, }, { @@ -92,7 +92,7 @@ const esqlFarequoteData = { aggregatable: true, loading: false, exampleCount: 10, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', viewableInLens: false, }, { @@ -102,7 +102,7 @@ const esqlFarequoteData = { aggregatable: false, loading: false, exampleCount: 1, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', viewableInLens: false, }, { @@ -112,7 +112,7 @@ const esqlFarequoteData = { aggregatable: true, loading: false, exampleCount: 1, - docCountFormatted: '86,274 (100%)', + docCountFormatted: '10,000 (100%)', viewableInLens: false, }, ], @@ -253,7 +253,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { it(`${testData.suiteTitle} updates data when limit size changes`, async () => { if (testData.expected.initialLimitSize !== undefined) { - await ml.testExecution.logTestStep('shows analysis for 10,000 rows by default'); + await ml.testExecution.logTestStep('shows analysis for 5,000 rows by default'); for (const fieldRow of testData.expected.metricFields as Array< Required >) { @@ -263,13 +263,13 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { undefined, false, false, - true + false ); } } - await ml.testExecution.logTestStep('sets limit size to Analyze all'); - await ml.dataVisualizer.setLimitSize(100000); + await ml.testExecution.logTestStep('sets limit size to 10,000 rows'); + await ml.dataVisualizer.setLimitSize(10000); await ml.testExecution.logTestStep('updates table with newly set limit size'); for (const fieldRow of testData.expected.metricFields as Array< @@ -281,7 +281,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { undefined, false, false, - true + false ); } diff --git a/x-pack/test/functional/services/ml/data_visualizer.ts b/x-pack/test/functional/services/ml/data_visualizer.ts index 33d4e2f8f68ba..8597492a50a11 100644 --- a/x-pack/test/functional/services/ml/data_visualizer.ts +++ b/x-pack/test/functional/services/ml/data_visualizer.ts @@ -99,14 +99,14 @@ export function MachineLearningDataVisualizerProvider({ getService }: FtrProvide await testSubjects.existOrFail(`dvESQLLimitSize-${size}`, { timeout: 1000 }); }, - async setLimitSize(size: 5000 | 10000 | 100000) { + async setLimitSize(size: 5000 | 10000) { await retry.tryForTime(5000, async () => { // escape popover await browser.pressKeys(browser.keys.ESCAPE); // Once clicked, show list of options await testSubjects.clickWhenNotDisabled('dvESQLLimitSizeSelect'); - for (const option of [5000, 10000, 100000]) { + for (const option of [5000, 10000]) { await testSubjects.existOrFail(`dvESQLLimitSize-${option}`, { timeout: 1000 }); } 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 a6f936e43bf37..9bf1baf4a33d5 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -398,29 +398,31 @@ export function MachineLearningDataVisualizerTableProvider( await this.assertFieldDocCount(fieldName, docCountFormatted); await this.ensureDetailsOpen(fieldName); - await testSubjects.existOrFail( - this.detailsSelector(fieldName, 'dataVisualizerNumberSummaryTable') - ); - - if (topValuesCount !== undefined) { + await retry.tryForTime(3000, async () => { await testSubjects.existOrFail( - this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValues') + this.detailsSelector(fieldName, 'dataVisualizerNumberSummaryTable') ); - await this.assertTopValuesCount(fieldName, topValuesCount); - } - if (checkDistributionPreviewExist) { - await this.assertDistributionPreviewExist(fieldName); - } - if (viewableInLens) { - if (hasActionMenu) { - await this.assertActionMenuViewInLensEnabled(fieldName, true); + if (topValuesCount !== undefined) { + await testSubjects.existOrFail( + this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValues') + ); + await this.assertTopValuesCount(fieldName, topValuesCount); + } + + if (checkDistributionPreviewExist) { + await this.assertDistributionPreviewExist(fieldName); + } + if (viewableInLens) { + if (hasActionMenu) { + await this.assertActionMenuViewInLensEnabled(fieldName, true); + } else { + await this.assertViewInLensActionEnabled(fieldName, true); + } } else { - await this.assertViewInLensActionEnabled(fieldName, true); + await this.assertViewInLensActionNotExists(fieldName); } - } else { - await this.assertViewInLensActionNotExists(fieldName); - } + }); await this.ensureDetailsClosed(fieldName); } @@ -525,33 +527,35 @@ export function MachineLearningDataVisualizerTableProvider( hasActionMenu?: boolean, exampleContent?: string[] ) { - // Currently the data used in the data visualizer tests only contains these field types. - if (fieldType === ML_JOB_FIELD_TYPES.DATE) { - await this.assertDateFieldContents(fieldName, docCountFormatted); - } else if (fieldType === ML_JOB_FIELD_TYPES.KEYWORD) { - await this.assertKeywordFieldContents( - fieldName, - docCountFormatted, - exampleCount, - exampleContent - ); - } else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) { - await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount); - } else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) { - await this.assertGeoPointFieldContents(fieldName, docCountFormatted, exampleCount); - } else if (fieldType === ML_JOB_FIELD_TYPES.UNKNOWN) { - await this.assertUnknownFieldContents(fieldName, docCountFormatted); - } + await retry.tryForTime(3000, async () => { + // Currently the data used in the data visualizer tests only contains these field types. + if (fieldType === ML_JOB_FIELD_TYPES.DATE) { + await this.assertDateFieldContents(fieldName, docCountFormatted); + } else if (fieldType === ML_JOB_FIELD_TYPES.KEYWORD) { + await this.assertKeywordFieldContents( + fieldName, + docCountFormatted, + exampleCount, + exampleContent + ); + } else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) { + await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount); + } else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) { + await this.assertGeoPointFieldContents(fieldName, docCountFormatted, exampleCount); + } else if (fieldType === ML_JOB_FIELD_TYPES.UNKNOWN) { + await this.assertUnknownFieldContents(fieldName, docCountFormatted); + } - if (viewableInLens) { - if (hasActionMenu) { - await this.assertActionMenuViewInLensEnabled(fieldName, true); + if (viewableInLens) { + if (hasActionMenu) { + await this.assertActionMenuViewInLensEnabled(fieldName, true); + } else { + await this.assertViewInLensActionEnabled(fieldName, true); + } } else { - await this.assertViewInLensActionEnabled(fieldName, true); + await this.assertViewInLensActionNotExists(fieldName); } - } else { - await this.assertViewInLensActionNotExists(fieldName); - } + }); } public async assertLensActionShowChart(fieldName: string, visualizationContainer?: string) { From e4939a8468cf9eb0085d1f5f79e2ffd595effc19 Mon Sep 17 00:00:00 2001 From: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:18:44 +0100 Subject: [PATCH 04/42] [8.x] Migrate `/test/apm_api_integration/tests/suggestions` to be deployment agnostic api tests (#200556) (#200678) # Backport This will backport the following commits from `main` to `8.x`: - [Migrate `/test/apm_api_integration/tests/suggestions` to be deployment agnostic api tests (#200556)](https://github.com/elastic/kibana/pull/200556) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../apis/observability/apm/index.ts | 2 ++ .../apm}/suggestions/generate_data.ts | 0 .../observability/apm/suggestions/index.ts | 14 ++++++++++++++ .../apm}/suggestions/suggestions.spec.ts | 17 ++++++++++------- .../apm}/throughput/dependencies_apis.spec.ts | 18 ++++++++++-------- .../observability/apm/throughput/index.ts | 16 ++++++++++++++++ .../apm}/throughput/service_apis.spec.ts | 18 ++++++++++-------- .../apm}/throughput/service_maps.spec.ts | 19 ++++++++++--------- 8 files changed, 72 insertions(+), 32 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/suggestions/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/suggestions/suggestions.spec.ts (94%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/dependencies_apis.spec.ts (94%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/service_apis.spec.ts (92%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/service_maps.spec.ts (90%) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index 28a1f0a5c1e93..558777e459bd7 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -36,5 +36,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./diagnostics')); loadTestFile(require.resolve('./service_nodes')); loadTestFile(require.resolve('./span_links')); + loadTestFile(require.resolve('./suggestions')); + loadTestFile(require.resolve('./throughput')); }); } diff --git a/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts new file mode 100644 index 0000000000000..9b2563c093a9d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Suggestions', () => { + loadTestFile(require.resolve('./suggestions.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts index d4d1c3b141700..a6e9342885571 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts @@ -11,7 +11,8 @@ import { TRANSACTION_TYPE, } from '@kbn/apm-plugin/common/es_fields/apm'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateData } from './generate_data'; const startNumber = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -20,14 +21,16 @@ const endNumber = new Date('2021-01-01T00:05:00.000Z').getTime() - 1; const start = new Date(startNumber).toISOString(); const end = new Date(endNumber).toISOString(); -export default function suggestionsTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function suggestionsTests({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('suggestions when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; - // FLAKY: https://github.com/elastic/kibana/issues/177538 - registry.when('suggestions when data is loaded', { config: 'basic', archives: [] }, async () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start: startNumber, diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts index fe591631fafe7..84d293f287b2f 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts @@ -8,13 +8,13 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { DependencyNode, ServiceNode } from '@kbn/apm-plugin/common/connections'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -93,11 +93,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputValues: Awaited>; - // FLAKY: https://github.com/elastic/kibana/issues/177536 - registry.when.skip('Dependencies throughput value', { config: 'basic', archives: [] }, () => { + describe('Dependencies throughput value', () => { describe('when data is loaded', () => { const GO_PROD_RATE = 75; const JAVA_PROD_RATE = 25; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) @@ -105,6 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceJavaInstance = apm .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts new file mode 100644 index 0000000000000..e0176b18be783 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Throughput', () => { + loadTestFile(require.resolve('./dependencies_apis.spec.ts')); + loadTestFile(require.resolve('./service_apis.spec.ts')); + loadTestFile(require.resolve('./service_maps.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts similarity index 92% rename from x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts index 9d69ce74bf0ea..429d29090a1d2 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts @@ -11,13 +11,13 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { meanBy, sumBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -141,11 +141,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - // FLAKY: https://github.com/elastic/kibana/issues/177535 - registry.when('Services APIs', { config: 'basic', archives: [] }, () => { + describe('Services APIs', () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) @@ -153,6 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceGoDevInstance = apm .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts similarity index 90% rename from x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts index 5ee475344e286..883e81ea24524 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts @@ -9,13 +9,13 @@ import expect from '@kbn/expect'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -83,10 +83,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - registry.when('Service Maps APIs', { config: 'trial', archives: [] }, () => { + describe('Service Maps APIs', () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) @@ -94,6 +96,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceGoDevInstance = apm .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) @@ -119,7 +122,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(() => apmSynthtraceEsClient.clean()); - // FLAKY: https://github.com/elastic/kibana/issues/176984 describe('compare throughput value between service inventory and service maps', () => { before(async () => { [throughputTransactionValues, throughputMetricValues] = await Promise.all([ @@ -136,7 +138,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/176987 describe('when calling service maps transactions stats api', () => { let serviceMapsNodeThroughput: number | null | undefined; before(async () => { From 99e3f675c6d8bf68193fbe3d9c42a65d0a4e2a33 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:39:20 +1100 Subject: [PATCH 05/42] [8.x] [Infra] Use callback for logger.trace calls (#199805) (#200684) # Backport This will backport the following commits from `main` to `8.x`: - [[Infra] Use callback for logger.trace calls (#199805)](https://github.com/elastic/kibana/pull/199805) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Dario Gieselaar --- .../lib/alerting/inventory_metric_threshold/lib/get_data.ts | 4 ++-- .../lib/alerting/metric_threshold/lib/check_missing_group.ts | 4 ++-- .../server/lib/alerting/metric_threshold/lib/get_data.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts index 207f2fcb7cb27..e911440ce5aa2 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts @@ -159,9 +159,9 @@ export const getData = async ( customMetric, fieldsExisted ); - logger.trace(`Request: ${JSON.stringify(request)}`); + logger.trace(() => `Request: ${JSON.stringify(request)}`); const body = await esClient.search(request); - logger.trace(`Response: ${JSON.stringify(body)}`); + logger.trace(() => `Response: ${JSON.stringify(body)}`); if (body.aggregations) { return handleResponse(body.aggregations, previousNodes); } diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/check_missing_group.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/check_missing_group.ts index f5e2a19cb70e9..d50c11710db76 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/check_missing_group.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/check_missing_group.ts @@ -57,9 +57,9 @@ export const checkMissingGroups = async ( ]; }); - logger.trace(`Request: ${JSON.stringify({ searches })}`); + logger.trace(() => `Request: ${JSON.stringify({ searches })}`); const response = await esClient.msearch({ searches }); - logger.trace(`Response: ${JSON.stringify(response)}`); + logger.trace(() => `Response: ${JSON.stringify(response)}`); const verifiedMissingGroups = response.responses .map((resp, index) => { diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts index d2afb40cecf50..e30edbeac9360 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/lib/get_data.ts @@ -277,10 +277,10 @@ export const getData = async ( fieldsExisted ), }; - logger.trace(`Request: ${JSON.stringify(request)}`); + logger.trace(() => `Request: ${JSON.stringify(request)}`); const body = await esClient.search(request); const { aggregations, _shards } = body; - logger.trace(`Response: ${JSON.stringify(body)}`); + logger.trace(() => `Response: ${JSON.stringify(body)}`); if (aggregations) { return handleResponse(aggregations, previousResults, _shards.successful); } else if (_shards.successful) { From 0ee05fe5876ba61d01cf1de3bdf81f422d437bf4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 19 Nov 2024 21:51:06 +1100 Subject: [PATCH 06/42] [8.x] [SecuritySolution] Update file validation because the file type is empty on windows (#199791) (#200189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[SecuritySolution] Update file validation because the file type is empty on windows (#199791)](https://github.com/elastic/kibana/pull/199791) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Pablo Machado --- .../asset_criticality_file_uploader/constants.ts | 8 +++++++- .../asset_criticality_file_uploader/hooks.test.ts | 3 ++- .../asset_criticality_file_uploader/validations.test.ts | 8 ++++++++ .../asset_criticality_file_uploader/validations.ts | 5 ++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts index c64128274aa3d..966068eb4c074 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/constants.ts @@ -5,5 +5,11 @@ * 2.0. */ -export const SUPPORTED_FILE_TYPES = ['text/csv', 'text/plain', 'text/tab-separated-values']; +export const SUPPORTED_FILE_TYPES = [ + 'text/csv', + 'text/plain', + 'text/tab-separated-values', + '.tsv', // Useful for Windows when it can't recognise the file extension. + '.csv', // Useful for Windows when it can't recognise the file extension. +]; export const SUPPORTED_FILE_EXTENSIONS = ['CSV', 'TXT', 'TSV']; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.test.ts index 675535365a0b0..a3b9a2e0ce24e 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.test.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/hooks.test.ts @@ -37,12 +37,13 @@ describe('useFileValidation', () => { test('should call onError when an error occurs', () => { const onErrorMock = jest.fn(); const onCompleteMock = jest.fn(); + const invalidFileType = 'invalid file type'; const { result } = renderHook( () => useFileValidation({ onError: onErrorMock, onComplete: onCompleteMock }), { wrapper: TestProviders } ); - result.current(new File([invalidLine], 'test.csv')); + result.current(new File([invalidLine], 'test.csv', { type: invalidFileType })); expect(onErrorMock).toHaveBeenCalled(); expect(onCompleteMock).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.test.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.test.ts index 4e742d4d92505..4a77e024528dd 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.test.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.test.ts @@ -56,6 +56,14 @@ describe('validateFile', () => { expect(result.valid).toBe(true); }); + it('should return valid if the mime type is empty (Windows)', () => { + const file = new File(['file content'], 'test.csv', { type: '' }); + + const result = validateFile(file, formatBytes); + + expect(result.valid).toBe(true); + }); + it('should return an error message if the file type is invalid', () => { const file = new File(['file content'], 'test.txt', { type: 'invalid-type' }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts index 06018a2496768..6f8bd8fb816bf 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/validations.ts @@ -53,7 +53,10 @@ export const validateFile = ( file: File, formatBytes: (bytes: number) => string ): { valid: false; errorMessage: string; code: string } | { valid: true } => { - if (!SUPPORTED_FILE_TYPES.includes(file.type)) { + if ( + file.type !== '' && // file.type might be an empty string on windows + !SUPPORTED_FILE_TYPES.includes(file.type) + ) { return { valid: false, code: 'unsupported_file_type', From 1cd1b5c2cc305ff5a8d4378c7b8295e1a844f7d2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:06:09 +1100 Subject: [PATCH 07/42] [8.x] [Cloud Security] Fixed an issue with Host.name Alerts contextual flyout (#200626) (#200688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[Cloud Security] Fixed an issue with Host.name Alerts contextual flyout (#200626)](https://github.com/elastic/kibana/pull/200626) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Rickyanto Ang --- .../components/alerts/alerts_preview.tsx | 2 +- .../public/flyout/entity_details/host_right/index.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx index c832f12c93f78..a5f08527cdc77 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/components/alerts/alerts_preview.tsx @@ -225,7 +225,7 @@ export const AlertsPreview = ({ From 1d5d9f1fd46beb696425cc4064bf1d860599e18a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 00:05:13 +1100 Subject: [PATCH 08/42] [8.x] [ML] Fixing edit calendar locator (#200681) (#200705) # Backport This will backport the following commits from `main` to `8.x`: - [[ML] Fixing edit calendar locator (#200681)](https://github.com/elastic/kibana/pull/200681) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: James Gowdy --- x-pack/plugins/ml/public/locator/ml_locator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index f2807687110f6..0d3ba0189aa7a 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -125,6 +125,7 @@ export class MlLocatorDefinition implements LocatorDefinition { break; case ML_PAGES.CALENDARS_EDIT: path = formatEditCalendarUrl('', params.pageState); + break; case ML_PAGES.CALENDARS_DST_EDIT: path = formatEditCalendarDstUrl('', params.pageState); break; From af956f6c9edd625de60655bedbea33af94cfa164 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Tue, 19 Nov 2024 14:23:36 +0100 Subject: [PATCH 09/42] [8.x] Kibana Sustainable Architecture: Expose `StatusResponse` in core-status-common (#200524) (#200685) # Backport This will backport the following commits from `main` to `8.x`: - [Kibana Sustainable Architecture: Expose `StatusResponse` in core-status-common (#200524)](https://github.com/elastic/kibana/pull/200524) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- package.json | 1 - .../status/components/status_table.test.tsx | 2 +- .../status/components/version_header.test.tsx | 2 +- .../src/status/components/version_header.tsx | 2 +- .../src/status/lib/load_status.test.ts | 2 +- .../src/status/lib/load_status.ts | 4 ++-- .../src/status/lib/status_level.test.ts | 2 +- .../core-apps-browser-internal/tsconfig.json | 1 - .../core-status-common-internal/README.md | 3 --- .../core-status-common-internal/index.ts | 17 -------------- .../jest.config.js | 14 ------------ .../core-status-common-internal/kibana.jsonc | 5 ----- .../core-status-common-internal/package.json | 7 ------ .../core-status-common-internal/src/index.ts | 17 -------------- .../core-status-common-internal/tsconfig.json | 22 ------------------- .../core/status/core-status-common/index.ts | 13 +++++++++-- .../status/core-status-common/jest.config.js | 2 +- .../status/core-status-common/src/index.ts | 12 ---------- .../src/status.ts | 3 ++- .../status/core-status-common/tsconfig.json | 4 +++- .../src/routes/status.ts | 2 +- .../src/routes/status_response_schemas.ts | 6 ++--- .../core-status-server-internal/tsconfig.json | 1 - packages/kbn-manifest/index.ts | 2 +- .../public/progress_indicator.tsx | 2 +- src/plugins/interactive_setup/tsconfig.json | 2 +- tsconfig.base.json | 2 -- .../common/endpoint/utils/kibana_status.ts | 6 ++--- .../plugins/security_solution/tsconfig.json | 2 +- yarn.lock | 4 ---- 30 files changed, 35 insertions(+), 129 deletions(-) delete mode 100644 packages/core/status/core-status-common-internal/README.md delete mode 100644 packages/core/status/core-status-common-internal/index.ts delete mode 100644 packages/core/status/core-status-common-internal/jest.config.js delete mode 100644 packages/core/status/core-status-common-internal/kibana.jsonc delete mode 100644 packages/core/status/core-status-common-internal/package.json delete mode 100644 packages/core/status/core-status-common-internal/src/index.ts delete mode 100644 packages/core/status/core-status-common-internal/tsconfig.json delete mode 100644 packages/core/status/core-status-common/src/index.ts rename packages/core/status/{core-status-common-internal => core-status-common}/src/status.ts (92%) diff --git a/package.json b/package.json index f106615a5f2cf..9905b03e9e2f7 100644 --- a/package.json +++ b/package.json @@ -386,7 +386,6 @@ "@kbn/core-security-server-internal": "link:packages/core/security/core-security-server-internal", "@kbn/core-security-server-mocks": "link:packages/core/security/core-security-server-mocks", "@kbn/core-status-common": "link:packages/core/status/core-status-common", - "@kbn/core-status-common-internal": "link:packages/core/status/core-status-common-internal", "@kbn/core-status-server": "link:packages/core/status/core-status-server", "@kbn/core-status-server-internal": "link:packages/core/status/core-status-server-internal", "@kbn/core-test-helpers-deprecations-getters": "link:packages/core/test-helpers/core-test-helpers-deprecations-getters", diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx index 38d69311d741e..b9949a6decf44 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common-internal'; +import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common'; import { StatusTable } from './status_table'; const state = { diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx index 62e48467ae51f..6180860df780d 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; -import type { ServerVersion } from '@kbn/core-status-common-internal'; +import type { ServerVersion } from '@kbn/core-status-common'; import { VersionHeader } from './version_header'; const buildServerVersion = (parts: Partial = {}): ServerVersion => ({ diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx index 0dc64a3cb7db0..15c1f9d07a273 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx @@ -10,7 +10,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ServerVersion } from '@kbn/core-status-common-internal'; +import type { ServerVersion } from '@kbn/core-status-common'; interface VersionHeaderProps { version: ServerVersion; diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts index a63c5011dcaf8..c37db930de789 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts @@ -8,7 +8,7 @@ */ import { httpServiceMock } from '@kbn/core-http-browser-mocks'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { mocked } from '@kbn/core-metrics-collectors-server-mocks'; import { loadStatus } from './load_status'; diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts index f89e2196d2122..e8519030c3fdf 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts @@ -11,11 +11,11 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { NotificationsSetup } from '@kbn/core-notifications-browser'; -import type { ServiceStatusLevelId } from '@kbn/core-status-common'; import type { + ServiceStatusLevelId, StatusResponse, StatusInfoServiceStatus as ServiceStatus, -} from '@kbn/core-status-common-internal'; +} from '@kbn/core-status-common'; import type { DataType } from './format_number'; interface MetricMeta { diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts index 3d393bd8e4719..290845c4bdd08 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common-internal'; +import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common'; import { getLevelSortValue, groupByLevel, getHighestStatus } from './status_level'; import { FormattedStatus, StatusState } from './load_status'; diff --git a/packages/core/apps/core-apps-browser-internal/tsconfig.json b/packages/core/apps/core-apps-browser-internal/tsconfig.json index a18bb3421a1f4..9902b12732760 100644 --- a/packages/core/apps/core-apps-browser-internal/tsconfig.json +++ b/packages/core/apps/core-apps-browser-internal/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/core-application-browser", "@kbn/core-application-browser-internal", "@kbn/core-mount-utils-browser-internal", - "@kbn/core-status-common-internal", "@kbn/core-http-browser-internal", "@kbn/core-application-browser-mocks", "@kbn/core-notifications-browser-mocks", diff --git a/packages/core/status/core-status-common-internal/README.md b/packages/core/status/core-status-common-internal/README.md deleted file mode 100644 index f4e4af7fd3b3a..0000000000000 --- a/packages/core/status/core-status-common-internal/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/core-status-common-internal - -This package contains the common internal types for Core's `status` domain. diff --git a/packages/core/status/core-status-common-internal/index.ts b/packages/core/status/core-status-common-internal/index.ts deleted file mode 100644 index f6a7a29056145..0000000000000 --- a/packages/core/status/core-status-common-internal/index.ts +++ /dev/null @@ -1,17 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - StatusInfoCoreStatus, - StatusInfoServiceStatus, - StatusInfo, - StatusResponse, - ServerVersion, - ServerMetrics, -} from './src'; diff --git a/packages/core/status/core-status-common-internal/jest.config.js b/packages/core/status/core-status-common-internal/jest.config.js deleted file mode 100644 index bc848cd656199..0000000000000 --- a/packages/core/status/core-status-common-internal/jest.config.js +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/core/status/core-status-common-internal'], -}; diff --git a/packages/core/status/core-status-common-internal/kibana.jsonc b/packages/core/status/core-status-common-internal/kibana.jsonc deleted file mode 100644 index 20ce17ae3cefa..0000000000000 --- a/packages/core/status/core-status-common-internal/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/core-status-common-internal", - "owner": "@elastic/kibana-core" -} diff --git a/packages/core/status/core-status-common-internal/package.json b/packages/core/status/core-status-common-internal/package.json deleted file mode 100644 index d2c456b6dc96a..0000000000000 --- a/packages/core/status/core-status-common-internal/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@kbn/core-status-common-internal", - "private": true, - "version": "1.0.0", - "author": "Kibana Core", - "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" -} \ No newline at end of file diff --git a/packages/core/status/core-status-common-internal/src/index.ts b/packages/core/status/core-status-common-internal/src/index.ts deleted file mode 100644 index 60c51dcf47632..0000000000000 --- a/packages/core/status/core-status-common-internal/src/index.ts +++ /dev/null @@ -1,17 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - StatusInfo, - StatusInfoCoreStatus, - StatusInfoServiceStatus, - StatusResponse, - ServerVersion, - ServerMetrics, -} from './status'; diff --git a/packages/core/status/core-status-common-internal/tsconfig.json b/packages/core/status/core-status-common-internal/tsconfig.json deleted file mode 100644 index 7d31fa090eb0f..0000000000000 --- a/packages/core/status/core-status-common-internal/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "target/types", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "**/*.ts", - "**/*.tsx", - ], - "kbn_references": [ - "@kbn/core-status-common", - "@kbn/core-metrics-server", - "@kbn/config" - ], - "exclude": [ - "target/**/*", - ] -} diff --git a/packages/core/status/core-status-common/index.ts b/packages/core/status/core-status-common/index.ts index 50eb85608522e..1aae83558016a 100644 --- a/packages/core/status/core-status-common/index.ts +++ b/packages/core/status/core-status-common/index.ts @@ -7,5 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { ServiceStatusLevels } from './src'; -export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId, CoreStatus } from './src'; +export { ServiceStatusLevels } from './src/service_status'; +export type { CoreStatus } from './src/core_status'; +export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId } from './src/service_status'; +export type { + StatusInfo, + StatusInfoCoreStatus, + StatusInfoServiceStatus, + StatusResponse, + ServerVersion, + ServerMetrics, +} from './src/status'; diff --git a/packages/core/status/core-status-common/jest.config.js b/packages/core/status/core-status-common/jest.config.js index bc848cd656199..48ce844bb7d3f 100644 --- a/packages/core/status/core-status-common/jest.config.js +++ b/packages/core/status/core-status-common/jest.config.js @@ -10,5 +10,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', - roots: ['/packages/core/status/core-status-common-internal'], + roots: ['/packages/core/status/core-status-common'], }; diff --git a/packages/core/status/core-status-common/src/index.ts b/packages/core/status/core-status-common/src/index.ts deleted file mode 100644 index 7cfcc7dbf79a8..0000000000000 --- a/packages/core/status/core-status-common/src/index.ts +++ /dev/null @@ -1,12 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { ServiceStatusLevels } from './service_status'; -export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId } from './service_status'; -export type { CoreStatus } from './core_status'; diff --git a/packages/core/status/core-status-common-internal/src/status.ts b/packages/core/status/core-status-common/src/status.ts similarity index 92% rename from packages/core/status/core-status-common-internal/src/status.ts rename to packages/core/status/core-status-common/src/status.ts index 370d2c9ac6e5d..7c981c56ceeb3 100644 --- a/packages/core/status/core-status-common-internal/src/status.ts +++ b/packages/core/status/core-status-common/src/status.ts @@ -8,8 +8,9 @@ */ import type { BuildFlavor } from '@kbn/config'; -import type { ServiceStatusLevelId, ServiceStatus, CoreStatus } from '@kbn/core-status-common'; import type { OpsMetrics } from '@kbn/core-metrics-server'; +import type { ServiceStatusLevelId, ServiceStatus } from './service_status'; +import type { CoreStatus } from './core_status'; export interface StatusInfoServiceStatus extends Omit { level: ServiceStatusLevelId; diff --git a/packages/core/status/core-status-common/tsconfig.json b/packages/core/status/core-status-common/tsconfig.json index a63f70f93043d..3b61a574a06bb 100644 --- a/packages/core/status/core-status-common/tsconfig.json +++ b/packages/core/status/core-status-common/tsconfig.json @@ -12,7 +12,9 @@ "**/*.tsx", ], "kbn_references": [ - "@kbn/std" + "@kbn/std", + "@kbn/config", + "@kbn/core-metrics-server" ], "exclude": [ "target/**/*", diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts index 87e0e6e745a92..bafda87c2b08d 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status.ts @@ -15,7 +15,7 @@ import type { IRouter } from '@kbn/core-http-server'; import type { MetricsServiceSetup } from '@kbn/core-metrics-server'; import type { CoreIncrementUsageCounter } from '@kbn/core-usage-data-server'; import { type ServiceStatus, type CoreStatus, ServiceStatusLevels } from '@kbn/core-status-common'; -import { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { calculateLegacyStatus, type LegacyStatusInfo } from '../legacy_status'; import { statusResponse, type RedactedStatusHttpBody } from './status_response_schemas'; diff --git a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts index a2dcbcf7d21b6..68cebab4392e0 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts @@ -9,15 +9,15 @@ import { schema, type Type, type TypeOf } from '@kbn/config-schema'; import type { BuildFlavor } from '@kbn/config'; -import type { ServiceStatusLevelId, ServiceStatus } from '@kbn/core-status-common'; - import type { + ServiceStatusLevelId, + ServiceStatus, StatusResponse, StatusInfoCoreStatus, ServerMetrics, StatusInfo, ServerVersion, -} from '@kbn/core-status-common-internal'; +} from '@kbn/core-status-common'; const serviceStatusLevelId: () => Type = () => schema.oneOf( diff --git a/packages/core/status/core-status-server-internal/tsconfig.json b/packages/core/status/core-status-server-internal/tsconfig.json index bda646809e414..5ca46556cac33 100644 --- a/packages/core/status/core-status-server-internal/tsconfig.json +++ b/packages/core/status/core-status-server-internal/tsconfig.json @@ -29,7 +29,6 @@ "@kbn/core-saved-objects-server-internal", "@kbn/core-status-server", "@kbn/core-status-common", - "@kbn/core-status-common-internal", "@kbn/core-usage-data-base-server-internal", "@kbn/core-base-server-mocks", "@kbn/core-environment-server-mocks", diff --git a/packages/kbn-manifest/index.ts b/packages/kbn-manifest/index.ts index 5fc4727a1a72d..ce890742ea61f 100644 --- a/packages/kbn-manifest/index.ts +++ b/packages/kbn-manifest/index.ts @@ -37,7 +37,7 @@ export const runKbnManifestCli = () => { --list all List all the manifests --package [packageId] Select a package to update. --plugin [pluginId] Select a plugin to update. - --set [property] [value] Set the desired "[property]": "[value]" + --set [property]=[value] Set the desired "[property]": "[value]" --unset [property] Removes the desired "[property]: value" from the manifest `, }, diff --git a/src/plugins/interactive_setup/public/progress_indicator.tsx b/src/plugins/interactive_setup/public/progress_indicator.tsx index 6bb87a792e809..be094f9ef7e8d 100644 --- a/src/plugins/interactive_setup/public/progress_indicator.tsx +++ b/src/plugins/interactive_setup/public/progress_indicator.tsx @@ -15,7 +15,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import useTimeoutFn from 'react-use/lib/useTimeoutFn'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { i18n } from '@kbn/i18n'; import { useKibana } from './use_kibana'; diff --git a/src/plugins/interactive_setup/tsconfig.json b/src/plugins/interactive_setup/tsconfig.json index 51fff541980cc..048143fd464e0 100644 --- a/src/plugins/interactive_setup/tsconfig.json +++ b/src/plugins/interactive_setup/tsconfig.json @@ -14,7 +14,7 @@ "@kbn/i18n", "@kbn/ui-theme", "@kbn/core-http-browser", - "@kbn/core-status-common-internal", + "@kbn/core-status-common", "@kbn/safer-lodash-set", "@kbn/test-jest-helpers", "@kbn/config-schema", diff --git a/tsconfig.base.json b/tsconfig.base.json index e5c90332dfc2d..7457eb69a05df 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -610,8 +610,6 @@ "@kbn/core-security-server-mocks/*": ["packages/core/security/core-security-server-mocks/*"], "@kbn/core-status-common": ["packages/core/status/core-status-common"], "@kbn/core-status-common/*": ["packages/core/status/core-status-common/*"], - "@kbn/core-status-common-internal": ["packages/core/status/core-status-common-internal"], - "@kbn/core-status-common-internal/*": ["packages/core/status/core-status-common-internal/*"], "@kbn/core-status-server": ["packages/core/status/core-status-server"], "@kbn/core-status-server/*": ["packages/core/status/core-status-server/*"], "@kbn/core-status-server-internal": ["packages/core/status/core-status-server-internal"], diff --git a/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts b/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts index 78147439eedd9..f1fcac6e758c0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts +++ b/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts @@ -7,7 +7,7 @@ import { KbnClient } from '@kbn/test'; import type { Client } from '@elastic/elasticsearch'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { catchAxiosErrorFormatAndThrow } from '../format_axios_error'; export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise => { @@ -15,11 +15,11 @@ export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise({ method: 'GET', path: '/api/status', }) - .then((response) => response.data as StatusResponse) + .then(({ data }) => data) .catch(catchAxiosErrorFormatAndThrow); }; /** diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 1e75cd2895693..ba11c79a3548d 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -126,7 +126,7 @@ "@kbn/dev-cli-errors", "@kbn/dev-utils", "@kbn/tooling-log", - "@kbn/core-status-common-internal", + "@kbn/core-status-common", "@kbn/repo-info", "@kbn/storybook", "@kbn/controls-plugin", diff --git a/yarn.lock b/yarn.lock index 698fb11128828..c02ae16affe05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4470,10 +4470,6 @@ version "0.0.0" uid "" -"@kbn/core-status-common-internal@link:packages/core/status/core-status-common-internal": - version "0.0.0" - uid "" - "@kbn/core-status-common@link:packages/core/status/core-status-common": version "0.0.0" uid "" From 3392c1fc2c4177e1c44c58ad4b7605e92d39d774 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:08:37 +1100 Subject: [PATCH 10/42] [8.x] Authorized route migration for routes owned by @elastic/kibana-cloud-security-posture (#198189) (#200714) # Backport This will backport the following commits from `main` to `8.x`: - [Authorized route migration for routes owned by @elastic/kibana-cloud-security-posture (#198189)](https://github.com/elastic/kibana/pull/198189) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../plugins/cloud_defend/server/routes/policies/policies.ts | 6 ++++-- x-pack/plugins/cloud_defend/server/routes/status/status.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts b/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts index 71d7f17ca612e..418e319f1aa1b 100644 --- a/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts +++ b/x-pack/plugins/cloud_defend/server/routes/policies/policies.ts @@ -60,8 +60,10 @@ export const defineGetPoliciesRoute = (router: CloudDefendRouter) => .get({ access: 'internal', path: POLICIES_ROUTE_PATH, - options: { - tags: ['access:cloud-defend-read'], + security: { + authz: { + requiredPrivileges: ['cloud-defend-read'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/cloud_defend/server/routes/status/status.ts b/x-pack/plugins/cloud_defend/server/routes/status/status.ts index ff422638f1c8b..d7ba84d2d2108 100644 --- a/x-pack/plugins/cloud_defend/server/routes/status/status.ts +++ b/x-pack/plugins/cloud_defend/server/routes/status/status.ts @@ -218,8 +218,10 @@ export const defineGetCloudDefendStatusRoute = (router: CloudDefendRouter) => .get({ access: 'internal', path: STATUS_ROUTE_PATH, - options: { - tags: ['access:cloud-defend-read'], + security: { + authz: { + requiredPrivileges: ['cloud-defend-read'], + }, }, }) .addVersion({ version: '1', validate: {} }, async (context, request, response) => { From b7dad9669963ace7532cce60e9ebe553e1a84c02 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:36:58 +1100 Subject: [PATCH 11/42] [8.x] [ResponseOps][Connectors] Optional field blocks editing of case Webhook connector (#198314) (#200724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][Connectors] Optional field blocks editing of case Webhook connector (#198314)](https://github.com/elastic/kibana/pull/198314) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Georgiana-Andreea Onoleață --- .../cases_webhook/steps/update.tsx | 432 ++++++++++-------- .../cases_webhook/translations.ts | 21 +- .../cases_webhook/validator.ts | 32 +- .../cases_webhook/webhook_connectors.test.tsx | 43 ++ 4 files changed, 332 insertions(+), 196 deletions(-) diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/update.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/update.tsx index e7a37d415f4af..dba4f13ec9c86 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/update.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/steps/update.tsx @@ -5,13 +5,22 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useState, useMemo } from 'react'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; -import { FIELD_TYPES, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiSwitch } from '@elastic/eui'; +import { + FIELD_TYPES, + UseField, + useFormContext, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { JsonFieldWrapper, MustacheTextFieldWrapper } from '@kbn/triggers-actions-ui-plugin/public'; -import { containsCommentsOrEmpty, containsTitleAndDesc, isUrlButCanBeEmpty } from '../validator'; +import { + containsCommentsOrEmpty, + containsTitleAndDesc, + isUrlButCanBeEmpty, + validateCreateComment, +} from '../validator'; import { casesVars, commentVars, urlVars } from '../action_variables'; import { HTTP_VERBS } from '../webhook_connectors'; import { styles } from './update.styles'; @@ -23,185 +32,238 @@ interface Props { readOnly: boolean; } -export const UpdateStep: FunctionComponent = ({ display, readOnly }) => ( - - -

{i18n.STEP_4A}

- -

{i18n.STEP_4A_DESCRIPTION}

-
-
- - - - ({ text: verb.toUpperCase(), value: verb })), - readOnly, - }, - }} - /> - - - - - - - - - - - - -

{i18n.STEP_4B}

- -

{i18n.STEP_4B_DESCRIPTION}

-
-
- - - - ({ text: verb.toUpperCase(), value: verb })), - readOnly, - }, - }} - /> - - - - - - - - = ({ display, readOnly }) => { + const { getFieldDefaultValue } = useFormContext(); + + const hasCommentDefaultValue = + !!getFieldDefaultValue('config.createCommentUrl') || + !!getFieldDefaultValue('config.createCommentJson'); + + const [isAddCommentToggled, setIsAddCommentToggled] = useState(Boolean(hasCommentDefaultValue)); + + const onAddCommentToggle = () => { + setIsAddCommentToggled((prev) => !prev); + }; + + const updateIncidentMethodConfig = useMemo( + () => ({ + label: i18n.UPDATE_INCIDENT_METHOD, + defaultValue: 'put', + type: FIELD_TYPES.SELECT, + validations: [{ validator: emptyField(i18n.UPDATE_METHOD_REQUIRED) }], + }), + [] + ); + + const updateIncidentUrlConfig = useMemo( + () => ({ + label: i18n.UPDATE_INCIDENT_URL, + validations: [{ validator: urlField(i18n.UPDATE_URL_REQUIRED) }], + helpText: i18n.UPDATE_INCIDENT_URL_HELP, + }), + [] + ); + + const updateIncidentJsonConfig = useMemo( + () => ({ + label: i18n.UPDATE_INCIDENT_JSON, + helpText: i18n.UPDATE_INCIDENT_JSON_HELP, + validations: [ + { validator: emptyField(i18n.UPDATE_INCIDENT_REQUIRED) }, + { validator: containsTitleAndDesc() }, + ], + }), + [] + ); + + const createCommentMethodConfig = useMemo( + () => ({ + label: i18n.CREATE_COMMENT_METHOD, + defaultValue: 'put', + type: FIELD_TYPES.SELECT, + validations: [{ validator: emptyField(i18n.CREATE_COMMENT_METHOD_REQUIRED) }], + }), + [] + ); + + const createCommentUrlConfig = useMemo( + () => ({ + label: i18n.CREATE_COMMENT_URL, + fieldsToValidateOnChange: ['config.createCommentUrl', 'config.createCommentJson'], + validations: [ + { validator: isUrlButCanBeEmpty(i18n.CREATE_COMMENT_URL_FORMAT_REQUIRED) }, + { + validator: validateCreateComment( + i18n.CREATE_COMMENT_URL_MISSING, + 'config.createCommentJson' + ), + }, + ], + helpText: i18n.CREATE_COMMENT_URL_HELP, + }), + [] + ); + + const createCommentJsonConfig = useMemo( + () => ({ + label: i18n.CREATE_COMMENT_JSON, + helpText: i18n.CREATE_COMMENT_JSON_HELP, + fieldsToValidateOnChange: ['config.createCommentJson', 'config.createCommentUrl'], + validations: [ + { validator: containsCommentsOrEmpty(i18n.CREATE_COMMENT_FORMAT_MESSAGE) }, + { + validator: validateCreateComment( + i18n.CREATE_COMMENT_JSON_MISSING, + 'config.createCommentUrl' + ), + }, + ], + }), + [] + ); + + return ( + <> + + +

{i18n.STEP_4A}

+ +

{i18n.STEP_4A_DESCRIPTION}

+
+
+ + + + ({ text: verb.toUpperCase(), value: verb })), + readOnly, + }, + }} + /> + + + + + + + + + + + + -
-
-
-); + {isAddCommentToggled && ( + <> + + + +

{i18n.STEP_4B_DESCRIPTION}

+
+
+ + + ({ + text: verb.toUpperCase(), + value: verb, + })), + readOnly, + }, + }} + /> + + + + + + + + + + + + )} + + + ); +}; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts index 8c44b6197ef9c..5653fe4adc851 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/translations.ts @@ -54,13 +54,28 @@ export const UPDATE_METHOD_REQUIRED = i18n.translate( } ); -export const CREATE_COMMENT_URL_REQUIRED = i18n.translate( +export const CREATE_COMMENT_URL_FORMAT_REQUIRED = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentUrlText', { defaultMessage: 'Create comment URL must be URL format.', } ); -export const CREATE_COMMENT_MESSAGE = i18n.translate( + +export const CREATE_COMMENT_URL_MISSING = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentUrlMissing', + { + defaultMessage: 'Create comment URL is required.', + } +); + +export const CREATE_COMMENT_JSON_MISSING = i18n.translate( + 'xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentJsonMissing', + { + defaultMessage: 'Create comment Json is required.', + } +); + +export const CREATE_COMMENT_FORMAT_MESSAGE = i18n.translate( 'xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentIncidentText', { defaultMessage: 'Create comment object must be valid JSON.', @@ -373,7 +388,7 @@ export const STEP_4A_DESCRIPTION = i18n.translate( ); export const STEP_4B = i18n.translate('xpack.stackConnectors.components.casesWebhook.step4b', { - defaultMessage: 'Add comment in case (optional)', + defaultMessage: 'Add comment in case', }); export const STEP_4B_DESCRIPTION = i18n.translate( diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts index d972c9bbd1f86..8c64042801635 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/validator.ts @@ -100,6 +100,11 @@ export const containsCommentsOrEmpty = (message: string) => (...args: Parameters): ReturnType> => { const [{ value, path }] = args; + + if (value === null || value === undefined || value === '') { + return undefined; + } + if (typeof value !== 'string') { return { code: 'ERR_FIELD_FORMAT', @@ -107,9 +112,6 @@ export const containsCommentsOrEmpty = message, }; } - if (value.length === 0) { - return undefined; - } const comment = templateActionVariable( commentVars.find((actionVariable) => actionVariable.name === 'case.comment')! @@ -128,16 +130,30 @@ export const isUrlButCanBeEmpty = (message: string) => (...args: Parameters) => { const [{ value }] = args; + const error: ValidationError = { code: 'ERR_FIELD_FORMAT', formatType: 'URL', message, }; - if (typeof value !== 'string') { - return error; - } - if (value.length === 0) { + + if (value === null || value === undefined || value === '') { return undefined; } - return isUrl(value) ? undefined : error; + return typeof value === 'string' && isUrl(value) ? undefined : error; + }; + +export const validateCreateComment = + (message: string, fieldName: string) => + (...args: Parameters) => { + const [{ value, formData }] = args; + const otherFielValue = formData[fieldName]; + + const error: ValidationError = { + code: 'ERR_FIELD_FORMAT', + formatType: 'STRING', + message, + }; + + return !value && otherFielValue ? error : undefined; }; diff --git a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx index 713f2bd9e6f83..911875f31eb26 100644 --- a/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/stack_connectors/public/connector_types/cases_webhook/webhook_connectors.test.tsx @@ -97,6 +97,49 @@ describe('CasesWebhookActionConnectorFields renders', () => { expect(await screen.findByTestId('webhookCreateCommentJson')).toBeInTheDocument(); }); + it('Add comment to case section is rendered only when the toggle button is on', async () => { + const incompleteActionConnector = { + ...actionConnector, + config: { + ...actionConnector.config, + createCommentUrl: undefined, + createCommentJson: undefined, + }, + }; + render( + + {}} + /> + + ); + + await userEvent.click(await screen.findByTestId('webhookAddCommentToggle')); + + expect(await screen.findByTestId('webhookCreateCommentMethodSelect')).toBeInTheDocument(); + expect(await screen.findByTestId('createCommentUrlInput')).toBeInTheDocument(); + expect(await screen.findByTestId('webhookCreateCommentJson')).toBeInTheDocument(); + }); + + it('Toggle button is active when create comment section fields are populated', async () => { + render( + + {}} + /> + + ); + + expect(await screen.findByTestId('webhookAddCommentToggle')).toHaveAttribute( + 'aria-checked', + 'true' + ); + }); + it('connector auth toggles work as expected', async () => { render( From 43fa8a50f82b403fe6a0f1cad2c6ae703f88ef67 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Tue, 19 Nov 2024 15:42:26 +0100 Subject: [PATCH 12/42] [8.x] Authorized route migration for routes owned by @elastic/security-detection-engine (#198195) (#199752) # Backport This will backport the following commits from `main` to `8.x`: - [Authorized route migration for routes owned by @elastic/security-detection-engine (#198195)](https://github.com/elastic/kibana/pull/198195) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../server/routes/create_endpoint_list_item_route.ts | 8 +++++--- .../lists/server/routes/create_endpoint_list_route.ts | 8 +++++--- .../server/routes/create_exception_list_item_route.ts | 8 +++++--- .../server/routes/create_exception_list_route.ts | 8 +++++--- .../server/routes/delete_endpoint_list_item_route.ts | 8 +++++--- .../server/routes/delete_exception_list_item_route.ts | 8 +++++--- .../server/routes/delete_exception_list_route.ts | 8 +++++--- .../server/routes/duplicate_exception_list_route.ts | 8 +++++--- .../server/routes/export_exception_list_route.ts | 8 +++++--- .../server/routes/find_endpoint_list_item_route.ts | 8 +++++--- .../server/routes/find_exception_list_item_route.ts | 8 +++++--- .../lists/server/routes/find_exception_list_route.ts | 8 +++++--- .../lists/server/routes/import_exceptions_route.ts | 6 +++++- .../routes/internal/create_exception_filter_route.ts | 8 +++++--- .../routes/internal/create_exceptions_list_route.ts | 11 +++++------ .../routes/internal/find_lists_by_size_route.ts | 8 +++++--- .../lists/server/routes/list/create_list_route.ts | 8 +++++--- .../lists/server/routes/list/delete_list_route.ts | 8 +++++--- .../server/routes/list/import_list_item_route.ts | 6 +++++- .../lists/server/routes/list/patch_list_route.ts | 8 +++++--- .../lists/server/routes/list/read_list_route.ts | 8 +++++--- .../lists/server/routes/list/update_list_route.ts | 8 +++++--- .../routes/list_index/create_list_index_route.ts | 8 +++++--- .../routes/list_index/delete_list_index_route.ts | 8 +++++--- .../routes/list_index/export_list_item_route.ts | 8 +++++--- .../lists/server/routes/list_index/find_list_route.ts | 8 +++++--- .../server/routes/list_index/read_list_index_route.ts | 8 +++++--- .../server/routes/list_item/create_list_item_route.ts | 8 +++++--- .../server/routes/list_item/delete_list_item_route.ts | 8 +++++--- .../server/routes/list_item/find_list_item_route.ts | 8 +++++--- .../server/routes/list_item/patch_list_item_route.ts | 8 +++++--- .../server/routes/list_item/read_list_item_route.ts | 8 +++++--- .../server/routes/list_item/update_list_item_route.ts | 8 +++++--- .../list_privileges/read_list_privileges_route.ts | 8 +++++--- .../server/routes/read_endpoint_list_item_route.ts | 8 +++++--- .../server/routes/read_exception_list_item_route.ts | 8 +++++--- .../lists/server/routes/read_exception_list_route.ts | 8 +++++--- .../server/routes/summary_exception_list_route.ts | 8 +++++--- .../server/routes/update_endpoint_list_item_route.ts | 8 +++++--- .../server/routes/update_exception_list_item_route.ts | 8 +++++--- .../server/routes/update_exception_list_route.ts | 8 +++++--- .../alert_status/alert_status.ts | 2 +- .../migrations/finalize_alerts_migrations.ts | 2 +- .../indicator_match_alert_suppression.ts | 1 + 44 files changed, 208 insertions(+), 124 deletions(-) diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index 29f7c14c863c6..1ee178e9bc646 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -23,10 +23,12 @@ export const createEndpointListItemRoute = (router: ListsPluginRouter): void => router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: ENDPOINT_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index b15658a40d7fb..54887adba7df4 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -27,10 +27,12 @@ export const createEndpointListRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: ENDPOINT_LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index 7071ec6412a27..e5c6bfd09dfc5 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -25,10 +25,12 @@ export const createExceptionListItemRoute = (router: ListsPluginRouter): void => router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: EXCEPTION_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index a0c0568c31b8d..c7e0a952743c3 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -22,10 +22,12 @@ export const createExceptionListRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: EXCEPTION_LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 0262b747744ec..ee7093bcc1c50 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -25,10 +25,12 @@ export const deleteEndpointListItemRoute = (router: ListsPluginRouter): void => router.versioned .delete({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: ENDPOINT_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index d460610cd02b7..d8eb32e9eeaf3 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -25,10 +25,12 @@ export const deleteExceptionListItemRoute = (router: ListsPluginRouter): void => router.versioned .delete({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: EXCEPTION_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index 938fc9b9bcc2d..db6bb460cbd37 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -21,10 +21,12 @@ export const deleteExceptionListRoute = (router: ListsPluginRouter): void => { router.versioned .delete({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: EXCEPTION_LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts b/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts index 38a51f12a7ed5..308a2e4cd3a4c 100644 --- a/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts @@ -21,10 +21,12 @@ export const duplicateExceptionsRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: `${EXCEPTION_LIST_URL}/_duplicate`, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts index 72ac564604337..8fdd7dbc5e392 100644 --- a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -18,10 +18,12 @@ export const exportExceptionsRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${EXCEPTION_LIST_URL}/_export`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 01539424b8d69..d54560fb6c929 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -21,10 +21,12 @@ export const findEndpointListItemRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${ENDPOINT_LIST_ITEM_URL}/_find`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index f0e4b5546df4f..964a13296c804 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -21,10 +21,12 @@ export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${EXCEPTION_LIST_ITEM_URL}/_find`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 93206b178e2d1..43a890780013b 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -21,10 +21,12 @@ export const findExceptionListRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${EXCEPTION_LIST_URL}/_find`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/import_exceptions_route.ts b/x-pack/plugins/lists/server/routes/import_exceptions_route.ts index af6a88254915c..946f1a02ac855 100644 --- a/x-pack/plugins/lists/server/routes/import_exceptions_route.ts +++ b/x-pack/plugins/lists/server/routes/import_exceptions_route.ts @@ -35,9 +35,13 @@ export const importExceptionsRoute = (router: ListsPluginRouter, config: ConfigT maxBytes: config.maxImportPayloadBytes, output: 'stream', }, - tags: ['access:lists-all'], }, path: `${EXCEPTION_LIST_URL}/_import`, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts b/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts index 41c4e982a5b81..032951d7f750a 100644 --- a/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts +++ b/x-pack/plugins/lists/server/routes/internal/create_exception_filter_route.ts @@ -22,10 +22,12 @@ export const getExceptionFilterRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'internal', - options: { - tags: ['access:securitySolution'], - }, path: INTERNAL_EXCEPTION_FILTER, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts b/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts index 2ca2333333c7e..325c545777628 100644 --- a/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts +++ b/x-pack/plugins/lists/server/routes/internal/create_exceptions_list_route.ts @@ -20,13 +20,12 @@ export const internalCreateExceptionListRoute = (router: ListsPluginRouter): voi router.versioned .post({ access: 'internal', - options: { - // Access control is set to `read` on purpose, as this route is internal and meant to - // ensure we have lists created (if not already) for Endpoint artifacts in order to support - // the UI. The Schema ensures that only endpoint artifact list IDs are allowed. - tags: ['access:lists-read'], - }, path: INTERNAL_EXCEPTIONS_LIST_ENSURE_CREATED_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts b/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts index 3b0bc716cade6..f8e5fc23e2e15 100644 --- a/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts +++ b/x-pack/plugins/lists/server/routes/internal/find_lists_by_size_route.ts @@ -23,10 +23,12 @@ export const findListsBySizeRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'internal', - options: { - tags: ['access:lists-read'], - }, path: INTERNAL_FIND_LISTS_BY_SIZE, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list/create_list_route.ts b/x-pack/plugins/lists/server/routes/list/create_list_route.ts index 9b4714e14720c..23934bdfc792f 100644 --- a/x-pack/plugins/lists/server/routes/list/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/create_list_route.ts @@ -18,10 +18,12 @@ export const createListRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list/delete_list_route.ts b/x-pack/plugins/lists/server/routes/list/delete_list_route.ts index 66c8cb2ee4509..51877b511aca8 100644 --- a/x-pack/plugins/lists/server/routes/list/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/delete_list_route.ts @@ -30,10 +30,12 @@ export const deleteListRoute = (router: ListsPluginRouter): void => { router.versioned .delete({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts index f3f52828f7872..cbe0816c2366f 100644 --- a/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list/import_list_item_route.ts @@ -34,12 +34,16 @@ export const importListItemRoute = (router: ListsPluginRouter, config: ConfigTyp maxBytes: config.maxImportPayloadBytes, parse: false, }, - tags: ['access:lists-all'], timeout: { payload: config.importTimeout.asMilliseconds(), }, }, path: `${LIST_ITEM_URL}/_import`, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list/patch_list_route.ts b/x-pack/plugins/lists/server/routes/list/patch_list_route.ts index 90855ed96885a..369084cc21a2d 100644 --- a/x-pack/plugins/lists/server/routes/list/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/patch_list_route.ts @@ -18,10 +18,12 @@ export const patchListRoute = (router: ListsPluginRouter): void => { router.versioned .patch({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list/read_list_route.ts b/x-pack/plugins/lists/server/routes/list/read_list_route.ts index fff8ef9e60971..7fa6d20867bec 100644 --- a/x-pack/plugins/lists/server/routes/list/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/read_list_route.ts @@ -18,10 +18,12 @@ export const readListRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list/update_list_route.ts b/x-pack/plugins/lists/server/routes/list/update_list_route.ts index cf8e0dc4de83f..a09c91b869372 100644 --- a/x-pack/plugins/lists/server/routes/list/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list/update_list_route.ts @@ -18,10 +18,12 @@ export const updateListRoute = (router: ListsPluginRouter): void => { router.versioned .put({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts index 5842d7032a8bc..1881e51c5888b 100644 --- a/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/create_list_index_route.ts @@ -17,10 +17,12 @@ export const createListIndexRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_INDEX, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion({ validate: false, version: '2023-10-31' }, async (context, _, response) => { const siemResponse = buildSiemResponse(response); diff --git a/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts index 0814739ab11e7..bb1801c29eb3f 100644 --- a/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/delete_list_index_route.ts @@ -34,10 +34,12 @@ export const deleteListIndexRoute = (router: ListsPluginRouter): void => { router.versioned .delete({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_INDEX, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts index 94cacc2f89c40..0c66787b80739 100644 --- a/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/export_list_item_route.ts @@ -20,10 +20,12 @@ export const exportListItemRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${LIST_ITEM_URL}/_export`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts b/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts index 2bdbcc5239363..13dd137a3d84f 100644 --- a/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/find_list_route.ts @@ -18,10 +18,12 @@ export const findListRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${LIST_URL}/_find`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts index 79c82e739ebe8..2cbe90aa3c81e 100644 --- a/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/list_index/read_list_index_route.ts @@ -17,10 +17,12 @@ export const readListIndexRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: LIST_INDEX, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts index e5b1b15ef10d4..b43b5e258d42a 100644 --- a/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/create_list_item_route.ts @@ -21,10 +21,12 @@ export const createListItemRoute = (router: ListsPluginRouter): void => { router.versioned .post({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts index 4cf9dd4d96911..94c6b17f28d4d 100644 --- a/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/delete_list_item_route.ts @@ -21,10 +21,12 @@ export const deleteListItemRoute = (router: ListsPluginRouter): void => { router.versioned .delete({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts index 6bfd673f8fbc0..5ed305de7ec8a 100644 --- a/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/find_list_item_route.ts @@ -21,10 +21,12 @@ export const findListItemRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: `${LIST_ITEM_URL}/_find`, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts index 3545516e17f3c..ef9290bc2ef32 100644 --- a/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/patch_list_item_route.ts @@ -21,10 +21,12 @@ export const patchListItemRoute = (router: ListsPluginRouter): void => { router.versioned .patch({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts index 29513aa23f74f..421108552b7bd 100644 --- a/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/read_list_item_route.ts @@ -21,10 +21,12 @@ export const readListItemRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts index 408391ca63f11..14c992870e921 100644 --- a/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/list_item/update_list_item_route.ts @@ -21,10 +21,12 @@ export const updateListItemRoute = (router: ListsPluginRouter): void => { router.versioned .put({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts b/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts index 94c171a2ec79c..bf322d10cfc85 100644 --- a/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts +++ b/x-pack/plugins/lists/server/routes/list_privileges/read_list_privileges_route.ts @@ -16,10 +16,12 @@ export const readPrivilegesRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: LIST_PRIVILEGES_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index d7e057a70d5de..2f607d4c4c334 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -25,10 +25,12 @@ export const readEndpointListItemRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: ENDPOINT_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index 9f35da7fa6fe8..ceb0195c390ab 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -25,10 +25,12 @@ export const readExceptionListItemRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: EXCEPTION_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index b98b7dfe86ee8..2ff46ffba56f4 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -21,10 +21,12 @@ export const readExceptionListRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-read'], - }, path: EXCEPTION_LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-read'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts b/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts index 28810283770be..bf5fe000a7fb6 100644 --- a/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/summary_exception_list_route.ts @@ -21,10 +21,12 @@ export const summaryExceptionListRoute = (router: ListsPluginRouter): void => { router.versioned .get({ access: 'public', - options: { - tags: ['access:lists-summary'], - }, path: `${EXCEPTION_LIST_URL}/summary`, + security: { + authz: { + requiredPrivileges: ['lists-summary'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index 048816c519a0f..a6c633ab57c3a 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -23,10 +23,12 @@ export const updateEndpointListItemRoute = (router: ListsPluginRouter): void => router.versioned .put({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: ENDPOINT_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index f3f925317afb0..da1541bb86178 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -24,10 +24,12 @@ export const updateExceptionListItemRoute = (router: ListsPluginRouter): void => router.versioned .put({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: EXCEPTION_LIST_ITEM_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 6998b612c78a2..36d65d9b1ac5e 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -21,10 +21,12 @@ export const updateExceptionListRoute = (router: ListsPluginRouter): void => { router.versioned .put({ access: 'public', - options: { - tags: ['access:lists-all'], - }, path: EXCEPTION_LIST_URL, + security: { + authz: { + requiredPrivileges: ['lists-all'], + }, + }, }) .addVersion( { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/alert_status/alert_status.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/alert_status/alert_status.ts index 1a26ae97e3817..bf2e5831c10f7 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/alert_status/alert_status.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/alert_status/alert_status.ts @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext) => { describe('@ess @serverless change alert status endpoints', () => { // Flakey: See https://github.com/elastic/kibana/issues/179704 - describe.skip('validation checks', () => { + describe('validation checks', () => { describe('update by ids', () => { it('should not give errors when querying and the alerts index does not exist yet', async () => { const { body } = await supertest diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/ess_specific_index_logic/migrations/finalize_alerts_migrations.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/ess_specific_index_logic/migrations/finalize_alerts_migrations.ts index 02d681fe29712..00195bc813c97 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/ess_specific_index_logic/migrations/finalize_alerts_migrations.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/alerts/basic_license_essentials_tier/ess_specific_index_logic/migrations/finalize_alerts_migrations.ts @@ -192,7 +192,7 @@ export default ({ getService }: FtrProviderContext): void => { // it's been skipped since it was originally introduced in // https://github.com/elastic/kibana/pull/85690. Created ticket to track skip. // https://github.com/elastic/kibana/issues/179593 - it.skip('deletes the underlying migration task', async () => { + it('deletes the underlying migration task', async () => { await waitFor( async () => { const { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_alert_suppression.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_alert_suppression.ts index 1ecf949b18951..1acb416808081 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_alert_suppression.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/indicator_match/trial_license_complete_tier/indicator_match_alert_suppression.ts @@ -167,6 +167,7 @@ export default ({ getService }: FtrProviderContext) => { }); cases.forEach(({ eventsCount, threatsCount, title }) => { + // FLAKY: https://github.com/elastic/kibana/issues/197765 describe(`Code execution path: ${title}`, () => { it('should suppress an alert on real rule executions', async () => { const id = uuidv4(); From acafd047d844002742b8b9abb0990b7636c51d1b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:56:18 +1100 Subject: [PATCH 13/42] [8.x] Authorized route migration for routes owned by security-detection-rule-management (#198383) (#200728) # Backport This will backport the following commits from `main` to `8.x`: - [Authorized route migration for routes owned by security-detection-rule-management (#198383)](https://github.com/elastic/kibana/pull/198383) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../api/get_all_integrations/route.ts | 6 ++++-- .../api/get_installed_integrations/route.ts | 6 ++++-- .../bootstrap_prebuilt_rules.ts | 6 ++++-- .../get_prebuilt_rules_and_timelines_status_route.ts | 6 ++++-- .../get_prebuilt_rules_status_route.ts | 6 ++++-- .../install_prebuilt_rules_and_timelines_route.ts | 6 +++++- .../perform_rule_installation_route.ts | 6 +++++- .../perform_rule_upgrade_route.ts | 6 +++++- .../review_rule_installation_route.ts | 6 +++++- .../review_rule_upgrade/review_rule_upgrade_route.ts | 6 +++++- .../rule_management/api/rules/bulk_actions/route.ts | 7 ++++++- .../api/rules/bulk_create_rules/route.ts | 6 +++++- .../api/rules/bulk_patch_rules/route.ts | 6 +++++- .../api/rules/bulk_update_rules/route.ts | 6 +++++- .../api/rules/coverage_overview/route.ts | 6 ++++-- .../rule_management/api/rules/create_rule/route.ts | 6 ++++-- .../rule_management/api/rules/delete_rule/route.ts | 6 ++++-- .../rule_management/api/rules/export_rules/route.ts | 6 +++++- .../rule_management/api/rules/filters/route.ts | 6 ++++-- .../rule_management/api/rules/find_rules/route.ts | 6 ++++-- .../rule_management/api/rules/import_rules/route.ts | 6 +++++- .../rule_management/api/rules/patch_rule/route.ts | 6 ++++-- .../rule_management/api/rules/read_rule/route.ts | 6 ++++-- .../rule_management/api/rules/update_rule/route.ts | 6 ++++-- .../rule_management/api/tags/read_tags/route.ts | 6 ++++-- .../get_cluster_health/get_cluster_health_route.ts | 12 ++++++++---- .../get_rule_health/get_rule_health_route.ts | 6 ++++-- .../get_space_health/get_space_health_route.ts | 12 ++++++++---- .../setup/setup_health_route.ts | 6 ++++-- .../get_rule_execution_events_route.ts | 6 ++++-- .../get_rule_execution_results_route.ts | 6 ++++-- 31 files changed, 144 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts index 5b4eab27f71ab..4b5642b9d199b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_all_integrations/route.ts @@ -23,8 +23,10 @@ export const getAllIntegrationsRoute = (router: SecuritySolutionPluginRouter) => .get({ access: 'internal', path: GET_ALL_INTEGRATIONS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts index 3a3d159d1337f..27b1c4b103ab7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/fleet_integrations/api/get_installed_integrations/route.ts @@ -21,8 +21,10 @@ export const getInstalledIntegrationsRoute = (router: SecuritySolutionPluginRout .get({ access: 'internal', path: GET_INSTALLED_INTEGRATIONS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts index d17435a543320..8d3788a2cf7f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/bootstrap_prebuilt_rules/bootstrap_prebuilt_rules.ts @@ -21,8 +21,10 @@ export const bootstrapPrebuiltRulesRoute = (router: SecuritySolutionPluginRouter .post({ access: 'internal', path: BOOTSTRAP_PREBUILT_RULES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts index 3713176e919c5..dc9c15ac5b5f7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.ts @@ -30,8 +30,10 @@ export const getPrebuiltRulesAndTimelinesStatusRoute = (router: SecuritySolution .get({ access: 'public', path: PREBUILT_RULES_STATUS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts index 86809a3a79a93..0561c826e0c78 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/get_prebuilt_rules_status/get_prebuilt_rules_status_route.ts @@ -20,8 +20,10 @@ export const getPrebuiltRulesStatusRoute = (router: SecuritySolutionPluginRouter .get({ access: 'internal', path: GET_PREBUILT_RULES_STATUS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts index 11e841ed50431..8740d27fce817 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.ts @@ -33,8 +33,12 @@ export const installPrebuiltRulesAndTimelinesRoute = (router: SecuritySolutionPl .put({ access: 'public', path: PREBUILT_RULES_URL, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts index 1a29568ca496b..8b4d38bd2f4a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_installation/perform_rule_installation_route.ts @@ -34,8 +34,12 @@ export const performRuleInstallationRoute = (router: SecuritySolutionPluginRoute .post({ access: 'internal', path: PERFORM_RULE_INSTALLATION_URL, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts index c8b5d459f6787..db5f7a186d303 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/perform_rule_upgrade/perform_rule_upgrade_route.ts @@ -35,8 +35,12 @@ export const performRuleUpgradeRoute = ( .post({ access: 'internal', path: PERFORM_RULE_UPGRADE_URL, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts index 00fc5e2beb5b8..c1c45532f61bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_installation/review_rule_installation_route.ts @@ -26,8 +26,12 @@ export const reviewRuleInstallationRoute = (router: SecuritySolutionPluginRouter .post({ access: 'internal', path: REVIEW_RULE_INSTALLATION_URL, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts index 382ec27a1bf35..3da62bd9bb21d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/prebuilt_rules/api/review_rule_upgrade/review_rule_upgrade_route.ts @@ -35,8 +35,12 @@ export const reviewRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) => .post({ access: 'internal', path: REVIEW_RULE_UPGRADE_URL, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: PREBUILT_RULES_OPERATION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 658a9b193e0a2..e599ff4a936ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -61,8 +61,13 @@ export const performBulkActionRoute = ( .post({ access: 'public', path: DETECTION_ENGINE_RULES_BULK_ACTION, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution', routeLimitedConcurrencyTag(MAX_ROUTE_CONCURRENCY)], + tags: [routeLimitedConcurrencyTag(MAX_ROUTE_CONCURRENCY)], timeout: { idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_create_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_create_rules/route.ts index 98910ea337630..4a9b0b659e982 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_create_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_create_rules/route.ts @@ -37,8 +37,12 @@ export const bulkCreateRulesRoute = (router: SecuritySolutionPluginRouter, logge .post({ access: 'public', path: DETECTION_ENGINE_RULES_BULK_CREATE, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts index da75e4e33362a..d3d4648ccdde9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_patch_rules/route.ts @@ -31,8 +31,12 @@ export const bulkPatchRulesRoute = (router: SecuritySolutionPluginRouter, logger .patch({ access: 'public', path: DETECTION_ENGINE_RULES_BULK_UPDATE, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_update_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_update_rules/route.ts index fb95e7e452afd..a2c14610fdf2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_update_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_update_rules/route.ts @@ -35,8 +35,12 @@ export const bulkUpdateRulesRoute = (router: SecuritySolutionPluginRouter, logge .put({ access: 'public', path: DETECTION_ENGINE_RULES_BULK_UPDATE, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/coverage_overview/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/coverage_overview/route.ts index 0a298008dd354..a959c522c1718 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/coverage_overview/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/coverage_overview/route.ts @@ -22,8 +22,10 @@ export const getCoverageOverviewRoute = (router: SecuritySolutionPluginRouter) = .post({ access: 'internal', path: RULE_MANAGEMENT_COVERAGE_OVERVIEW_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.ts index aa6425b2e673c..a5fee66d00148 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/create_rule/route.ts @@ -27,8 +27,10 @@ export const createRuleRoute = (router: SecuritySolutionPluginRouter): void => { access: 'public', path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/delete_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/delete_rule/route.ts index 42a6a1c47544f..a5854e9a2caf5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/delete_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/delete_rule/route.ts @@ -24,8 +24,10 @@ export const deleteRuleRoute = (router: SecuritySolutionPluginRouter) => { .delete({ access: 'public', path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts index 3c770c714334c..a37bb29963332 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/export_rules/route.ts @@ -33,8 +33,12 @@ export const exportRulesRoute = ( .post({ access: 'public', path: `${DETECTION_ENGINE_RULES_URL}/_export`, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], timeout: { idleSocket: RULE_MANAGEMENT_IMPORT_EXPORT_SOCKET_TIMEOUT_MS, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts index 183d4f8e2f78d..05633892cdddb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/filters/route.ts @@ -56,8 +56,10 @@ export const getRuleManagementFilters = (router: SecuritySolutionPluginRouter) = .get({ access: 'internal', path: RULE_MANAGEMENT_FILTERS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/find_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/find_rules/route.ts index 02ff637ab6f10..899e568e79630 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/find_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/find_rules/route.ts @@ -25,8 +25,10 @@ export const findRulesRoute = (router: SecuritySolutionPluginRouter, logger: Log .get({ access: 'public', path: DETECTION_ENGINE_RULES_URL_FIND, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts index e9131050d9629..d6a5213fcbea6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/import_rules/route.ts @@ -47,8 +47,12 @@ export const importRulesRoute = (router: SecuritySolutionPluginRouter, config: C .post({ access: 'public', path: `${DETECTION_ENGINE_RULES_URL}/_import`, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], body: { maxBytes: config.maxRuleImportPayloadBytes, output: 'stream', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts index 3886f63c482b0..fcd388e81d1e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/patch_rule/route.ts @@ -26,8 +26,10 @@ export const patchRuleRoute = (router: SecuritySolutionPluginRouter) => { .patch({ access: 'public', path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/read_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/read_rule/route.ts index a119d1afae912..f8826e8aad45b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/read_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/read_rule/route.ts @@ -24,8 +24,10 @@ export const readRuleRoute = (router: SecuritySolutionPluginRouter, logger: Logg .get({ access: 'public', path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.ts index fb7a7a9e3197d..0bedcb25de528 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/update_rule/route.ts @@ -27,8 +27,10 @@ export const updateRuleRoute = (router: SecuritySolutionPluginRouter) => { .put({ access: 'public', path: DETECTION_ENGINE_RULES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts index 5120603f9f674..d94f695f39179 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/tags/read_tags/route.ts @@ -18,8 +18,10 @@ export const readTagsRoute = (router: SecuritySolutionPluginRouter) => { .get({ access: 'public', path: DETECTION_ENGINE_TAGS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts index 719f46788a524..d6d9e6843e5a2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_cluster_health/get_cluster_health_route.ts @@ -36,8 +36,10 @@ export const getClusterHealthRoute = (router: SecuritySolutionPluginRouter) => { .get({ access: 'internal', path: GET_CLUSTER_HEALTH_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( @@ -62,8 +64,10 @@ export const getClusterHealthRoute = (router: SecuritySolutionPluginRouter) => { .post({ access: 'internal', path: GET_CLUSTER_HEALTH_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts index a69f7961b19f8..401040b33faa5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_rule_health/get_rule_health_route.ts @@ -33,8 +33,10 @@ export const getRuleHealthRoute = (router: SecuritySolutionPluginRouter) => { .post({ access: 'internal', path: GET_RULE_HEALTH_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts index 96ced4e34151d..772de5aead760 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/get_space_health/get_space_health_route.ts @@ -36,8 +36,10 @@ export const getSpaceHealthRoute = (router: SecuritySolutionPluginRouter) => { .get({ access: 'internal', path: GET_SPACE_HEALTH_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( @@ -62,8 +64,10 @@ export const getSpaceHealthRoute = (router: SecuritySolutionPluginRouter) => { .post({ access: 'internal', path: GET_SPACE_HEALTH_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/setup/setup_health_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/setup/setup_health_route.ts index 685ce8f677952..0e8e5e5b676fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/setup/setup_health_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/detection_engine_health/setup/setup_health_route.ts @@ -22,8 +22,10 @@ export const setupHealthRoute = (router: SecuritySolutionPluginRouter) => { .post({ access: 'internal', path: SETUP_HEALTH_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.ts index 4e8001193b5c5..fc3c485710c1a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_events/get_rule_execution_events_route.ts @@ -27,8 +27,10 @@ export const getRuleExecutionEventsRoute = (router: SecuritySolutionPluginRouter .get({ access: 'internal', path: GET_RULE_EXECUTION_EVENTS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts index bf3a9864260ac..c23396e139afc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.ts @@ -27,8 +27,10 @@ export const getRuleExecutionResultsRoute = (router: SecuritySolutionPluginRoute .get({ access: 'internal', path: GET_RULE_EXECUTION_RESULTS_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( From 74d20993ab69c43b6a2528d0aa6c1caf564ebf18 Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Tue, 19 Nov 2024 16:05:41 +0100 Subject: [PATCH 14/42] [8.x] [Inventory] Remove open in Discover button (#200574) (#200729) # Backport This will backport the following commits from `main` to `8.x`: - [[Inventory] Remove open in Discover button (#200574)](https://github.com/elastic/kibana/pull/200574) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../inventory/e2e/cypress/e2e/home.cy.ts | 29 ------------- .../components/search_bar/discover_button.tsx | 35 ---------------- .../public/components/search_bar/index.tsx | 42 +++++++------------ .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 6 files changed, 18 insertions(+), 94 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index 17b6cf502280a..c9d341c708965 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -121,35 +121,6 @@ describe('Home page', () => { cy.url().should('include', '/app/metrics/detail/host/server1'); }); - it('Navigates to discover with default filter', () => { - cy.intercept('GET', '/internal/entities/managed/enablement', { - fixture: 'eem_enabled.json', - }).as('getEEMStatus'); - cy.visitKibana('/app/inventory'); - cy.wait('@getEEMStatus'); - cy.contains('Open in discover').click(); - cy.url().should( - 'include', - "query:(language:kuery,query:'entity.definition_id%20:%20builtin*" - ); - }); - - it('Navigates to discover with kuery filter', () => { - cy.intercept('GET', '/internal/entities/managed/enablement', { - fixture: 'eem_enabled.json', - }).as('getEEMStatus'); - cy.visitKibana('/app/inventory'); - cy.wait('@getEEMStatus'); - cy.getByTestSubj('queryInput').type('service.name : foo'); - - cy.contains('Update').click(); - cy.contains('Open in discover').click(); - cy.url().should( - 'include', - "query:'service.name%20:%20foo%20AND%20entity.definition_id%20:%20builtin*'" - ); - }); - it('Navigates to infra when clicking on a container type entity', () => { cy.intercept('GET', '/internal/entities/managed/enablement', { fixture: 'eem_enabled.json', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx deleted file mode 100644 index 13477d63e5f82..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx +++ /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 { EuiButton } from '@elastic/eui'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useDiscoverRedirect } from '../../hooks/use_discover_redirect'; - -export function DiscoverButton({ dataView }: { dataView: DataView }) { - const { getDiscoverRedirectUrl } = useDiscoverRedirect(); - - const discoverLink = getDiscoverRedirectUrl(); - - if (!discoverLink) { - return null; - } - - return ( - - {i18n.translate('xpack.inventory.searchBar.discoverButton', { - defaultMessage: 'Open in discover', - })} - - ); -} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx index d1ccfd3f358e3..3464c5749dbc3 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar'; @@ -14,7 +13,6 @@ import { useKibana } from '../../hooks/use_kibana'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context'; import { getKqlFieldsWithFallback } from '../../utils/get_kql_field_names_with_fallback'; import { ControlGroups } from './control_groups'; -import { DiscoverButton } from './discover_button'; export function SearchBar() { const { refreshSubject$, dataView, searchState, onQueryChange } = useUnifiedSearchContext(); @@ -73,30 +71,20 @@ export function SearchBar() { ); return ( - - - } - onQuerySubmit={handleQuerySubmit} - placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', { - defaultMessage: - 'Search for your entities by name or its metadata (e.g. entity.type : service)', - })} - showDatePicker={false} - showFilterBar - showQueryInput - showQueryMenu - /> - - - {dataView ? ( - - - - ) : null} - + } + onQuerySubmit={handleQuerySubmit} + placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', { + defaultMessage: + 'Search for your entities by name or its metadata (e.g. entity.type : service)', + })} + showDatePicker={false} + showFilterBar + showQueryInput + showQueryMenu + /> ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c3d757f1117b6..88a3e85ece298 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26203,6 +26203,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "Ouvrir la fenêtre contextuelle", "xpack.inventory.data_view.creation_failed": "Une erreur s'est produite lors de la création de la vue de données", "xpack.inventory.eemEnablement.errorTitle": "Erreur lors de l'activation du nouveau modèle d'entité", + "xpack.inventory.entityActions.discoverLink": "Ouvrir dans Discover", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "Alertes", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "Le nombre d'alertes actives", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "Nom de l'entité", @@ -26232,7 +26233,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "L'affichage de vos entités peut prendre quelques minutes. Essayez de rafraîchir à nouveau dans une minute ou deux.", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "En savoir plus", "xpack.inventory.noEntitiesEmptyState.title": "Aucune entité disponible", - "xpack.inventory.searchBar.discoverButton": "Ouvrir dans Discover", "xpack.inventory.searchBar.placeholder": "Recherchez vos entités par nom ou par leurs métadonnées (par exemple entity.type : service)", "xpack.inventory.shareLink.shareButtonLabel": "Partager", "xpack.inventory.shareLink.shareToastFailureLabel": "Les URL courtes ne peuvent pas être copiées.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7d5049d030541..aa7f66dde6817 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26175,6 +26175,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "ポップオーバーを開く", "xpack.inventory.data_view.creation_failed": "データビューの作成中にエラーが発生しました", "xpack.inventory.eemEnablement.errorTitle": "新しいエンティティモデルの有効化エラー", + "xpack.inventory.entityActions.discoverLink": "Discoverで開く", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "アラート", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "アクティブなアラートの件数", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "エンティティ名", @@ -26204,7 +26205,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "エンティティが表示されるまで数分かかる場合があります。1〜2分後に更新してください。", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "詳細", "xpack.inventory.noEntitiesEmptyState.title": "エンティティがありません", - "xpack.inventory.searchBar.discoverButton": "Discoverで開く", "xpack.inventory.searchBar.placeholder": "エンティティを名前またはメタデータ(例:entity.type : service)で検索します。", "xpack.inventory.shareLink.shareButtonLabel": "共有", "xpack.inventory.shareLink.shareToastFailureLabel": "短縮URLをコピーできません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5e47a4c655827..beeaabe1752f3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26231,6 +26231,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "打开弹出框", "xpack.inventory.data_view.creation_failed": "创建数据视图时出错", "xpack.inventory.eemEnablement.errorTitle": "启用新实体模型时出错", + "xpack.inventory.entityActions.discoverLink": "在 Discover 中打开", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "告警", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "活动告警计数", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "实体名称", @@ -26260,7 +26261,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "您的实体可能需要数分钟才能显示。请尝试在一或两分钟后刷新。", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "了解详情", "xpack.inventory.noEntitiesEmptyState.title": "无可用实体", - "xpack.inventory.searchBar.discoverButton": "在 Discover 中打开", "xpack.inventory.searchBar.placeholder": "按名称或其元数据(例如,entity.type:服务)搜索您的实体", "xpack.inventory.shareLink.shareButtonLabel": "共享", "xpack.inventory.shareLink.shareToastFailureLabel": "无法复制短 URL。", From f38870d176a153581e0459d5c2e749023f13ae5e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 02:55:45 +1100 Subject: [PATCH 15/42] [8.x] Swaps template literals for sprintf style interpolation (#200634) (#200737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Swaps template literals for sprintf style interpolation (#200634)](https://github.com/elastic/kibana/pull/200634) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Jason Rhodes --- .../synthetics/public/utils/api_service/api_service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts b/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts index 58c1d88226e5e..d16e34b430f1c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts @@ -54,11 +54,15 @@ class ApiService { if (isRight(decoded)) { return decoded.right as T; } else { + // This was changed from using template literals to using %s string + // interpolation, but the previous version included the apiUrl value + // twice. To ensure the log output doesn't change, this continues. + // // eslint-disable-next-line no-console console.error( - `API ${apiUrl} is not returning expected response, ${formatErrors( - decoded.left - )} for response`, + 'API %s is not returning expected response, %s for response', + apiUrl, + formatErrors(decoded.left).toString(), apiUrl, response ); From bf5c9dea41656f79f7964cef7dfd821e1efafe86 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 03:16:15 +1100 Subject: [PATCH 16/42] [8.x] [Indx Mgmt] Enable semantic text adaptive allocations by default (#200168) (#200747) # Backport This will backport the following commits from `main` to `8.x`: - [[Indx Mgmt] Enable semantic text adaptive allocations by default (#200168)](https://github.com/elastic/kibana/pull/200168) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sander Philipse <94373878+sphilipse@users.noreply.github.com> --- .../create_field/semantic_text/use_semantic_text.test.ts | 4 +++- .../fields/create_field/semantic_text/use_semantic_text.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts index c4e668f9635d1..65415a287d94c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.test.ts @@ -258,7 +258,9 @@ describe('useSemanticText', () => { { service: 'elser', service_settings: { - num_allocations: 1, + adaptive_allocations: { + enabled: true, + }, num_threads: 1, model_id: '.elser_model_2', }, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts index 42d220ba4724b..6662c2852ad7b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/create_field/semantic_text/use_semantic_text.ts @@ -85,7 +85,7 @@ export function useSemanticText(props: UseSemanticTextProps) { : { service: defaultInferenceEndpointConfig.service, service_settings: { - num_allocations: 1, + adaptive_allocations: { enabled: true }, num_threads: 1, model_id: trainedModelId, }, From 59e80060487ea79c738b701a6dc13c541e5f82db Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 03:31:02 +1100 Subject: [PATCH 17/42] [8.x] Authorized route migration for routes owned by @elastic/obs-ux-infra_services-team (#198196) (#200752) # Backport This will backport the following commits from `main` to `8.x`: - [Authorized route migration for routes owned by @elastic/obs-ux-infra_services-team (#198196)](https://github.com/elastic/kibana/pull/198196) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../profiling/server/routes/apm.ts | 6 +++++- .../profiling/server/routes/flamechart.ts | 7 ++++++- .../profiling/server/routes/functions.ts | 7 ++++++- .../profiling/server/routes/setup/route.ts | 18 +++++++++++++++--- .../server/routes/storage_explorer/route.ts | 18 +++++++++++++++--- .../profiling/server/routes/topn.ts | 7 ++++++- 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability_solution/profiling/server/routes/apm.ts b/x-pack/plugins/observability_solution/profiling/server/routes/apm.ts index e5119c17ee5da..7ad001831c0e4 100644 --- a/x-pack/plugins/observability_solution/profiling/server/routes/apm.ts +++ b/x-pack/plugins/observability_solution/profiling/server/routes/apm.ts @@ -34,8 +34,12 @@ export function registerTopNFunctionsAPMTransactionsRoute({ router.get( { path: paths.APMTransactions, + security: { + authz: { + requiredPrivileges: ['profiling', 'apm'], + }, + }, options: { - tags: ['access:profiling', 'access:apm'], timeout: { idleSocket: IDLE_SOCKET_TIMEOUT }, }, validate: { query: querySchema }, diff --git a/x-pack/plugins/observability_solution/profiling/server/routes/flamechart.ts b/x-pack/plugins/observability_solution/profiling/server/routes/flamechart.ts index 86d384f62f609..2b318e57eb364 100644 --- a/x-pack/plugins/observability_solution/profiling/server/routes/flamechart.ts +++ b/x-pack/plugins/observability_solution/profiling/server/routes/flamechart.ts @@ -23,7 +23,12 @@ export function registerFlameChartSearchRoute({ router.get( { path: paths.Flamechart, - options: { tags: ['access:profiling'], timeout: { idleSocket: IDLE_SOCKET_TIMEOUT } }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, + options: { timeout: { idleSocket: IDLE_SOCKET_TIMEOUT } }, validate: { query: schema.object({ timeFrom: schema.number(), diff --git a/x-pack/plugins/observability_solution/profiling/server/routes/functions.ts b/x-pack/plugins/observability_solution/profiling/server/routes/functions.ts index 4f30ff0c8f238..1689e707a9d80 100644 --- a/x-pack/plugins/observability_solution/profiling/server/routes/functions.ts +++ b/x-pack/plugins/observability_solution/profiling/server/routes/functions.ts @@ -34,7 +34,12 @@ export function registerTopNFunctionsSearchRoute({ router.get( { path: paths.TopNFunctions, - options: { tags: ['access:profiling'], timeout: { idleSocket: IDLE_SOCKET_TIMEOUT } }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, + options: { timeout: { idleSocket: IDLE_SOCKET_TIMEOUT } }, validate: { query: querySchema }, }, async (context, request, response) => { diff --git a/x-pack/plugins/observability_solution/profiling/server/routes/setup/route.ts b/x-pack/plugins/observability_solution/profiling/server/routes/setup/route.ts index cbd0f6ee2170c..a5bc8d3187bda 100644 --- a/x-pack/plugins/observability_solution/profiling/server/routes/setup/route.ts +++ b/x-pack/plugins/observability_solution/profiling/server/routes/setup/route.ts @@ -27,7 +27,11 @@ export function registerSetupRoute({ router.get( { path: paths.HasSetupESResources, - options: { tags: ['access:profiling'] }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, validate: false, }, async (context, request, response) => { @@ -62,7 +66,11 @@ export function registerSetupRoute({ router.post( { path: paths.HasSetupESResources, - options: { tags: ['access:profiling'] }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, validate: false, }, async (context, request, response) => { @@ -166,7 +174,11 @@ export function registerSetupRoute({ router.get( { path: paths.SetupDataCollectionInstructions, - options: { tags: ['access:profiling'] }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, validate: false, }, async (context, request, response) => { diff --git a/x-pack/plugins/observability_solution/profiling/server/routes/storage_explorer/route.ts b/x-pack/plugins/observability_solution/profiling/server/routes/storage_explorer/route.ts index 2447bfea61011..d3148fd9ff03a 100644 --- a/x-pack/plugins/observability_solution/profiling/server/routes/storage_explorer/route.ts +++ b/x-pack/plugins/observability_solution/profiling/server/routes/storage_explorer/route.ts @@ -29,7 +29,11 @@ export function registerStorageExplorerRoute({ router.get( { path: paths.StorageExplorerSummary, - options: { tags: ['access:profiling'] }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, validate: { query: schema.object({ indexLifecyclePhase: schema.oneOf([ @@ -112,7 +116,11 @@ export function registerStorageExplorerRoute({ router.get( { path: paths.StorageExplorerHostStorageDetails, - options: { tags: ['access:profiling'] }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, validate: { query: schema.object({ indexLifecyclePhase: schema.oneOf([ @@ -156,7 +164,11 @@ export function registerStorageExplorerRoute({ router.get( { path: paths.StorageExplorerIndicesStorageDetails, - options: { tags: ['access:profiling'] }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, validate: { query: schema.object({ indexLifecyclePhase: schema.oneOf([ diff --git a/x-pack/plugins/observability_solution/profiling/server/routes/topn.ts b/x-pack/plugins/observability_solution/profiling/server/routes/topn.ts index 944245a9d15cc..a675cc8e4b31a 100644 --- a/x-pack/plugins/observability_solution/profiling/server/routes/topn.ts +++ b/x-pack/plugins/observability_solution/profiling/server/routes/topn.ts @@ -171,7 +171,12 @@ export function queryTopNCommon({ router.get( { path: pathName, - options: { tags: ['access:profiling'], timeout: { idleSocket: IDLE_SOCKET_TIMEOUT } }, + security: { + authz: { + requiredPrivileges: ['profiling'], + }, + }, + options: { timeout: { idleSocket: IDLE_SOCKET_TIMEOUT } }, validate: { query: schema.object({ timeFrom: schema.number(), From d2f38b9db8edb9988b86461cb4fea0eb24d8c9c2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 03:49:28 +1100 Subject: [PATCH 18/42] [8.x] Authorized route migration for routes owned by security-threat-hunting-investigations (#198387) (#200753) # Backport This will backport the following commits from `main` to `8.x`: - [Authorized route migration for routes owned by security-threat-hunting-investigations (#198387)](https://github.com/elastic/kibana/pull/198387) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../routes/draft_timelines/clean_draft_timelines/index.ts | 6 ++++-- .../routes/draft_timelines/get_draft_timelines/index.ts | 6 ++++-- .../server/lib/timeline/routes/notes/delete_note.ts | 6 ++++-- .../server/lib/timeline/routes/notes/get_notes.ts | 6 ++++-- .../server/lib/timeline/routes/notes/persist_note.ts | 6 ++++-- .../timeline/routes/pinned_events/persist_pinned_event.ts | 6 ++++-- .../install_prepackaged_timelines/index.ts | 6 +++++- .../lib/timeline/routes/timelines/copy_timeline/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/create_timelines/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/delete_timelines/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/export_timelines/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/get_timeline/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/get_timelines/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/import_timelines/index.ts | 6 +++++- .../lib/timeline/routes/timelines/patch_timelines/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/persist_favorite/index.ts | 6 ++++-- .../lib/timeline/routes/timelines/resolve_timeline/index.ts | 6 ++++-- 17 files changed, 70 insertions(+), 32 deletions(-) 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 6515817f28e11..fb6ffba7995b8 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 @@ -31,8 +31,10 @@ export const cleanDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) = router.versioned .post({ path: TIMELINE_DRAFT_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 1ba3167cdefae..e83d2cc839db0 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 @@ -24,8 +24,10 @@ export const getDraftTimelinesRoute = (router: SecuritySolutionPluginRouter) => router.versioned .get({ path: TIMELINE_DRAFT_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts index 9e6aeb5473fc2..7308801030f4a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/delete_note.ts @@ -22,8 +22,10 @@ export const deleteNoteRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .delete({ path: NOTE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts index 0cd7853b38a1b..3a1ae1ba27e2f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts @@ -37,8 +37,10 @@ export const getNotesRoute = ( router.versioned .get({ path: NOTE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 2e825b4ff3a15..f9759444b26d8 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 @@ -25,8 +25,10 @@ export const persistNoteRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .patch({ path: NOTE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 74db9e58d904b..51b001c9ea29e 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 @@ -26,8 +26,10 @@ export const persistPinnedEventRoute = (router: SecuritySolutionPluginRouter) => router.versioned .patch({ path: PINNED_EVENT_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 b1a6e2f781f45..99c4d95942f15 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 @@ -34,8 +34,12 @@ export const installPrepackedTimelinesRoute = ( router.versioned .post({ path: `${TIMELINE_PREPACKAGED_URL}`, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], body: { maxBytes: config.maxTimelineImportPayloadBytes, output: 'stream', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts index e795ec89dd926..502b43d4e347f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/copy_timeline/index.ts @@ -23,8 +23,10 @@ export const copyTimelineRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .post({ path: TIMELINE_COPY_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'internal', }) 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 95fb09fb28e56..a91fefc20f934 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 @@ -32,8 +32,10 @@ export const createTimelinesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .post({ path: TIMELINE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 8dd476c9f4e44..07cffb3e13bf5 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 @@ -23,8 +23,10 @@ export const deleteTimelinesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .delete({ path: TIMELINE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 163b212840423..5a055d54a76ce 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 @@ -26,8 +26,10 @@ export const exportTimelinesRoute = (router: SecuritySolutionPluginRouter, confi router.versioned .post({ path: TIMELINE_EXPORT_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 870955f7e8691..a1ae2178fb6fd 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 @@ -26,8 +26,10 @@ export const getTimelineRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .get({ path: TIMELINE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 52995efcf4be1..01a3801ad8672 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 @@ -25,8 +25,10 @@ export const getTimelinesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .get({ path: TIMELINES_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 59a86238941ab..f66c5456c0396 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 @@ -32,8 +32,12 @@ export const importTimelinesRoute = (router: SecuritySolutionPluginRouter, confi router.versioned .post({ path: `${TIMELINE_IMPORT_URL}`, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { - tags: ['access:securitySolution'], body: { maxBytes: config.maxTimelineImportPayloadBytes, output: 'stream', 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 1297f0cb1a829..7ddea9bd5ffe7 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 @@ -26,8 +26,10 @@ export const patchTimelinesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .patch({ path: TIMELINE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) 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 cf66c02cf9c97..22d579229a73b 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 @@ -26,8 +26,10 @@ export const persistFavoriteRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .patch({ path: TIMELINE_FAVORITE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts index 0afc7d21ae296..773e74faaaf46 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/resolve_timeline/index.ts @@ -27,8 +27,10 @@ export const resolveTimelineRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .get({ path: TIMELINE_RESOLVE_URL, - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, access: 'public', }) From b6fe3628e32eabc0b7ea33e8e447df7301bd25b6 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 19 Nov 2024 18:06:20 +0100 Subject: [PATCH 19/42] [8.x] [LLM tasks] Add product documentation retrieval task (#194379) (#200754) # Backport This will backport the following commits from `main` to `8.x`: - [[LLM tasks] Add product documentation retrieval task (#194379)](https://github.com/elastic/kibana/pull/194379) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- docs/developer/plugin-list.asciidoc | 8 + docs/user/security/audit-logging.asciidoc | 9 + package.json | 3 + .../current_fields.json | 7 + .../current_mappings.json | 20 ++ packages/kbn-optimizer/limits.yml | 1 + .../check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + tsconfig.base.json | 6 + .../product-doc-artifact-builder/README.md | 48 +++- .../src/artifact/manifest.ts | 8 +- .../src/artifact/mappings.ts | 5 +- .../src/artifact/product_name.ts | 35 ++- .../src/build_artifacts.ts | 8 +- .../src/command.ts | 8 +- .../src/tasks/create_artifact.ts | 4 +- .../src/tasks/create_chunk_files.ts | 2 +- .../src/tasks/create_index.ts | 5 +- .../src/tasks/extract_documentation.ts | 10 +- .../src/tasks/index.ts | 2 +- .../src/tasks/process_documents.ts | 59 ++++ .../product-doc-artifact-builder/src/types.ts | 4 +- .../tsconfig.json | 1 + .../ai-infra/product-doc-common/README.md | 3 + .../ai-infra/product-doc-common/index.ts | 17 ++ .../product-doc-common/jest.config.js | 12 + .../ai-infra/product-doc-common/kibana.jsonc | 5 + .../ai-infra/product-doc-common/package.json | 6 + .../product-doc-common/src/artifact.test.ts | 64 +++++ .../product-doc-common/src/artifact.ts | 39 +++ .../src/artifact_content.test.ts | 23 ++ .../src/artifact_content.ts | 12 + .../product-doc-common/src/documents.ts | 31 +++ .../product-doc-common/src/indices.ts | 15 ++ .../src/manifest.ts} | 14 +- .../product-doc-common/src/product.ts | 15 ++ .../ai-infra/product-doc-common/tsconfig.json | 17 ++ x-pack/plugins/ai_infra/llm_tasks/README.md | 45 ++++ .../plugins/ai_infra/llm_tasks/jest.config.js | 19 ++ .../plugins/ai_infra/llm_tasks/kibana.jsonc | 15 ++ .../ai_infra/llm_tasks/server/config.ts | 18 ++ .../ai_infra/llm_tasks/server/index.ts | 28 ++ .../ai_infra/llm_tasks/server/plugin.ts | 56 ++++ .../ai_infra/llm_tasks/server/tasks/index.ts | 8 + .../tasks/retrieve_documentation/index.ts | 13 + .../retrieve_documentation.test.ts | 182 +++++++++++++ .../retrieve_documentation.ts | 88 ++++++ .../summarize_document.ts | 67 +++++ .../tasks/retrieve_documentation/types.ts | 72 +++++ .../ai_infra/llm_tasks/server/types.ts | 42 +++ .../llm_tasks/server/utils/tokens.test.ts | 27 ++ .../ai_infra/llm_tasks/server/utils/tokens.ts | 21 ++ .../plugins/ai_infra/llm_tasks/tsconfig.json | 27 ++ .../ai_infra/product_doc_base/README.md | 3 + .../product_doc_base/common/consts.ts | 14 + .../common/http_api/installation.ts | 26 ++ .../product_doc_base/common/install_status.ts | 28 ++ .../ai_infra/product_doc_base/jest.config.js | 23 ++ .../ai_infra/product_doc_base/kibana.jsonc | 15 ++ .../ai_infra/product_doc_base/public/index.ts | 26 ++ .../product_doc_base/public/plugin.tsx | 51 ++++ .../public/services/installation/index.ts | 9 + .../installation/installation_service.test.ts | 79 ++++++ .../installation/installation_service.ts | 40 +++ .../public/services/installation/types.ts | 18 ++ .../ai_infra/product_doc_base/public/types.ts | 22 ++ .../product_doc_base/server/config.ts | 22 ++ .../ai_infra/product_doc_base/server/index.ts | 29 ++ .../product_doc_base/server/plugin.test.ts | 96 +++++++ .../product_doc_base/server/plugin.ts | 133 +++++++++ .../product_doc_base/server/routes/index.ts | 20 ++ .../server/routes/installation.ts | 115 ++++++++ .../server/saved_objects/index.ts | 11 + .../saved_objects/product_doc_install.ts | 46 ++++ .../services/doc_install_status/index.ts | 8 + .../model_conversion.test.ts | 44 +++ .../doc_install_status/model_conversion.ts | 26 ++ .../product_doc_install_service.test.ts | 65 +++++ .../product_doc_install_service.ts | 89 ++++++ .../doc_install_status/service.mock.ts | 24 ++ .../services/doc_manager/check_license.ts | 13 + .../services/doc_manager/doc_manager.test.ts | 247 +++++++++++++++++ .../services/doc_manager/doc_manager.ts | 204 ++++++++++++++ .../server/services/doc_manager/index.ts | 15 ++ .../server/services/doc_manager/types.ts | 98 +++++++ .../endpoint_manager.test.ts | 58 ++++ .../inference_endpoint/endpoint_manager.ts | 41 +++ .../services/inference_endpoint/index.ts | 8 + .../inference_endpoint/service.mock.ts | 20 ++ .../utils/get_model_install_status.ts | 34 +++ .../inference_endpoint/utils/index.ts | 10 + .../inference_endpoint/utils/install_elser.ts | 35 +++ .../utils/wait_until_model_deployed.ts | 40 +++ .../services/package_installer/index.ts | 8 + .../package_installer.test.mocks.ts | 36 +++ .../package_installer.test.ts | 255 ++++++++++++++++++ .../package_installer/package_installer.ts | 218 +++++++++++++++ .../steps/create_index.test.ts | 84 ++++++ .../package_installer/steps/create_index.ts | 50 ++++ .../steps/fetch_artifact_versions.test.ts | 129 +++++++++ .../steps/fetch_artifact_versions.ts | 59 ++++ .../services/package_installer/steps/index.ts | 11 + .../steps/populate_index.test.ts | 109 ++++++++ .../package_installer/steps/populate_index.ts | 84 ++++++ .../steps/validate_artifact_archive.test.ts | 73 +++++ .../steps/validate_artifact_archive.ts | 24 ++ .../utils/archive_accessors.test.ts | 78 ++++++ .../utils/archive_accessors.ts | 33 +++ .../package_installer/utils/download.ts | 23 ++ .../services/package_installer/utils/index.ts | 10 + .../package_installer/utils/semver.test.ts | 29 ++ .../package_installer/utils/semver.ts | 27 ++ .../utils/test_data/test_archive_1.zip | Bin 0 -> 800 bytes .../utils/zip_archive.test.ts | 43 +++ .../package_installer/utils/zip_archive.ts | 91 +++++++ .../server/services/search/index.ts | 9 + .../server/services/search/perform_search.ts} | 28 +- .../services/search/search_service.test.ts | 51 ++++ .../server/services/search/search_service.ts | 37 +++ .../server/services/search/types.ts | 27 ++ .../get_indices_for_product_names.test.ts | 22 ++ .../utils/get_indices_for_product_names.ts | 21 ++ .../server/services/search/utils/index.ts | 9 + .../services/search/utils/map_result.test.ts | 46 ++++ .../services/search/utils/map_result.ts | 19 ++ .../server/tasks/ensure_up_to_date.ts | 70 +++++ .../product_doc_base/server/tasks/index.ts | 29 ++ .../server/tasks/install_all.ts | 70 +++++ .../server/tasks/uninstall_all.ts | 70 +++++ .../product_doc_base/server/tasks/utils.ts | 69 +++++ .../ai_infra/product_doc_base/server/types.ts | 44 +++ .../ai_infra/product_doc_base/tsconfig.json | 29 ++ .../server/plugin.ts | 2 +- .../kibana.jsonc | 3 +- .../server/functions/documentation.ts | 82 ++++++ .../server/functions/index.ts | 2 + .../server/types.ts | 2 + .../tsconfig.json | 2 + .../kibana.jsonc | 3 +- .../public/constants.ts | 3 + .../hooks/use_get_product_doc_status.ts | 32 +++ .../public/hooks/use_install_product_doc.ts | 57 ++++ .../public/hooks/use_uninstall_product_doc.ts | 57 ++++ .../public/plugin.ts | 2 + .../settings_tab/product_doc_entry.tsx | 171 ++++++++++++ .../components/settings_tab/settings_tab.tsx | 3 + .../tsconfig.json | 3 +- .../check_registered_task_types.ts | 3 + yarn.lock | 12 + 149 files changed, 5660 insertions(+), 64 deletions(-) create mode 100644 x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/process_documents.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/README.md create mode 100644 x-pack/packages/ai-infra/product-doc-common/index.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/jest.config.js create mode 100644 x-pack/packages/ai-infra/product-doc-common/kibana.jsonc create mode 100644 x-pack/packages/ai-infra/product-doc-common/package.json create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/artifact.test.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/artifact.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/artifact_content.test.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/artifact_content.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/documents.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/indices.ts rename x-pack/packages/ai-infra/{product-doc-artifact-builder/src/artifact/artifact_name.ts => product-doc-common/src/manifest.ts} (59%) create mode 100644 x-pack/packages/ai-infra/product-doc-common/src/product.ts create mode 100644 x-pack/packages/ai-infra/product-doc-common/tsconfig.json create mode 100644 x-pack/plugins/ai_infra/llm_tasks/README.md create mode 100644 x-pack/plugins/ai_infra/llm_tasks/jest.config.js create mode 100644 x-pack/plugins/ai_infra/llm_tasks/kibana.jsonc create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/config.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/index.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/plugin.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/tasks/index.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/index.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/summarize_document.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/types.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.test.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.ts create mode 100644 x-pack/plugins/ai_infra/llm_tasks/tsconfig.json create mode 100644 x-pack/plugins/ai_infra/product_doc_base/README.md create mode 100644 x-pack/plugins/ai_infra/product_doc_base/common/consts.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/common/http_api/installation.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/common/install_status.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/jest.config.js create mode 100644 x-pack/plugins/ai_infra/product_doc_base/kibana.jsonc create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/plugin.tsx create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/services/installation/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/services/installation/types.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/public/types.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/config.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/plugin.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/plugin.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/routes/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/check_license.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/types.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/service.mock.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/get_model_install_status.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/install_elser.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/wait_until_model_deployed.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.mocks.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/download.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/test_data/test_archive_1.zip create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/index.ts rename x-pack/{packages/ai-infra/product-doc-artifact-builder/src/tasks/perform_semantic_search.ts => plugins/ai_infra/product_doc_base/server/services/search/perform_search.ts} (78%) create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/types.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.test.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/tasks/index.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/tasks/install_all.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/tasks/uninstall_all.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/tasks/utils.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/server/types.ts create mode 100644 x-pack/plugins/ai_infra/product_doc_base/tsconfig.json create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/documentation.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b6ba24df78976..6915662b1eec6 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -690,6 +690,10 @@ the infrastructure monitoring use-case within Kibana. using the CURL scripts in the scripts folder. +|{kib-repo}blob/{branch}/x-pack/plugins/ai_infra/llm_tasks/README.md[llmTasks] +|This plugin contains various LLM tasks. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/logs_data_access/README.md[logsDataAccess] |Exposes services to access logs data. @@ -767,6 +771,10 @@ Elastic. |This plugin helps users learn how to use the Painless scripting language. +|{kib-repo}blob/{branch}/x-pack/plugins/ai_infra/product_doc_base/README.md[productDocBase] +|This plugin contains the product documentation base service. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_solution/profiling/README.md[profiling] |Universal Profiling provides fleet-wide, whole-system, continuous profiling with zero instrumentation. Get a comprehensive understanding of what lines of code are consuming compute resources throughout your entire fleet by visualizing your data in Kibana using the flamegraph, stacktraces, and top functions views. diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index 1ac40bcc7764a..ef12f4303c1b4 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -148,6 +148,9 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | Creating trained model. | `failure` | Failed to create trained model. +.1+| `product_documentation_create` +| `unknown` | User requested to install the product documentation for use in AI Assistants. + 3+a| ====== Type: change @@ -334,6 +337,9 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | Updating trained model deployment. | `failure` | Failed to update trained model deployment. +.1+| `product_documentation_update` +| `unknown` | User requested to update the product documentation for use in AI Assistants. + 3+a| ====== Type: deletion @@ -425,6 +431,9 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | Deleting trained model. | `failure` | Failed to delete trained model. +.1+| `product_documentation_delete` +| `unknown` | User requested to delete the product documentation for use in AI Assistants. + 3+a| ====== Type: access diff --git a/package.json b/package.json index 9905b03e9e2f7..355f1db6c8967 100644 --- a/package.json +++ b/package.json @@ -616,6 +616,7 @@ "@kbn/licensing-plugin": "link:x-pack/plugins/licensing", "@kbn/links-plugin": "link:src/plugins/links", "@kbn/lists-plugin": "link:x-pack/plugins/lists", + "@kbn/llm-tasks-plugin": "link:x-pack/plugins/ai_infra/llm_tasks", "@kbn/locator-examples-plugin": "link:examples/locator_examples", "@kbn/locator-explorer-plugin": "link:examples/locator_explorer", "@kbn/logging": "link:packages/kbn-logging", @@ -718,6 +719,8 @@ "@kbn/presentation-panel-plugin": "link:src/plugins/presentation_panel", "@kbn/presentation-publishing": "link:packages/presentation/presentation_publishing", "@kbn/presentation-util-plugin": "link:src/plugins/presentation_util", + "@kbn/product-doc-base-plugin": "link:x-pack/plugins/ai_infra/product_doc_base", + "@kbn/product-doc-common": "link:x-pack/packages/ai-infra/product-doc-common", "@kbn/profiling-data-access-plugin": "link:x-pack/plugins/observability_solution/profiling_data_access", "@kbn/profiling-plugin": "link:x-pack/plugins/observability_solution/profiling", "@kbn/profiling-utils": "link:packages/kbn-profiling-utils", diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 3808543d2319c..ab8d23b6a5a8a 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -855,6 +855,13 @@ "policy-settings-protection-updates-note": [ "note" ], + "product-doc-install-status": [ + "index_name", + "installation_status", + "last_installation_date", + "product_name", + "product_version" + ], "query": [ "description", "title", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 660814010f853..f2ad165cc4b72 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -2841,6 +2841,26 @@ } } }, + "product-doc-install-status": { + "dynamic": false, + "properties": { + "index_name": { + "type": "keyword" + }, + "installation_status": { + "type": "keyword" + }, + "last_installation_date": { + "type": "date" + }, + "product_name": { + "type": "keyword" + }, + "product_version": { + "type": "keyword" + } + } + }, "query": { "dynamic": false, "properties": { diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7e6d18a75dd4d..c232610a4f51b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -124,6 +124,7 @@ pageLoadAssetSize: painlessLab: 179748 presentationPanel: 55463 presentationUtil: 58834 + productDocBase: 22500 profiling: 36694 remoteClusters: 51327 reporting: 58600 diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 41bc90ddacc2e..3890a9fe360e4 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -145,6 +145,7 @@ describe('checking migration metadata changes on all registered SO types', () => "osquery-pack-asset": "cd140bc2e4b092e93692b587bf6e38051ef94c75", "osquery-saved-query": "6095e288750aa3164dfe186c74bc5195c2bf2bd4", "policy-settings-protection-updates-note": "33924bb246f9e5bcb876109cc83e3c7a28308352", + "product-doc-install-status": "ca6e96840228e4cc2f11bae24a0797f4f7238c8c", "query": "501bece68f26fe561286a488eabb1a8ab12f1137", "risk-engine-configuration": "aea0c371a462e6d07c3ceb3aff11891b47feb09d", "rules-settings": "ba57ef1881b3dcbf48fbfb28902d8f74442190b2", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index ba06073e454a9..3ceba522d08cb 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -115,6 +115,7 @@ const previouslyRegisteredTypes = [ 'osquery-usage-metric', 'osquery-manager-usage-metric', 'policy-settings-protection-updates-note', + 'product-doc-install-status', 'query', 'rules-settings', 'sample-data-telemetry', diff --git a/tsconfig.base.json b/tsconfig.base.json index 7457eb69a05df..ce13591067fa9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1146,6 +1146,8 @@ "@kbn/lint-ts-projects-cli/*": ["packages/kbn-lint-ts-projects-cli/*"], "@kbn/lists-plugin": ["x-pack/plugins/lists"], "@kbn/lists-plugin/*": ["x-pack/plugins/lists/*"], + "@kbn/llm-tasks-plugin": ["x-pack/plugins/ai_infra/llm_tasks"], + "@kbn/llm-tasks-plugin/*": ["x-pack/plugins/ai_infra/llm_tasks/*"], "@kbn/locator-examples-plugin": ["examples/locator_examples"], "@kbn/locator-examples-plugin/*": ["examples/locator_examples/*"], "@kbn/locator-explorer-plugin": ["examples/locator_explorer"], @@ -1384,6 +1386,10 @@ "@kbn/presentation-util-plugin/*": ["src/plugins/presentation_util/*"], "@kbn/product-doc-artifact-builder": ["x-pack/packages/ai-infra/product-doc-artifact-builder"], "@kbn/product-doc-artifact-builder/*": ["x-pack/packages/ai-infra/product-doc-artifact-builder/*"], + "@kbn/product-doc-base-plugin": ["x-pack/plugins/ai_infra/product_doc_base"], + "@kbn/product-doc-base-plugin/*": ["x-pack/plugins/ai_infra/product_doc_base/*"], + "@kbn/product-doc-common": ["x-pack/packages/ai-infra/product-doc-common"], + "@kbn/product-doc-common/*": ["x-pack/packages/ai-infra/product-doc-common/*"], "@kbn/profiling-data-access-plugin": ["x-pack/plugins/observability_solution/profiling_data_access"], "@kbn/profiling-data-access-plugin/*": ["x-pack/plugins/observability_solution/profiling_data_access/*"], "@kbn/profiling-plugin": ["x-pack/plugins/observability_solution/profiling"], diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md b/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md index eb64d53b5b8f7..49949def3e5e7 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/README.md @@ -1,3 +1,49 @@ # @kbn/product-doc-artifact-builder -Script to build the knowledge base artifacts +Script to build the knowledge base artifacts. + +## How to run + +``` +node scripts/build_product_doc_artifacts.js --stack-version {version} --product-name {product} +``` + +### parameters + +#### `stack-version`: + +the stack version to generate the artifacts for. + +#### `product-name`: + +(multi-value) the list of products to generate artifacts for. + +possible values: +- "kibana" +- "elasticsearch" +- "observability" +- "security" + +#### `target-folder`: + +The folder to generate the artifacts in. + +Defaults to `{REPO_ROOT}/build-kb-artifacts`. + +#### `build-folder`: + +The folder to use for temporary files. + +Defaults to `{REPO_ROOT}/build/temp-kb-artifacts` + +#### Cluster infos + +- params for the source cluster: +`sourceClusterUrl` / env.KIBANA_SOURCE_CLUSTER_URL +`sourceClusterUsername` / env.KIBANA_SOURCE_CLUSTER_USERNAME +`sourceClusterPassword` / env.KIBANA_SOURCE_CLUSTER_PASSWORD + +- params for the embedding cluster: +`embeddingClusterUrl` / env.KIBANA_EMBEDDING_CLUSTER_URL +`embeddingClusterUsername` / env.KIBANA_EMBEDDING_CLUSTER_USERNAME +`embeddingClusterPassword` / env.KIBANA_EMBEDDING_CLUSTER_PASSWORD \ No newline at end of file diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/manifest.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/manifest.ts index cbebcdc22981b..a8aa927c5ef1f 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/manifest.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/manifest.ts @@ -5,17 +5,13 @@ * 2.0. */ -export interface ArtifactManifest { - formatVersion: string; - productName: string; - productVersion: string; -} +import type { ArtifactManifest, ProductName } from '@kbn/product-doc-common'; export const getArtifactManifest = ({ productName, stackVersion, }: { - productName: string; + productName: ProductName; stackVersion: string; }): ArtifactManifest => { return { diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts index ae84ae60616a3..979845ec31844 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/mappings.ts @@ -21,10 +21,7 @@ export const getArtifactMappings = (inferenceEndpoint: string): MappingTypeMappi slug: { type: 'keyword' }, url: { type: 'keyword' }, version: { type: 'version' }, - ai_subtitle: { - type: 'semantic_text', - inference_id: inferenceEndpoint, - }, + ai_subtitle: { type: 'text' }, ai_summary: { type: 'semantic_text', inference_id: inferenceEndpoint, diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/product_name.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/product_name.ts index cfcc141323f4f..e4ca33849a527 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/product_name.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/product_name.ts @@ -5,7 +5,34 @@ * 2.0. */ -/** - * The allowed product names, as found in the source's cluster - */ -export const sourceProductNames = ['Kibana', 'Elasticsearch', 'Security', 'Observability']; +import type { ProductName } from '@kbn/product-doc-common'; + +const productNameToSourceNamesMap: Record = { + kibana: ['Kibana'], + elasticsearch: ['Elasticsearch'], + security: ['Security'], + observability: ['Observability'], +}; + +const sourceNameToProductName = Object.entries(productNameToSourceNamesMap).reduce< + Record +>((map, [productName, sourceNames]) => { + sourceNames.forEach((sourceName) => { + map[sourceName] = productName as ProductName; + }); + return map; +}, {}); + +export const getSourceNamesFromProductName = (productName: ProductName): string[] => { + if (!productNameToSourceNamesMap[productName]) { + throw new Error(`Unknown product name: ${productName}`); + } + return productNameToSourceNamesMap[productName]; +}; + +export const getProductNameFromSource = (source: string): ProductName => { + if (!sourceNameToProductName[source]) { + throw new Error(`Unknown source name: ${source}`); + } + return sourceNameToProductName[source]; +}; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts index bbde3310f8e3a..551f58bc68308 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/build_artifacts.ts @@ -8,6 +8,7 @@ import Path from 'path'; import { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; +import type { ProductName } from '@kbn/product-doc-common'; import { // checkConnectivity, createTargetIndex, @@ -18,6 +19,7 @@ import { createArtifact, cleanupFolders, deleteIndex, + processDocuments, } from './tasks'; import type { TaskConfig } from './types'; @@ -93,7 +95,7 @@ const buildArtifact = async ({ sourceClient, log, }: { - productName: string; + productName: ProductName; stackVersion: string; buildFolder: string; targetFolder: string; @@ -105,7 +107,7 @@ const buildArtifact = async ({ const targetIndex = getTargetIndexName({ productName, stackVersion }); - const documents = await extractDocumentation({ + let documents = await extractDocumentation({ client: sourceClient, index: 'search-docs-1', log, @@ -113,6 +115,8 @@ const buildArtifact = async ({ stackVersion, }); + documents = await processDocuments({ documents, log }); + await createTargetIndex({ client: embeddingClient, indexName: targetIndex, diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts index 49af1d158db83..e8d0d9486e331 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/command.ts @@ -6,19 +6,19 @@ */ import Path from 'path'; -import { REPO_ROOT } from '@kbn/repo-info'; import yargs from 'yargs'; +import { REPO_ROOT } from '@kbn/repo-info'; +import { DocumentationProduct } from '@kbn/product-doc-common'; import type { TaskConfig } from './types'; import { buildArtifacts } from './build_artifacts'; -import { sourceProductNames } from './artifact/product_name'; function options(y: yargs.Argv) { return y .option('productName', { describe: 'name of products to generate documentation for', array: true, - choices: sourceProductNames, - default: ['Kibana'], + choices: Object.values(DocumentationProduct), + default: [DocumentationProduct.kibana], }) .option('stackVersion', { describe: 'The stack version to generate documentation for', diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts index 343099876585a..056887a41a4d2 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_artifact.ts @@ -8,9 +8,9 @@ import Path from 'path'; import AdmZip from 'adm-zip'; import type { ToolingLog } from '@kbn/tooling-log'; +import { getArtifactName, type ProductName } from '@kbn/product-doc-common'; import { getArtifactMappings } from '../artifact/mappings'; import { getArtifactManifest } from '../artifact/manifest'; -import { getArtifactName } from '../artifact/artifact_name'; export const createArtifact = async ({ productName, @@ -21,7 +21,7 @@ export const createArtifact = async ({ }: { buildFolder: string; targetFolder: string; - productName: string; + productName: ProductName; stackVersion: string; log: ToolingLog; }) => { diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_chunk_files.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_chunk_files.ts index 8b0e7323c2886..73cf8f0109228 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_chunk_files.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_chunk_files.ts @@ -10,7 +10,7 @@ import Fs from 'fs/promises'; import type { Client } from '@elastic/elasticsearch'; import type { ToolingLog } from '@kbn/tooling-log'; -const fileSizeLimit = 250_000; +const fileSizeLimit = 500_000; export const createChunkFiles = async ({ index, diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts index e4f24725883ab..d26ffc980f3ab 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/create_index.ts @@ -21,10 +21,7 @@ const mappings: MappingTypeMapping = { slug: { type: 'keyword' }, url: { type: 'keyword' }, version: { type: 'version' }, - ai_subtitle: { - type: 'semantic_text', - inference_id: 'kibana-elser2', - }, + ai_subtitle: { type: 'text' }, ai_summary: { type: 'semantic_text', inference_id: 'kibana-elser2', diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts index f1dd051394bbd..6aa8bb49b0cfd 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/extract_documentation.ts @@ -8,6 +8,8 @@ import type { Client } from '@elastic/elasticsearch'; import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import type { ToolingLog } from '@kbn/tooling-log'; +import type { ProductName } from '@kbn/product-doc-common'; +import { getSourceNamesFromProductName, getProductNameFromSource } from '../artifact/product_name'; /** the list of fields to import from the source cluster */ const fields = [ @@ -27,7 +29,7 @@ const fields = [ export interface ExtractedDocument { content_title: string; content_body: string; - product_name: string; + product_name: ProductName; root_type: string; slug: string; url: string; @@ -43,7 +45,7 @@ const convertHit = (hit: SearchHit): ExtractedDocument => { return { content_title: source.content_title, content_body: source.content_body, - product_name: source.product_name, + product_name: getProductNameFromSource(source.product_name), root_type: 'documentation', slug: source.slug, url: source.url, @@ -65,7 +67,7 @@ export const extractDocumentation = async ({ client: Client; index: string; stackVersion: string; - productName: string; + productName: ProductName; log: ToolingLog; }) => { log.info(`Starting to extract documents from source cluster`); @@ -76,7 +78,7 @@ export const extractDocumentation = async ({ query: { bool: { must: [ - { term: { product_name: productName } }, + { terms: { product_name: getSourceNamesFromProductName(productName) } }, { term: { version: stackVersion } }, { exists: { field: 'ai_fields.ai_summary' } }, ], diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/index.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/index.ts index 0c63431362329..ec94e4c135c17 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/index.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/index.ts @@ -10,8 +10,8 @@ export { indexDocuments } from './index_documents'; export { createTargetIndex } from './create_index'; export { installElser } from './install_elser'; export { createChunkFiles } from './create_chunk_files'; -export { performSemanticSearch } from './perform_semantic_search'; export { checkConnectivity } from './check_connectivity'; export { createArtifact } from './create_artifact'; export { cleanupFolders } from './cleanup_folders'; export { deleteIndex } from './delete_index'; +export { processDocuments } from './process_documents'; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/process_documents.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/process_documents.ts new file mode 100644 index 0000000000000..69141ca167ab4 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/process_documents.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 { uniqBy } from 'lodash'; +import { encode } from 'gpt-tokenizer'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type { ExtractedDocument } from './extract_documentation'; + +export const processDocuments = async ({ + documents, + log, +}: { + documents: ExtractedDocument[]; + log: ToolingLog; +}): Promise => { + log.info('Starting processing documents.'); + const initialCount = documents.length; + documents = removeDuplicates(documents); + const noDupCount = documents.length; + log.info(`Removed ${initialCount - noDupCount} duplicates`); + documents.forEach(processDocument); + documents = filterEmptyDocs(documents); + log.info(`Removed ${noDupCount - documents.length} empty documents`); + log.info('Done processing documents.'); + return documents; +}; + +const removeDuplicates = (documents: ExtractedDocument[]): ExtractedDocument[] => { + return uniqBy(documents, (doc) => doc.slug); +}; + +/** + * Filter "this content has moved" or "deleted pages" type of documents, just based on token count. + */ +const filterEmptyDocs = (documents: ExtractedDocument[]): ExtractedDocument[] => { + return documents.filter((doc) => { + const tokenCount = encode(doc.content_body).length; + if (tokenCount < 100) { + return false; + } + return true; + }); +}; + +const processDocument = (document: ExtractedDocument) => { + document.content_body = document.content_body + // remove those "edit" button text that got embedded into titles. + .replaceAll(/([a-zA-Z])edit\n/g, (match) => { + return `${match[0]}\n`; + }) + // limit to 2 consecutive carriage return + .replaceAll(/\n\n+/g, '\n\n'); + + return document; +}; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts index d2acfb5774500..1eb4a4348d218 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/types.ts @@ -5,8 +5,10 @@ * 2.0. */ +import type { ProductName } from '@kbn/product-doc-common'; + export interface TaskConfig { - productNames: string[]; + productNames: ProductName[]; stackVersion: string; buildFolder: string; targetFolder: string; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json b/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json index 508d4c715d0a7..68ff27852c4d1 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/tsconfig.json @@ -16,5 +16,6 @@ "kbn_references": [ "@kbn/tooling-log", "@kbn/repo-info", + "@kbn/product-doc-common", ] } diff --git a/x-pack/packages/ai-infra/product-doc-common/README.md b/x-pack/packages/ai-infra/product-doc-common/README.md new file mode 100644 index 0000000000000..ff20c0e0fd0e7 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/README.md @@ -0,0 +1,3 @@ +# @kbn/product-doc-common + +Common types and utilities for the product documentation feature. diff --git a/x-pack/packages/ai-infra/product-doc-common/index.ts b/x-pack/packages/ai-infra/product-doc-common/index.ts new file mode 100644 index 0000000000000..1a96737138991 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { getArtifactName, parseArtifactName } from './src/artifact'; +export { type ArtifactManifest } from './src/manifest'; +export { DocumentationProduct, type ProductName } from './src/product'; +export { isArtifactContentFilePath } from './src/artifact_content'; +export { + productDocIndexPrefix, + productDocIndexPattern, + getProductDocIndexName, +} from './src/indices'; +export type { ProductDocumentationAttributes } from './src/documents'; diff --git a/x-pack/packages/ai-infra/product-doc-common/jest.config.js b/x-pack/packages/ai-infra/product-doc-common/jest.config.js new file mode 100644 index 0000000000000..e6cae43806c8d --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/jest.config.js @@ -0,0 +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. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../..', + roots: ['/x-pack/packages/ai-infra/product-doc-common'], +}; diff --git a/x-pack/packages/ai-infra/product-doc-common/kibana.jsonc b/x-pack/packages/ai-infra/product-doc-common/kibana.jsonc new file mode 100644 index 0000000000000..16336c1fc8e27 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/product-doc-common", + "owner": "@elastic/appex-ai-infra" +} diff --git a/x-pack/packages/ai-infra/product-doc-common/package.json b/x-pack/packages/ai-infra/product-doc-common/package.json new file mode 100644 index 0000000000000..839d411a2efb9 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/product-doc-common", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/packages/ai-infra/product-doc-common/src/artifact.test.ts b/x-pack/packages/ai-infra/product-doc-common/src/artifact.test.ts new file mode 100644 index 0000000000000..2b6362dbf4aad --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/artifact.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getArtifactName, parseArtifactName } from './artifact'; + +describe('getArtifactName', () => { + it('builds the name based on the provided product name and version', () => { + expect( + getArtifactName({ + productName: 'kibana', + productVersion: '8.16', + }) + ).toEqual('kb-product-doc-kibana-8.16.zip'); + }); + + it('excludes the extension when excludeExtension is true', () => { + expect( + getArtifactName({ + productName: 'elasticsearch', + productVersion: '8.17', + excludeExtension: true, + }) + ).toEqual('kb-product-doc-elasticsearch-8.17'); + }); + + it('generates a lowercase name', () => { + expect( + getArtifactName({ + // @ts-expect-error testing + productName: 'ElasticSearch', + productVersion: '8.17', + excludeExtension: true, + }) + ).toEqual('kb-product-doc-elasticsearch-8.17'); + }); +}); + +describe('parseArtifactName', () => { + it('parses an artifact name with extension', () => { + expect(parseArtifactName('kb-product-doc-kibana-8.16.zip')).toEqual({ + productName: 'kibana', + productVersion: '8.16', + }); + }); + + it('parses an artifact name without extension', () => { + expect(parseArtifactName('kb-product-doc-security-8.17')).toEqual({ + productName: 'security', + productVersion: '8.17', + }); + }); + + it('returns undefined if the provided string does not match the artifact name pattern', () => { + expect(parseArtifactName('some-wrong-name')).toEqual(undefined); + }); + + it('returns undefined if the provided string is not strictly lowercase', () => { + expect(parseArtifactName('kb-product-doc-Security-8.17')).toEqual(undefined); + }); +}); diff --git a/x-pack/packages/ai-infra/product-doc-common/src/artifact.ts b/x-pack/packages/ai-infra/product-doc-common/src/artifact.ts new file mode 100644 index 0000000000000..1a6745abd733d --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/artifact.ts @@ -0,0 +1,39 @@ +/* + * 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 ProductName, DocumentationProduct } from './product'; + +// kb-product-doc-elasticsearch-8.15.zip +const artifactNameRegexp = /^kb-product-doc-([a-z]+)-([0-9]+\.[0-9]+)(\.zip)?$/; +const allowedProductNames: ProductName[] = Object.values(DocumentationProduct); + +export const getArtifactName = ({ + productName, + productVersion, + excludeExtension = false, +}: { + productName: ProductName; + productVersion: string; + excludeExtension?: boolean; +}): string => { + const ext = excludeExtension ? '' : '.zip'; + return `kb-product-doc-${productName}-${productVersion}${ext}`.toLowerCase(); +}; + +export const parseArtifactName = (artifactName: string) => { + const match = artifactNameRegexp.exec(artifactName); + if (match) { + const productName = match[1].toLowerCase() as ProductName; + const productVersion = match[2].toLowerCase(); + if (allowedProductNames.includes(productName)) { + return { + productName, + productVersion, + }; + } + } +}; diff --git a/x-pack/packages/ai-infra/product-doc-common/src/artifact_content.test.ts b/x-pack/packages/ai-infra/product-doc-common/src/artifact_content.test.ts new file mode 100644 index 0000000000000..3f97aaf94f880 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/artifact_content.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { isArtifactContentFilePath } from './artifact_content'; + +describe('isArtifactContentFilePath', () => { + it('returns true for filenames matching the pattern', () => { + expect(isArtifactContentFilePath('content/content-0.ndjson')).toEqual(true); + expect(isArtifactContentFilePath('content/content-007.ndjson')).toEqual(true); + expect(isArtifactContentFilePath('content/content-9042.ndjson')).toEqual(true); + }); + + it('returns false for filenames not matching the pattern', () => { + expect(isArtifactContentFilePath('content-0.ndjson')).toEqual(false); + expect(isArtifactContentFilePath('content/content-0')).toEqual(false); + expect(isArtifactContentFilePath('content/content.ndjson')).toEqual(false); + expect(isArtifactContentFilePath('content/content-9042.json')).toEqual(false); + }); +}); diff --git a/x-pack/packages/ai-infra/product-doc-common/src/artifact_content.ts b/x-pack/packages/ai-infra/product-doc-common/src/artifact_content.ts new file mode 100644 index 0000000000000..757e6664bb588 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/artifact_content.ts @@ -0,0 +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. + */ + +const contentFileRegexp = /^content\/content-[0-9]+\.ndjson$/; + +export const isArtifactContentFilePath = (path: string): boolean => { + return contentFileRegexp.test(path); +}; diff --git a/x-pack/packages/ai-infra/product-doc-common/src/documents.ts b/x-pack/packages/ai-infra/product-doc-common/src/documents.ts new file mode 100644 index 0000000000000..ef81b3d6411cc --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/documents.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 type { ProductName } from './product'; + +// don't need to define the other props +interface SemanticTextField { + text: string; +} + +interface SemanticTextArrayField { + text: string[]; +} + +export interface ProductDocumentationAttributes { + content_title: string; + content_body: SemanticTextField; + product_name: ProductName; + root_type: string; + slug: string; + url: string; + version: string; + ai_subtitle: string; + ai_summary: SemanticTextField; + ai_questions_answered: SemanticTextArrayField; + ai_tags: string[]; +} diff --git a/x-pack/packages/ai-infra/product-doc-common/src/indices.ts b/x-pack/packages/ai-infra/product-doc-common/src/indices.ts new file mode 100644 index 0000000000000..b48cacf79fd23 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/indices.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ProductName } from './product'; + +export const productDocIndexPrefix = '.kibana-ai-product-doc'; +export const productDocIndexPattern = `${productDocIndexPrefix}-*`; + +export const getProductDocIndexName = (productName: ProductName): string => { + return `${productDocIndexPrefix}-${productName.toLowerCase()}`; +}; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/artifact_name.ts b/x-pack/packages/ai-infra/product-doc-common/src/manifest.ts similarity index 59% rename from x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/artifact_name.ts rename to x-pack/packages/ai-infra/product-doc-common/src/manifest.ts index 678b17088c7b4..6c246cf58fd5f 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/artifact/artifact_name.ts +++ b/x-pack/packages/ai-infra/product-doc-common/src/manifest.ts @@ -5,12 +5,10 @@ * 2.0. */ -export const getArtifactName = ({ - productName, - productVersion, -}: { - productName: string; +import type { ProductName } from './product'; + +export interface ArtifactManifest { + formatVersion: string; + productName: ProductName; productVersion: string; -}): string => { - return `kibana-kb-${productName}-${productVersion}.zip`.toLowerCase(); -}; +} diff --git a/x-pack/packages/ai-infra/product-doc-common/src/product.ts b/x-pack/packages/ai-infra/product-doc-common/src/product.ts new file mode 100644 index 0000000000000..417033f5083ec --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/src/product.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum DocumentationProduct { + kibana = 'kibana', + elasticsearch = 'elasticsearch', + observability = 'observability', + security = 'security', +} + +export type ProductName = keyof typeof DocumentationProduct; diff --git a/x-pack/packages/ai-infra/product-doc-common/tsconfig.json b/x-pack/packages/ai-infra/product-doc-common/tsconfig.json new file mode 100644 index 0000000000000..0d78dace105e1 --- /dev/null +++ b/x-pack/packages/ai-infra/product-doc-common/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/x-pack/plugins/ai_infra/llm_tasks/README.md b/x-pack/plugins/ai_infra/llm_tasks/README.md new file mode 100644 index 0000000000000..e019d456cd65a --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/README.md @@ -0,0 +1,45 @@ +# LLM Tasks plugin + +This plugin contains various LLM tasks. + +## Retrieve documentation + +This task allows to retrieve documents from our Elastic product documentation. + +The task depends on the `product-doc-base` plugin, as this dependency is used +to install and manage the product documentation. + +### Checking if the task is available + +A `retrieveDocumentationAvailable` API is exposed from the start contract, that +should be used to assert that the `retrieve_doc` task can be used in the current +context. + +That API receive the inbound request as parameter. + +Example: +```ts +if (await llmTasksStart.retrieveDocumentationAvailable({ request })) { + // task is available +} else { + // task is not available +} +``` + +### Executing the task + +The task is executed as an API of the plugin's start contract, and can be invoked +as any other lifecycle API would. + +Example: +```ts +const result = await llmTasksStart.retrieveDocumentation({ + searchTerm: "How to create a space in Kibana?", + request, + connectorId: 'my-connector-id', +}); + +const { success, documents } = result; +``` + +The exhaustive list of options for the task is available on the `RetrieveDocumentationParams` type's TS doc. diff --git a/x-pack/plugins/ai_infra/llm_tasks/jest.config.js b/x-pack/plugins/ai_infra/llm_tasks/jest.config.js new file mode 100644 index 0000000000000..2a6206d4304b9 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/jest.config.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/plugins/ai_infra/llm_tasks/server'], + setupFiles: [], + collectCoverage: true, + collectCoverageFrom: [ + '/x-pack/plugins/ai_infra/llm_tasks/{public,server,common}/**/*.{js,ts,tsx}', + ], + + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/ai_infra/llm_tasks/kibana.jsonc b/x-pack/plugins/ai_infra/llm_tasks/kibana.jsonc new file mode 100644 index 0000000000000..1ef211d01210e --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/kibana.jsonc @@ -0,0 +1,15 @@ +{ + "type": "plugin", + "id": "@kbn/llm-tasks-plugin", + "owner": "@elastic/appex-ai-infra", + "plugin": { + "id": "llmTasks", + "server": true, + "browser": false, + "configPath": ["xpack", "llmTasks"], + "requiredPlugins": ["inference", "productDocBase"], + "requiredBundles": [], + "optionalPlugins": [], + "extraPublicDirs": [] + } +} diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/config.ts b/x-pack/plugins/ai_infra/llm_tasks/server/config.ts new file mode 100644 index 0000000000000..c509af8bda64b --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, type TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from '@kbn/core/server'; + +const configSchema = schema.object({}); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: {}, +}; + +export type LlmTasksConfig = TypeOf; diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/index.ts b/x-pack/plugins/ai_infra/llm_tasks/server/index.ts new file mode 100644 index 0000000000000..1b18426dc2c34 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginInitializer, PluginInitializerContext } from '@kbn/core/server'; +import type { LlmTasksConfig } from './config'; +import type { + LlmTasksPluginSetup, + LlmTasksPluginStart, + PluginSetupDependencies, + PluginStartDependencies, +} from './types'; +import { LlmTasksPlugin } from './plugin'; + +export { config } from './config'; + +export type { LlmTasksPluginSetup, LlmTasksPluginStart }; + +export const plugin: PluginInitializer< + LlmTasksPluginSetup, + LlmTasksPluginStart, + PluginSetupDependencies, + PluginStartDependencies +> = async (pluginInitializerContext: PluginInitializerContext) => + new LlmTasksPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/plugin.ts b/x-pack/plugins/ai_infra/llm_tasks/server/plugin.ts new file mode 100644 index 0000000000000..d10c495ece159 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import type { LlmTasksConfig } from './config'; +import type { + LlmTasksPluginSetup, + LlmTasksPluginStart, + PluginSetupDependencies, + PluginStartDependencies, +} from './types'; +import { retrieveDocumentation } from './tasks'; + +export class LlmTasksPlugin + implements + Plugin< + LlmTasksPluginSetup, + LlmTasksPluginStart, + PluginSetupDependencies, + PluginStartDependencies + > +{ + private logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + setupDependencies: PluginSetupDependencies + ): LlmTasksPluginSetup { + return {}; + } + + start(core: CoreStart, startDependencies: PluginStartDependencies): LlmTasksPluginStart { + const { inference, productDocBase } = startDependencies; + return { + retrieveDocumentationAvailable: async () => { + const docBaseStatus = await startDependencies.productDocBase.management.getStatus(); + return docBaseStatus.status === 'installed'; + }, + retrieveDocumentation: (options) => { + return retrieveDocumentation({ + outputAPI: inference.getClient({ request: options.request }).output, + searchDocAPI: productDocBase.search, + logger: this.logger.get('tasks.retrieve-documentation'), + })(options); + }, + }; + } +} diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/tasks/index.ts b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/index.ts new file mode 100644 index 0000000000000..41d3911823449 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { retrieveDocumentation } from './retrieve_documentation'; diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/index.ts b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/index.ts new file mode 100644 index 0000000000000..22bf0745bd77f --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/index.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { retrieveDocumentation } from './retrieve_documentation'; +export type { + RetrieveDocumentationAPI, + RetrieveDocumentationResult, + RetrieveDocumentationParams, +} from './types'; diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts new file mode 100644 index 0000000000000..5722b73ca039c --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.test.ts @@ -0,0 +1,182 @@ +/* + * 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 { httpServerMock } from '@kbn/core/server/mocks'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import type { DocSearchResult } from '@kbn/product-doc-base-plugin/server/services/search'; + +import { retrieveDocumentation } from './retrieve_documentation'; +import { truncate, count as countTokens } from '../../utils/tokens'; +jest.mock('../../utils/tokens'); +const truncateMock = truncate as jest.MockedFn; +const countTokensMock = countTokens as jest.MockedFn; + +import { summarizeDocument } from './summarize_document'; +jest.mock('./summarize_document'); +const summarizeDocumentMock = summarizeDocument as jest.MockedFn; + +describe('retrieveDocumentation', () => { + let logger: MockedLogger; + let request: ReturnType; + let outputAPI: jest.Mock; + let searchDocAPI: jest.Mock; + let retrieve: ReturnType; + + const createResult = (parts: Partial = {}): DocSearchResult => { + return { + title: 'title', + content: 'content', + url: 'url', + productName: 'kibana', + ...parts, + }; + }; + + beforeEach(() => { + logger = loggerMock.create(); + request = httpServerMock.createKibanaRequest(); + outputAPI = jest.fn(); + searchDocAPI = jest.fn(); + retrieve = retrieveDocumentation({ logger, searchDocAPI, outputAPI }); + }); + + afterEach(() => { + summarizeDocumentMock.mockReset(); + truncateMock.mockReset(); + countTokensMock.mockReset(); + }); + + it('calls the search API with the right parameters', async () => { + searchDocAPI.mockResolvedValue({ results: [] }); + + const result = await retrieve({ + searchTerm: 'What is Kibana?', + products: ['kibana'], + request, + max: 5, + connectorId: '.my-connector', + functionCalling: 'simulated', + }); + + expect(result).toEqual({ + success: true, + documents: [], + }); + + expect(searchDocAPI).toHaveBeenCalledTimes(1); + expect(searchDocAPI).toHaveBeenCalledWith({ + query: 'What is Kibana?', + products: ['kibana'], + max: 5, + }); + }); + + it('reduces the document length using the truncate strategy', async () => { + searchDocAPI.mockResolvedValue({ + results: [ + createResult({ content: 'content-1' }), + createResult({ content: 'content-2' }), + createResult({ content: 'content-3' }), + ], + }); + + countTokensMock.mockImplementation((text) => { + if (text === 'content-2') { + return 150; + } else { + return 50; + } + }); + truncateMock.mockReturnValue('truncated'); + + const result = await retrieve({ + searchTerm: 'What is Kibana?', + request, + connectorId: '.my-connector', + maxDocumentTokens: 100, + tokenReductionStrategy: 'truncate', + }); + + expect(result.documents.length).toEqual(3); + expect(result.documents[0].content).toEqual('content-1'); + expect(result.documents[1].content).toEqual('truncated'); + expect(result.documents[2].content).toEqual('content-3'); + + expect(truncateMock).toHaveBeenCalledTimes(1); + expect(truncateMock).toHaveBeenCalledWith('content-2', 100); + }); + + it('reduces the document length using the summarize strategy', async () => { + searchDocAPI.mockResolvedValue({ + results: [ + createResult({ content: 'content-1' }), + createResult({ content: 'content-2' }), + createResult({ content: 'content-3' }), + ], + }); + + countTokensMock.mockImplementation((text) => { + if (text === 'content-2') { + return 50; + } else { + return 150; + } + }); + truncateMock.mockImplementation((text) => text); + + summarizeDocumentMock.mockImplementation(({ documentContent }) => { + return Promise.resolve({ summary: `${documentContent}-summarized` }); + }); + + const result = await retrieve({ + searchTerm: 'What is Kibana?', + request, + connectorId: '.my-connector', + maxDocumentTokens: 100, + tokenReductionStrategy: 'summarize', + }); + + expect(result.documents.length).toEqual(3); + expect(result.documents[0].content).toEqual('content-1-summarized'); + expect(result.documents[1].content).toEqual('content-2'); + expect(result.documents[2].content).toEqual('content-3-summarized'); + + expect(truncateMock).toHaveBeenCalledTimes(2); + expect(truncateMock).toHaveBeenCalledWith('content-1-summarized', 100); + expect(truncateMock).toHaveBeenCalledWith('content-3-summarized', 100); + }); + + it('logs an error and return an empty list of docs in case of error', async () => { + searchDocAPI.mockResolvedValue({ + results: [createResult({ content: 'content-1' })], + }); + countTokensMock.mockImplementation(() => { + return 150; + }); + summarizeDocumentMock.mockImplementation(() => { + throw new Error('woups'); + }); + + const result = await retrieve({ + searchTerm: 'What is Kibana?', + request, + connectorId: '.my-connector', + maxDocumentTokens: 100, + tokenReductionStrategy: 'summarize', + }); + + expect(result).toEqual({ + success: false, + documents: [], + }); + + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Error retrieving documentation') + ); + }); +}); diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts new file mode 100644 index 0000000000000..96f966e483601 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/retrieve_documentation.ts @@ -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 type { Logger } from '@kbn/logging'; +import type { OutputAPI } from '@kbn/inference-common'; +import type { ProductDocSearchAPI } from '@kbn/product-doc-base-plugin/server'; +import { truncate, count as countTokens } from '../../utils/tokens'; +import type { RetrieveDocumentationAPI } from './types'; +import { summarizeDocument } from './summarize_document'; + +const MAX_DOCUMENTS_DEFAULT = 3; +const MAX_TOKENS_DEFAULT = 1000; + +export const retrieveDocumentation = + ({ + outputAPI, + searchDocAPI, + logger: log, + }: { + outputAPI: OutputAPI; + searchDocAPI: ProductDocSearchAPI; + logger: Logger; + }): RetrieveDocumentationAPI => + async ({ + searchTerm, + connectorId, + products, + functionCalling, + max = MAX_DOCUMENTS_DEFAULT, + maxDocumentTokens = MAX_TOKENS_DEFAULT, + tokenReductionStrategy = 'summarize', + }) => { + try { + const { results } = await searchDocAPI({ query: searchTerm, products, max }); + + log.debug(`searching with term=[${searchTerm}] returned ${results.length} documents`); + + const processedDocuments = await Promise.all( + results.map(async (document) => { + const tokenCount = countTokens(document.content); + const docHasTooManyTokens = tokenCount >= maxDocumentTokens; + log.debug( + `processing doc [${document.url}] - tokens : [${tokenCount}] - tooManyTokens: [${docHasTooManyTokens}]` + ); + + let content = document.content; + if (docHasTooManyTokens) { + if (tokenReductionStrategy === 'summarize') { + const extractResponse = await summarizeDocument({ + searchTerm, + documentContent: document.content, + outputAPI, + connectorId, + functionCalling, + }); + content = truncate(extractResponse.summary, maxDocumentTokens); + } else { + content = truncate(document.content, maxDocumentTokens); + } + } + + log.debug(`done processing document [${document.url}]`); + return { + title: document.title, + url: document.url, + content, + }; + }) + ); + + log.debug(() => { + const docsAsJson = JSON.stringify(processedDocuments); + return `searching with term=[${searchTerm}] - results: ${docsAsJson}`; + }); + + return { + success: true, + documents: processedDocuments.filter((doc) => doc.content.length > 0), + }; + } catch (e) { + log.error(`Error retrieving documentation: ${e.message}. Returning empty results.`); + return { success: false, documents: [] }; + } + }; diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/summarize_document.ts b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/summarize_document.ts new file mode 100644 index 0000000000000..815cbc94d08f8 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/summarize_document.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 type { ToolSchema, FunctionCallingMode, OutputAPI } from '@kbn/inference-common'; + +const summarizeDocumentSchema = { + type: 'object', + properties: { + useful: { + type: 'boolean', + description: `Whether the provided document has any useful information related to the user's query.`, + }, + summary: { + type: 'string', + description: `The condensed version of the document that can be used to answer the question. Can be empty.`, + }, + }, + required: ['useful'], +} as const satisfies ToolSchema; + +interface SummarizeDocumentResponse { + summary: string; +} + +export const summarizeDocument = async ({ + searchTerm, + documentContent, + connectorId, + outputAPI, + functionCalling, +}: { + searchTerm: string; + documentContent: string; + outputAPI: OutputAPI; + connectorId: string; + functionCalling?: FunctionCallingMode; +}): Promise => { + const result = await outputAPI({ + id: 'summarize_document', + connectorId, + functionCalling, + system: `You are an helpful Elastic assistant, and your current task is to help answer the user's question. + + Given a question and a document, please provide a condensed version of the document that can be used to answer the question. + - Limit the length of the output to 500 words. + - Try to include all relevant information that could be used to answer the question. If this + can't be done within the 500 words limit, then only include the most relevant information related to the question. + - If you think the document isn't relevant at all to answer the question, just return an empty text`, + input: ` + ## User question + + ${searchTerm} + + ## Document + + ${documentContent} + `, + schema: summarizeDocumentSchema, + }); + return { + summary: result.output.summary ?? '', + }; +}; diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts new file mode 100644 index 0000000000000..1e0637fcd344c --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/tasks/retrieve_documentation/types.ts @@ -0,0 +1,72 @@ +/* + * 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 { KibanaRequest } from '@kbn/core/server'; +import type { FunctionCallingMode } from '@kbn/inference-common'; +import type { ProductName } from '@kbn/product-doc-common'; + +/** + * Parameters for {@link RetrieveDocumentationAPI} + */ +export interface RetrieveDocumentationParams { + /** + * The search term to perform semantic text with. + * E.g. "What is Kibana Lens?" + */ + searchTerm: string; + /** + * Maximum number of documents to return. + * Defaults to 3. + */ + max?: number; + /** + * Optional list of products to restrict the search to. + */ + products?: ProductName[]; + /** + * The maximum number of tokens to return *per document*. + * Documents exceeding this limit will go through token reduction. + * + * Defaults to `1000`. + */ + maxDocumentTokens?: number; + /** + * The token reduction strategy to apply for documents exceeding max token count. + * - truncate: Will keep the N first tokens + * - summarize: Will call the LLM asking to generate a contextualized summary of the document + * + * Overall, `summarize` is way more efficient, but significantly slower, given that an additional + * LLM call will be performed. + * + * Defaults to `summarize` + */ + tokenReductionStrategy?: 'truncate' | 'summarize'; + /** + * The request that initiated the task. + */ + request: KibanaRequest; + /** + * Id of the LLM connector to use for the task. + */ + connectorId: string; + functionCalling?: FunctionCallingMode; +} + +export interface RetrievedDocument { + title: string; + url: string; + content: string; +} + +export interface RetrieveDocumentationResult { + success: boolean; + documents: RetrievedDocument[]; +} + +export type RetrieveDocumentationAPI = ( + options: RetrieveDocumentationParams +) => Promise; diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/types.ts b/x-pack/plugins/ai_infra/llm_tasks/server/types.ts new file mode 100644 index 0000000000000..d550e4398b509 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/types.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 type { InferenceServerStart } from '@kbn/inference-plugin/server'; +import type { ProductDocBaseStartContract } from '@kbn/product-doc-base-plugin/server'; +import type { RetrieveDocumentationAPI } from './tasks/retrieve_documentation'; + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface PluginSetupDependencies {} + +export interface PluginStartDependencies { + inference: InferenceServerStart; + productDocBase: ProductDocBaseStartContract; +} + +/** + * Describes public llmTasks plugin contract returned at the `setup` stage. + */ +export interface LlmTasksPluginSetup {} + +/** + * Describes public llmTasks plugin contract returned at the `start` stage. + */ +export interface LlmTasksPluginStart { + /** + * Checks if all prerequisites to use the `retrieveDocumentation` task + * are respected. Can be used to check if the task can be registered + * as LLM tool for example. + */ + retrieveDocumentationAvailable: () => Promise; + /** + * Perform the `retrieveDocumentation` task. + * + * @see RetrieveDocumentationAPI + */ + retrieveDocumentation: RetrieveDocumentationAPI; +} diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.test.ts b/x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.test.ts new file mode 100644 index 0000000000000..dce97eaea9b75 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { count, truncate } from './tokens'; + +describe('count', () => { + it('returns the token count of a given text', () => { + expect(count('some short sentence')).toBeGreaterThan(1); + }); +}); + +describe('truncate', () => { + it('truncates text that exceed the specified maximum token count', () => { + const text = 'some sentence that is likely longer than 5 tokens.'; + const output = truncate(text, 5); + expect(output.length).toBeLessThan(text.length); + }); + it('keeps text with a smaller amount of tokens unchanged', () => { + const text = 'some sentence that is likely less than 100 tokens.'; + const output = truncate(text, 100); + expect(output.length).toEqual(text.length); + }); +}); diff --git a/x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.ts b/x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.ts new file mode 100644 index 0000000000000..cb469144255b7 --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/server/utils/tokens.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 { encode, decode } from 'gpt-tokenizer'; + +export const count = (text: string): number => { + return encode(text).length; +}; + +export const truncate = (text: string, maxTokens: number): string => { + const encoded = encode(text); + if (encoded.length > maxTokens) { + const truncated = encoded.slice(0, maxTokens); + return decode(truncated); + } + return text; +}; diff --git a/x-pack/plugins/ai_infra/llm_tasks/tsconfig.json b/x-pack/plugins/ai_infra/llm_tasks/tsconfig.json new file mode 100644 index 0000000000000..03b87827d941a --- /dev/null +++ b/x-pack/plugins/ai_infra/llm_tasks/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*", + "scripts/**/*", + ".storybook/**/*" + ], + "exclude": ["target/**/*", ".storybook/**/*.js"], + "kbn_references": [ + "@kbn/core", + "@kbn/logging", + "@kbn/config-schema", + "@kbn/product-doc-common", + "@kbn/inference-plugin", + "@kbn/product-doc-base-plugin", + "@kbn/logging-mocks", + "@kbn/inference-common", + ] +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/README.md b/x-pack/plugins/ai_infra/product_doc_base/README.md new file mode 100644 index 0000000000000..0ff6c34dd2785 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/README.md @@ -0,0 +1,3 @@ +# Product documentation base plugin + +This plugin contains the product documentation base service. diff --git a/x-pack/plugins/ai_infra/product_doc_base/common/consts.ts b/x-pack/plugins/ai_infra/product_doc_base/common/consts.ts new file mode 100644 index 0000000000000..1622df5ed865c --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/common/consts.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const productDocInstallStatusSavedObjectTypeName = 'product-doc-install-status'; + +/** + * The id of the inference endpoint we're creating for our product doc indices. + * Could be replaced with the default elser 2 endpoint once the default endpoint feature is available. + */ +export const internalElserInferenceId = 'kibana-internal-elser2'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/common/http_api/installation.ts b/x-pack/plugins/ai_infra/product_doc_base/common/http_api/installation.ts new file mode 100644 index 0000000000000..0237bd2c3b488 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/common/http_api/installation.ts @@ -0,0 +1,26 @@ +/* + * 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 { ProductName } from '@kbn/product-doc-common'; +import type { ProductInstallState, InstallationStatus } from '../install_status'; + +export const INSTALLATION_STATUS_API_PATH = '/internal/product_doc_base/status'; +export const INSTALL_ALL_API_PATH = '/internal/product_doc_base/install'; +export const UNINSTALL_ALL_API_PATH = '/internal/product_doc_base/uninstall'; + +export interface InstallationStatusResponse { + overall: InstallationStatus; + perProducts: Record; +} + +export interface PerformInstallResponse { + installed: boolean; +} + +export interface UninstallResponse { + success: boolean; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/common/install_status.ts b/x-pack/plugins/ai_infra/product_doc_base/common/install_status.ts new file mode 100644 index 0000000000000..81102d43c1ff3 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/common/install_status.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ProductName } from '@kbn/product-doc-common'; + +export type InstallationStatus = 'installed' | 'uninstalled' | 'installing' | 'error'; + +/** + * DTO representation of the product doc install status SO + */ +export interface ProductDocInstallStatus { + id: string; + productName: ProductName; + productVersion: string; + installationStatus: InstallationStatus; + lastInstallationDate: Date | undefined; + lastInstallationFailureReason: string | undefined; + indexName?: string; +} + +export interface ProductInstallState { + status: InstallationStatus; + version?: string; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/jest.config.js b/x-pack/plugins/ai_infra/product_doc_base/jest.config.js new file mode 100644 index 0000000000000..fc06be251a6f7 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/jest.config.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: [ + '/x-pack/plugins/ai_infra/product_doc_base/public', + '/x-pack/plugins/ai_infra/product_doc_base/server', + '/x-pack/plugins/ai_infra/product_doc_base/common', + ], + setupFiles: [], + collectCoverage: true, + collectCoverageFrom: [ + '/x-pack/plugins/ai_infra/product_doc_base/{public,server,common}/**/*.{js,ts,tsx}', + ], + + coverageReporters: ['html'], +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/kibana.jsonc b/x-pack/plugins/ai_infra/product_doc_base/kibana.jsonc new file mode 100644 index 0000000000000..268b4a70c9921 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/kibana.jsonc @@ -0,0 +1,15 @@ +{ + "type": "plugin", + "id": "@kbn/product-doc-base-plugin", + "owner": "@elastic/appex-ai-infra", + "plugin": { + "id": "productDocBase", + "server": true, + "browser": true, + "configPath": ["xpack", "productDocBase"], + "requiredPlugins": ["licensing", "taskManager"], + "requiredBundles": [], + "optionalPlugins": [], + "extraPublicDirs": [] + } +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/index.ts b/x-pack/plugins/ai_infra/product_doc_base/public/index.ts new file mode 100644 index 0000000000000..b5ccbf029a73e --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { ProductDocBasePlugin } from './plugin'; +import type { + ProductDocBasePluginSetup, + ProductDocBasePluginStart, + PluginSetupDependencies, + PluginStartDependencies, + PublicPluginConfig, +} from './types'; + +export type { ProductDocBasePluginSetup, ProductDocBasePluginStart }; + +export const plugin: PluginInitializer< + ProductDocBasePluginSetup, + ProductDocBasePluginStart, + PluginSetupDependencies, + PluginStartDependencies +> = (pluginInitializerContext: PluginInitializerContext) => + new ProductDocBasePlugin(pluginInitializerContext); diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/plugin.tsx b/x-pack/plugins/ai_infra/product_doc_base/public/plugin.tsx new file mode 100644 index 0000000000000..6f2c989b6e45d --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/plugin.tsx @@ -0,0 +1,51 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import type { Logger } from '@kbn/logging'; +import type { + PublicPluginConfig, + ProductDocBasePluginSetup, + ProductDocBasePluginStart, + PluginSetupDependencies, + PluginStartDependencies, +} from './types'; +import { InstallationService } from './services/installation'; + +export class ProductDocBasePlugin + implements + Plugin< + ProductDocBasePluginSetup, + ProductDocBasePluginStart, + PluginSetupDependencies, + PluginStartDependencies + > +{ + logger: Logger; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + pluginsSetup: PluginSetupDependencies + ): ProductDocBasePluginSetup { + return {}; + } + + start(coreStart: CoreStart, pluginsStart: PluginStartDependencies): ProductDocBasePluginStart { + const installationService = new InstallationService({ http: coreStart.http }); + + return { + installation: { + getStatus: () => installationService.getInstallationStatus(), + install: () => installationService.install(), + uninstall: () => installationService.uninstall(), + }, + }; + } +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/index.ts b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/index.ts new file mode 100644 index 0000000000000..2eee8613d77dc --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/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 { InstallationService } from './installation_service'; +export type { InstallationAPI } from './types'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts new file mode 100644 index 0000000000000..294aeb99e0fd8 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { InstallationService } from './installation_service'; +import { + INSTALLATION_STATUS_API_PATH, + INSTALL_ALL_API_PATH, + UNINSTALL_ALL_API_PATH, +} from '../../../common/http_api/installation'; + +describe('InstallationService', () => { + let http: ReturnType; + let service: InstallationService; + + beforeEach(() => { + http = httpServiceMock.createSetupContract(); + service = new InstallationService({ http }); + }); + + describe('#getInstallationStatus', () => { + it('calls the endpoint with the right parameters', async () => { + await service.getInstallationStatus(); + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith(INSTALLATION_STATUS_API_PATH); + }); + it('returns the value from the server', async () => { + const expected = { stubbed: true }; + http.get.mockResolvedValue(expected); + + const response = await service.getInstallationStatus(); + expect(response).toEqual(expected); + }); + }); + describe('#install', () => { + beforeEach(() => { + http.post.mockResolvedValue({ installed: true }); + }); + + it('calls the endpoint with the right parameters', async () => { + await service.install(); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith(INSTALL_ALL_API_PATH); + }); + it('returns the value from the server', async () => { + const expected = { installed: true }; + http.post.mockResolvedValue(expected); + + const response = await service.install(); + expect(response).toEqual(expected); + }); + it('throws when the server returns installed: false', async () => { + const expected = { installed: false }; + http.post.mockResolvedValue(expected); + + await expect(service.install()).rejects.toThrowErrorMatchingInlineSnapshot( + `"Installation did not complete successfully"` + ); + }); + }); + describe('#uninstall', () => { + it('calls the endpoint with the right parameters', async () => { + await service.uninstall(); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith(UNINSTALL_ALL_API_PATH); + }); + it('returns the value from the server', async () => { + const expected = { stubbed: true }; + http.post.mockResolvedValue(expected); + + const response = await service.uninstall(); + expect(response).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.ts b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.ts new file mode 100644 index 0000000000000..ff347f52cb531 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/installation_service.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core-http-browser'; +import { + INSTALLATION_STATUS_API_PATH, + INSTALL_ALL_API_PATH, + UNINSTALL_ALL_API_PATH, + InstallationStatusResponse, + PerformInstallResponse, + UninstallResponse, +} from '../../../common/http_api/installation'; + +export class InstallationService { + private readonly http: HttpSetup; + + constructor({ http }: { http: HttpSetup }) { + this.http = http; + } + + async getInstallationStatus(): Promise { + return await this.http.get(INSTALLATION_STATUS_API_PATH); + } + + async install(): Promise { + const response = await this.http.post(INSTALL_ALL_API_PATH); + if (!response.installed) { + throw new Error('Installation did not complete successfully'); + } + return response; + } + + async uninstall(): Promise { + return await this.http.post(UNINSTALL_ALL_API_PATH); + } +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/types.ts b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/types.ts new file mode 100644 index 0000000000000..5c01c84b24625 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/services/installation/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + InstallationStatusResponse, + PerformInstallResponse, + UninstallResponse, +} from '../../../common/http_api/installation'; + +export interface InstallationAPI { + getStatus(): Promise; + install(): Promise; + uninstall(): Promise; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/public/types.ts b/x-pack/plugins/ai_infra/product_doc_base/public/types.ts new file mode 100644 index 0000000000000..1d06b0e08fa23 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/public/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { InstallationAPI } from './services/installation'; + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface PublicPluginConfig {} + +export interface PluginSetupDependencies {} + +export interface PluginStartDependencies {} + +export interface ProductDocBasePluginSetup {} + +export interface ProductDocBasePluginStart { + installation: InstallationAPI; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/config.ts b/x-pack/plugins/ai_infra/product_doc_base/server/config.ts new file mode 100644 index 0000000000000..bd0892d582701 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/config.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, type TypeOf } from '@kbn/config-schema'; +import type { PluginConfigDescriptor } from '@kbn/core/server'; + +const configSchema = schema.object({ + artifactRepositoryUrl: schema.string({ + defaultValue: 'https://kibana-knowledge-base-artifacts.elastic.co', + }), +}); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: {}, +}; + +export type ProductDocBaseConfig = TypeOf; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/index.ts new file mode 100644 index 0000000000000..805a0f2ea8c41 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginInitializer, PluginInitializerContext } from '@kbn/core/server'; +import type { ProductDocBaseConfig } from './config'; +import type { + ProductDocBaseSetupContract, + ProductDocBaseStartContract, + ProductDocBaseSetupDependencies, + ProductDocBaseStartDependencies, +} from './types'; +import { ProductDocBasePlugin } from './plugin'; + +export { config } from './config'; + +export type { ProductDocBaseSetupContract, ProductDocBaseStartContract }; +export type { SearchApi as ProductDocSearchAPI } from './services/search/types'; + +export const plugin: PluginInitializer< + ProductDocBaseSetupContract, + ProductDocBaseStartContract, + ProductDocBaseSetupDependencies, + ProductDocBaseStartDependencies +> = async (pluginInitializerContext: PluginInitializerContext) => + new ProductDocBasePlugin(pluginInitializerContext); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/plugin.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/plugin.test.ts new file mode 100644 index 0000000000000..bd5d6a720dd71 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/plugin.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { coreMock } from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { productDocInstallStatusSavedObjectTypeName } from '../common/consts'; +import { ProductDocBasePlugin } from './plugin'; +import { ProductDocBaseSetupDependencies, ProductDocBaseStartDependencies } from './types'; + +jest.mock('./services/package_installer'); +jest.mock('./services/search'); +jest.mock('./services/doc_install_status'); +jest.mock('./routes'); +jest.mock('./tasks'); +import { registerRoutes } from './routes'; +import { PackageInstaller } from './services/package_installer'; +import { registerTaskDefinitions, scheduleEnsureUpToDateTask } from './tasks'; + +const PackageInstallMock = PackageInstaller as jest.Mock; + +describe('ProductDocBasePlugin', () => { + let initContext: ReturnType; + let plugin: ProductDocBasePlugin; + let pluginSetupDeps: ProductDocBaseSetupDependencies; + let pluginStartDeps: ProductDocBaseStartDependencies; + + beforeEach(() => { + initContext = coreMock.createPluginInitializerContext(); + plugin = new ProductDocBasePlugin(initContext); + pluginSetupDeps = { + taskManager: taskManagerMock.createSetup(), + }; + pluginStartDeps = { + licensing: licensingMock.createStart(), + taskManager: taskManagerMock.createStart(), + }; + + PackageInstallMock.mockReturnValue({ ensureUpToDate: jest.fn().mockResolvedValue({}) }); + }); + + afterEach(() => { + (scheduleEnsureUpToDateTask as jest.Mock).mockReset(); + }); + + describe('#setup', () => { + it('register the routes', () => { + plugin.setup(coreMock.createSetup(), pluginSetupDeps); + + expect(registerRoutes).toHaveBeenCalledTimes(1); + }); + it('register the product-doc SO type', () => { + const coreSetup = coreMock.createSetup(); + plugin.setup(coreSetup, pluginSetupDeps); + + expect(coreSetup.savedObjects.registerType).toHaveBeenCalledTimes(1); + expect(coreSetup.savedObjects.registerType).toHaveBeenCalledWith( + expect.objectContaining({ + name: productDocInstallStatusSavedObjectTypeName, + }) + ); + }); + it('register the task definitions', () => { + plugin.setup(coreMock.createSetup(), pluginSetupDeps); + + expect(registerTaskDefinitions).toHaveBeenCalledTimes(3); + }); + }); + + describe('#start', () => { + it('returns a contract with the expected shape', () => { + plugin.setup(coreMock.createSetup(), pluginSetupDeps); + const startContract = plugin.start(coreMock.createStart(), pluginStartDeps); + expect(startContract).toEqual({ + management: { + getStatus: expect.any(Function), + install: expect.any(Function), + uninstall: expect.any(Function), + update: expect.any(Function), + }, + search: expect.any(Function), + }); + }); + + it('schedules the update task', () => { + plugin.setup(coreMock.createSetup(), pluginSetupDeps); + plugin.start(coreMock.createStart(), pluginStartDeps); + + expect(scheduleEnsureUpToDateTask).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/plugin.ts b/x-pack/plugins/ai_infra/product_doc_base/server/plugin.ts new file mode 100644 index 0000000000000..c8ed100cabb16 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/plugin.ts @@ -0,0 +1,133 @@ +/* + * 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 Path from 'path'; +import type { Logger } from '@kbn/logging'; +import { getDataPath } from '@kbn/utils'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import { SavedObjectsClient } from '@kbn/core/server'; +import { productDocInstallStatusSavedObjectTypeName } from '../common/consts'; +import type { ProductDocBaseConfig } from './config'; +import { + ProductDocBaseSetupContract, + ProductDocBaseStartContract, + ProductDocBaseSetupDependencies, + ProductDocBaseStartDependencies, + InternalServices, +} from './types'; +import { productDocInstallStatusSavedObjectType } from './saved_objects'; +import { PackageInstaller } from './services/package_installer'; +import { InferenceEndpointManager } from './services/inference_endpoint'; +import { ProductDocInstallClient } from './services/doc_install_status'; +import { DocumentationManager } from './services/doc_manager'; +import { SearchService } from './services/search'; +import { registerRoutes } from './routes'; +import { registerTaskDefinitions } from './tasks'; + +export class ProductDocBasePlugin + implements + Plugin< + ProductDocBaseSetupContract, + ProductDocBaseStartContract, + ProductDocBaseSetupDependencies, + ProductDocBaseStartDependencies + > +{ + private logger: Logger; + private internalServices?: InternalServices; + + constructor(private readonly context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup( + coreSetup: CoreSetup, + { taskManager }: ProductDocBaseSetupDependencies + ): ProductDocBaseSetupContract { + const getServices = () => { + if (!this.internalServices) { + throw new Error('getServices called before #start'); + } + return this.internalServices; + }; + + coreSetup.savedObjects.registerType(productDocInstallStatusSavedObjectType); + + registerTaskDefinitions({ + taskManager, + getServices, + }); + + const router = coreSetup.http.createRouter(); + registerRoutes({ + router, + getServices, + }); + + return {}; + } + + start( + core: CoreStart, + { licensing, taskManager }: ProductDocBaseStartDependencies + ): ProductDocBaseStartContract { + const soClient = new SavedObjectsClient( + core.savedObjects.createInternalRepository([productDocInstallStatusSavedObjectTypeName]) + ); + const productDocClient = new ProductDocInstallClient({ soClient }); + + const endpointManager = new InferenceEndpointManager({ + esClient: core.elasticsearch.client.asInternalUser, + logger: this.logger.get('endpoint-manager'), + }); + + const packageInstaller = new PackageInstaller({ + esClient: core.elasticsearch.client.asInternalUser, + productDocClient, + endpointManager, + kibanaVersion: this.context.env.packageInfo.version, + artifactsFolder: Path.join(getDataPath(), 'ai-kb-artifacts'), + artifactRepositoryUrl: this.context.config.get().artifactRepositoryUrl, + logger: this.logger.get('package-installer'), + }); + + const searchService = new SearchService({ + esClient: core.elasticsearch.client.asInternalUser, + logger: this.logger.get('search-service'), + }); + + const documentationManager = new DocumentationManager({ + logger: this.logger.get('doc-manager'), + docInstallClient: productDocClient, + licensing, + taskManager, + auditService: core.security.audit, + }); + + this.internalServices = { + logger: this.logger, + packageInstaller, + installClient: productDocClient, + documentationManager, + licensing, + taskManager, + }; + + documentationManager.update().catch((err) => { + this.logger.error(`Error scheduling product documentation update task: ${err.message}`); + }); + + return { + management: { + install: documentationManager.install.bind(documentationManager), + update: documentationManager.update.bind(documentationManager), + uninstall: documentationManager.uninstall.bind(documentationManager), + getStatus: documentationManager.getStatus.bind(documentationManager), + }, + search: searchService.search.bind(searchService), + }; + } +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/routes/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/routes/index.ts new file mode 100644 index 0000000000000..66660c199d819 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/routes/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { registerInstallationRoutes } from './installation'; +import type { InternalServices } from '../types'; + +export const registerRoutes = ({ + router, + getServices, +}: { + router: IRouter; + getServices: () => InternalServices; +}) => { + registerInstallationRoutes({ getServices, router }); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts b/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts new file mode 100644 index 0000000000000..dbede9f7d94d3 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/routes/installation.ts @@ -0,0 +1,115 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { + INSTALLATION_STATUS_API_PATH, + INSTALL_ALL_API_PATH, + UNINSTALL_ALL_API_PATH, + InstallationStatusResponse, + PerformInstallResponse, + UninstallResponse, +} from '../../common/http_api/installation'; +import type { InternalServices } from '../types'; + +export const registerInstallationRoutes = ({ + router, + getServices, +}: { + router: IRouter; + getServices: () => InternalServices; +}) => { + router.get( + { + path: INSTALLATION_STATUS_API_PATH, + validate: false, + options: { + access: 'internal', + security: { + authz: { + requiredPrivileges: ['manage_llm_product_doc'], + }, + }, + }, + }, + async (ctx, req, res) => { + const { installClient, documentationManager } = getServices(); + const installStatus = await installClient.getInstallationStatus(); + const { status: overallStatus } = await documentationManager.getStatus(); + + return res.ok({ + body: { + perProducts: installStatus, + overall: overallStatus, + }, + }); + } + ); + + router.post( + { + path: INSTALL_ALL_API_PATH, + validate: false, + options: { + access: 'internal', + security: { + authz: { + requiredPrivileges: ['manage_llm_product_doc'], + }, + }, + timeout: { idleSocket: 20 * 60 * 1000 }, // install can take time. + }, + }, + async (ctx, req, res) => { + const { documentationManager } = getServices(); + + await documentationManager.install({ + request: req, + force: false, + wait: true, + }); + + // check status after installation in case of failure + const { status } = await documentationManager.getStatus(); + + return res.ok({ + body: { + installed: status === 'installed', + }, + }); + } + ); + + router.post( + { + path: UNINSTALL_ALL_API_PATH, + validate: false, + options: { + access: 'internal', + security: { + authz: { + requiredPrivileges: ['manage_llm_product_doc'], + }, + }, + }, + }, + async (ctx, req, res) => { + const { documentationManager } = getServices(); + + await documentationManager.uninstall({ + request: req, + wait: true, + }); + + return res.ok({ + body: { + success: true, + }, + }); + } + ); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/index.ts new file mode 100644 index 0000000000000..f87c6d37eb66f --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { + productDocInstallStatusSavedObjectType, + type ProductDocInstallStatusAttributes, +} from './product_doc_install'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts b/x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts new file mode 100644 index 0000000000000..47cf7eb50cdd1 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/saved_objects/product_doc_install.ts @@ -0,0 +1,46 @@ +/* + * 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 { SavedObjectsType } from '@kbn/core/server'; +import type { ProductName } from '@kbn/product-doc-common'; +import { productDocInstallStatusSavedObjectTypeName } from '../../common/consts'; +import type { InstallationStatus } from '../../common/install_status'; + +/** + * Interface describing the raw attributes of the product doc install SO type. + * Contains more fields than the mappings, which only list + * indexed fields. + */ +export interface ProductDocInstallStatusAttributes { + product_name: ProductName; + product_version: string; + installation_status: InstallationStatus; + last_installation_date?: number; + last_installation_failure_reason?: string; + index_name?: string; +} + +export const productDocInstallStatusSavedObjectType: SavedObjectsType = + { + name: productDocInstallStatusSavedObjectTypeName, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: { + product_name: { type: 'keyword' }, + product_version: { type: 'keyword' }, + installation_status: { type: 'keyword' }, + last_installation_date: { type: 'date' }, + index_name: { type: 'keyword' }, + }, + }, + management: { + importableAndExportable: false, + }, + modelVersions: {}, + }; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/index.ts new file mode 100644 index 0000000000000..d55cb303b1908 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { ProductDocInstallClient } from './product_doc_install_service'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.test.ts new file mode 100644 index 0000000000000..6460d8452dc2b --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { SavedObject } from '@kbn/core/server'; +import type { ProductDocInstallStatusAttributes } from '../../saved_objects'; +import { soToModel } from './model_conversion'; + +const createObj = ( + attrs: ProductDocInstallStatusAttributes +): SavedObject => { + return { + id: 'some-id', + type: 'product-doc-install-status', + attributes: attrs, + references: [], + }; +}; + +describe('soToModel', () => { + it('converts the SO to the expected shape', () => { + const input = createObj({ + product_name: 'kibana', + product_version: '8.16', + installation_status: 'installed', + last_installation_date: 9000, + index_name: '.kibana', + }); + + const output = soToModel(input); + + expect(output).toEqual({ + id: 'some-id', + productName: 'kibana', + productVersion: '8.16', + indexName: '.kibana', + installationStatus: 'installed', + lastInstallationDate: expect.any(Date), + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.ts new file mode 100644 index 0000000000000..cf77bb9222a15 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/model_conversion.ts @@ -0,0 +1,26 @@ +/* + * 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 { SavedObject } from '@kbn/core/server'; +import type { ProductDocInstallStatus } from '../../../common/install_status'; +import type { ProductDocInstallStatusAttributes } from '../../saved_objects'; + +export const soToModel = ( + so: SavedObject +): ProductDocInstallStatus => { + return { + id: so.id, + productName: so.attributes.product_name, + productVersion: so.attributes.product_version, + installationStatus: so.attributes.installation_status, + indexName: so.attributes.index_name, + lastInstallationDate: so.attributes.last_installation_date + ? new Date(so.attributes.last_installation_date) + : undefined, + lastInstallationFailureReason: so.attributes.last_installation_failure_reason, + }; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.test.ts new file mode 100644 index 0000000000000..81249038a1294 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.test.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 type { SavedObjectsFindResult } from '@kbn/core/server'; +import { DocumentationProduct } from '@kbn/product-doc-common'; +import type { ProductDocInstallStatusAttributes as TypeAttributes } from '../../saved_objects'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { ProductDocInstallClient } from './product_doc_install_service'; + +const createObj = (attrs: TypeAttributes): SavedObjectsFindResult => { + return { + id: attrs.product_name, + type: 'type', + references: [], + attributes: attrs, + score: 42, + }; +}; + +describe('ProductDocInstallClient', () => { + let soClient: ReturnType; + let service: ProductDocInstallClient; + + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + service = new ProductDocInstallClient({ soClient }); + }); + + describe('getInstallationStatus', () => { + it('returns the installation status based on existing entries', async () => { + soClient.find.mockResolvedValue({ + saved_objects: [ + createObj({ + product_name: 'kibana', + product_version: '8.15', + installation_status: 'installed', + }), + createObj({ + product_name: 'elasticsearch', + product_version: '8.15', + installation_status: 'installing', + }), + ], + total: 2, + per_page: 100, + page: 1, + }); + + const installStatus = await service.getInstallationStatus(); + + expect(Object.keys(installStatus).sort()).toEqual(Object.keys(DocumentationProduct).sort()); + expect(installStatus.kibana).toEqual({ + status: 'installed', + version: '8.15', + }); + expect(installStatus.security).toEqual({ + status: 'uninstalled', + }); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.ts new file mode 100644 index 0000000000000..24625ebc51586 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/product_doc_install_service.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; +import { ProductName, DocumentationProduct } from '@kbn/product-doc-common'; +import type { ProductInstallState } from '../../../common/install_status'; +import { productDocInstallStatusSavedObjectTypeName as typeName } from '../../../common/consts'; +import type { ProductDocInstallStatusAttributes as TypeAttributes } from '../../saved_objects'; + +export class ProductDocInstallClient { + private soClient: SavedObjectsClientContract; + + constructor({ soClient }: { soClient: SavedObjectsClientContract }) { + this.soClient = soClient; + } + + async getInstallationStatus(): Promise> { + const response = await this.soClient.find({ + type: typeName, + perPage: 100, + }); + + const installStatus = Object.values(DocumentationProduct).reduce((memo, product) => { + memo[product] = { status: 'uninstalled' }; + return memo; + }, {} as Record); + + response.saved_objects.forEach(({ attributes }) => { + installStatus[attributes.product_name as ProductName] = { + status: attributes.installation_status, + version: attributes.product_version, + }; + }); + + return installStatus; + } + + async setInstallationStarted(fields: { productName: ProductName; productVersion: string }) { + const { productName, productVersion } = fields; + const objectId = getObjectIdFromProductName(productName); + const attributes = { + product_name: productName, + product_version: productVersion, + installation_status: 'installing' as const, + last_installation_failure_reason: '', + }; + await this.soClient.update(typeName, objectId, attributes, { + upsert: attributes, + }); + } + + async setInstallationSuccessful(productName: ProductName, indexName: string) { + const objectId = getObjectIdFromProductName(productName); + await this.soClient.update(typeName, objectId, { + installation_status: 'installed', + index_name: indexName, + }); + } + + async setInstallationFailed(productName: ProductName, failureReason: string) { + const objectId = getObjectIdFromProductName(productName); + await this.soClient.update(typeName, objectId, { + installation_status: 'error', + last_installation_failure_reason: failureReason, + }); + } + + async setUninstalled(productName: ProductName) { + const objectId = getObjectIdFromProductName(productName); + try { + await this.soClient.update(typeName, objectId, { + installation_status: 'uninstalled', + last_installation_failure_reason: '', + }); + } catch (e) { + if (!SavedObjectsErrorHelpers.isNotFoundError(e)) { + throw e; + } + } + } +} + +const getObjectIdFromProductName = (productName: ProductName) => + `kb-product-doc-${productName}-status`.toLowerCase(); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/service.mock.ts new file mode 100644 index 0000000000000..c2a0adbac9f29 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_install_status/service.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ProductDocInstallClient } from './product_doc_install_service'; + +export type InstallClientMock = jest.Mocked; + +const createInstallClientMock = (): InstallClientMock => { + return { + getInstallationStatus: jest.fn(), + setInstallationStarted: jest.fn(), + setInstallationSuccessful: jest.fn(), + setInstallationFailed: jest.fn(), + setUninstalled: jest.fn(), + } as unknown as InstallClientMock; +}; + +export const installClientMock = { + create: createInstallClientMock, +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/check_license.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/check_license.ts new file mode 100644 index 0000000000000..d4af5b7ebdb22 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/check_license.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ILicense } from '@kbn/licensing-plugin/server'; + +export const checkLicense = (license: ILicense): boolean => { + const result = license.check('elastic documentation', 'enterprise'); + return result.state === 'valid'; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts new file mode 100644 index 0000000000000..0be913ee6dd71 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts @@ -0,0 +1,247 @@ +/* + * 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 { loggerMock, MockedLogger } from '@kbn/logging-mocks'; +import { securityServiceMock, httpServerMock } from '@kbn/core/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import type { ProductDocInstallClient } from '../doc_install_status'; +import { DocumentationManager } from './doc_manager'; + +jest.mock('../../tasks'); +import { + scheduleInstallAllTask, + scheduleUninstallAllTask, + scheduleEnsureUpToDateTask, + getTaskStatus, + waitUntilTaskCompleted, +} from '../../tasks'; + +const scheduleInstallAllTaskMock = scheduleInstallAllTask as jest.MockedFn< + typeof scheduleInstallAllTask +>; +const scheduleUninstallAllTaskMock = scheduleUninstallAllTask as jest.MockedFn< + typeof scheduleUninstallAllTask +>; +const scheduleEnsureUpToDateTaskMock = scheduleEnsureUpToDateTask as jest.MockedFn< + typeof scheduleEnsureUpToDateTask +>; +const waitUntilTaskCompletedMock = waitUntilTaskCompleted as jest.MockedFn< + typeof waitUntilTaskCompleted +>; +const getTaskStatusMock = getTaskStatus as jest.MockedFn; + +describe('DocumentationManager', () => { + let logger: MockedLogger; + let taskManager: ReturnType; + let licensing: ReturnType; + let auditService: ReturnType['audit']; + let docInstallClient: jest.Mocked; + + let docManager: DocumentationManager; + + beforeEach(() => { + logger = loggerMock.create(); + taskManager = taskManagerMock.createStart(); + licensing = licensingMock.createStart(); + auditService = securityServiceMock.createStart().audit; + + docInstallClient = { + getInstallationStatus: jest.fn(), + } as unknown as jest.Mocked; + + docManager = new DocumentationManager({ + logger, + taskManager, + licensing, + auditService, + docInstallClient, + }); + }); + + afterEach(() => { + scheduleInstallAllTaskMock.mockReset(); + scheduleUninstallAllTaskMock.mockReset(); + scheduleEnsureUpToDateTaskMock.mockReset(); + waitUntilTaskCompletedMock.mockReset(); + getTaskStatusMock.mockReset(); + }); + + describe('#install', () => { + beforeEach(() => { + licensing.getLicense.mockResolvedValue( + licensingMock.createLicense({ license: { type: 'enterprise' } }) + ); + + getTaskStatusMock.mockResolvedValue('not_scheduled'); + + docInstallClient.getInstallationStatus.mockResolvedValue({ + kibana: { status: 'uninstalled' }, + } as Awaited>); + }); + + it('calls `scheduleInstallAllTask`', async () => { + await docManager.install({}); + + expect(scheduleInstallAllTaskMock).toHaveBeenCalledTimes(1); + expect(scheduleInstallAllTaskMock).toHaveBeenCalledWith({ + taskManager, + logger, + }); + + expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); + }); + + it('calls waitUntilTaskCompleted if wait=true', async () => { + await docManager.install({ wait: true }); + + expect(scheduleInstallAllTaskMock).toHaveBeenCalledTimes(1); + expect(waitUntilTaskCompletedMock).toHaveBeenCalledTimes(1); + }); + + it('does not call scheduleInstallAllTask if already installed and not force', async () => { + docInstallClient.getInstallationStatus.mockResolvedValue({ + kibana: { status: 'installed' }, + } as Awaited>); + + await docManager.install({ wait: true }); + + expect(scheduleInstallAllTaskMock).not.toHaveBeenCalled(); + expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); + }); + + it('records an audit log when request is provided', async () => { + const request = httpServerMock.createKibanaRequest(); + + const auditLog = auditService.withoutRequest; + auditService.asScoped = jest.fn(() => auditLog); + + await docManager.install({ force: false, wait: false, request }); + + expect(auditLog.log).toHaveBeenCalledTimes(1); + expect(auditLog.log).toHaveBeenCalledWith({ + message: expect.any(String), + event: { + action: 'product_documentation_create', + category: ['database'], + type: ['creation'], + outcome: 'unknown', + }, + }); + }); + + it('throws an error if license level is not sufficient', async () => { + licensing.getLicense.mockResolvedValue( + licensingMock.createLicense({ license: { type: 'basic' } }) + ); + + await expect( + docManager.install({ force: false, wait: false }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Elastic documentation requires an enterprise license"` + ); + }); + }); + + describe('#update', () => { + beforeEach(() => { + getTaskStatusMock.mockResolvedValue('not_scheduled'); + + docInstallClient.getInstallationStatus.mockResolvedValue({ + kibana: { status: 'uninstalled' }, + } as Awaited>); + }); + + it('calls `scheduleEnsureUpToDateTask`', async () => { + await docManager.update({}); + + expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledTimes(1); + expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledWith({ + taskManager, + logger, + }); + + expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); + }); + + it('calls waitUntilTaskCompleted if wait=true', async () => { + await docManager.update({ wait: true }); + + expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledTimes(1); + expect(waitUntilTaskCompletedMock).toHaveBeenCalledTimes(1); + }); + + it('records an audit log when request is provided', async () => { + const request = httpServerMock.createKibanaRequest(); + + const auditLog = auditService.withoutRequest; + auditService.asScoped = jest.fn(() => auditLog); + + await docManager.update({ wait: false, request }); + + expect(auditLog.log).toHaveBeenCalledTimes(1); + expect(auditLog.log).toHaveBeenCalledWith({ + message: expect.any(String), + event: { + action: 'product_documentation_update', + category: ['database'], + type: ['change'], + outcome: 'unknown', + }, + }); + }); + }); + + describe('#uninstall', () => { + beforeEach(() => { + getTaskStatusMock.mockResolvedValue('not_scheduled'); + + docInstallClient.getInstallationStatus.mockResolvedValue({ + kibana: { status: 'uninstalled' }, + } as Awaited>); + }); + + it('calls `scheduleUninstallAllTask`', async () => { + await docManager.uninstall({}); + + expect(scheduleUninstallAllTaskMock).toHaveBeenCalledTimes(1); + expect(scheduleUninstallAllTaskMock).toHaveBeenCalledWith({ + taskManager, + logger, + }); + + expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled(); + }); + + it('calls waitUntilTaskCompleted if wait=true', async () => { + await docManager.uninstall({ wait: true }); + + expect(scheduleUninstallAllTaskMock).toHaveBeenCalledTimes(1); + expect(waitUntilTaskCompletedMock).toHaveBeenCalledTimes(1); + }); + + it('records an audit log when request is provided', async () => { + const request = httpServerMock.createKibanaRequest(); + + const auditLog = auditService.withoutRequest; + auditService.asScoped = jest.fn(() => auditLog); + + await docManager.uninstall({ wait: false, request }); + + expect(auditLog.log).toHaveBeenCalledTimes(1); + expect(auditLog.log).toHaveBeenCalledWith({ + message: expect.any(String), + event: { + action: 'product_documentation_delete', + category: ['database'], + type: ['deletion'], + outcome: 'unknown', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts new file mode 100644 index 0000000000000..40dc53e19ceea --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.ts @@ -0,0 +1,204 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import type { CoreAuditService } from '@kbn/core/server'; +import { type TaskManagerStartContract, TaskStatus } from '@kbn/task-manager-plugin/server'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import type { InstallationStatus } from '../../../common/install_status'; +import type { ProductDocInstallClient } from '../doc_install_status'; +import { + INSTALL_ALL_TASK_ID, + scheduleInstallAllTask, + scheduleUninstallAllTask, + scheduleEnsureUpToDateTask, + getTaskStatus, + waitUntilTaskCompleted, +} from '../../tasks'; +import { checkLicense } from './check_license'; +import type { + DocumentationManagerAPI, + DocGetStatusResponse, + DocInstallOptions, + DocUninstallOptions, + DocUpdateOptions, +} from './types'; + +const TEN_MIN_IN_MS = 10 * 60 * 1000; + +/** + * High-level installation service, handling product documentation + * installation as unary operations, abstracting away the fact + * that documentation is composed of multiple entities. + */ +export class DocumentationManager implements DocumentationManagerAPI { + private logger: Logger; + private taskManager: TaskManagerStartContract; + private licensing: LicensingPluginStart; + private docInstallClient: ProductDocInstallClient; + private auditService: CoreAuditService; + + constructor({ + logger, + taskManager, + licensing, + docInstallClient, + auditService, + }: { + logger: Logger; + taskManager: TaskManagerStartContract; + licensing: LicensingPluginStart; + docInstallClient: ProductDocInstallClient; + auditService: CoreAuditService; + }) { + this.logger = logger; + this.taskManager = taskManager; + this.licensing = licensing; + this.docInstallClient = docInstallClient; + this.auditService = auditService; + } + + async install(options: DocInstallOptions = {}): Promise { + const { request, force = false, wait = false } = options; + + const { status } = await this.getStatus(); + if (!force && status === 'installed') { + return; + } + + const license = await this.licensing.getLicense(); + if (!checkLicense(license)) { + throw new Error('Elastic documentation requires an enterprise license'); + } + + const taskId = await scheduleInstallAllTask({ + taskManager: this.taskManager, + logger: this.logger, + }); + + if (request) { + this.auditService.asScoped(request).log({ + message: `User is requesting installation of product documentation for AI Assistants. Task ID=[${taskId}]`, + event: { + action: 'product_documentation_create', + category: ['database'], + type: ['creation'], + outcome: 'unknown', + }, + }); + } + + if (wait) { + await waitUntilTaskCompleted({ + taskManager: this.taskManager, + taskId, + timeout: TEN_MIN_IN_MS, + }); + } + } + + async update(options: DocUpdateOptions = {}): Promise { + const { request, wait = false } = options; + + const taskId = await scheduleEnsureUpToDateTask({ + taskManager: this.taskManager, + logger: this.logger, + }); + + if (request) { + this.auditService.asScoped(request).log({ + message: `User is requesting update of product documentation for AI Assistants. Task ID=[${taskId}]`, + event: { + action: 'product_documentation_update', + category: ['database'], + type: ['change'], + outcome: 'unknown', + }, + }); + } + + if (wait) { + await waitUntilTaskCompleted({ + taskManager: this.taskManager, + taskId, + timeout: TEN_MIN_IN_MS, + }); + } + } + + async uninstall(options: DocUninstallOptions = {}): Promise { + const { request, wait = false } = options; + + const taskId = await scheduleUninstallAllTask({ + taskManager: this.taskManager, + logger: this.logger, + }); + + if (request) { + this.auditService.asScoped(request).log({ + message: `User is requesting deletion of product documentation for AI Assistants. Task ID=[${taskId}]`, + event: { + action: 'product_documentation_delete', + category: ['database'], + type: ['deletion'], + outcome: 'unknown', + }, + }); + } + + if (wait) { + await waitUntilTaskCompleted({ + taskManager: this.taskManager, + taskId, + timeout: TEN_MIN_IN_MS, + }); + } + } + + async getStatus(): Promise { + const taskStatus = await getTaskStatus({ + taskManager: this.taskManager, + taskId: INSTALL_ALL_TASK_ID, + }); + if (taskStatus !== 'not_scheduled') { + const status = convertTaskStatus(taskStatus); + if (status !== 'unknown') { + return { status }; + } + } + + const installStatus = await this.docInstallClient.getInstallationStatus(); + const overallStatus = getOverallStatus(Object.values(installStatus).map((v) => v.status)); + return { status: overallStatus }; + } +} + +const convertTaskStatus = (taskStatus: TaskStatus): InstallationStatus | 'unknown' => { + switch (taskStatus) { + case TaskStatus.Idle: + case TaskStatus.Claiming: + case TaskStatus.Running: + return 'installing'; + case TaskStatus.Failed: + return 'error'; + case TaskStatus.Unrecognized: + case TaskStatus.DeadLetter: + case TaskStatus.ShouldDelete: + default: + return 'unknown'; + } +}; + +const getOverallStatus = (statuses: InstallationStatus[]): InstallationStatus => { + const statusOrder: InstallationStatus[] = ['error', 'installing', 'uninstalled', 'installed']; + for (const status of statusOrder) { + if (statuses.includes(status)) { + return status; + } + } + return 'installed'; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/index.ts new file mode 100644 index 0000000000000..588b5e2f5cc65 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { DocumentationManager } from './doc_manager'; +export type { + DocumentationManagerAPI, + DocUninstallOptions, + DocInstallOptions, + DocUpdateOptions, + DocGetStatusResponse, +} from './types'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/types.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/types.ts new file mode 100644 index 0000000000000..5a954a5ffb0fd --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/doc_manager/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest } from '@kbn/core/server'; +import type { InstallationStatus } from '../../../common/install_status'; + +/** + * APIs to manage the product documentation. + */ +export interface DocumentationManagerAPI { + /** + * Install the product documentation. + * By default, will only try to install if not already present. + * Can use the `force` option to forcefully reinstall. + */ + install(options?: DocInstallOptions): Promise; + /** + * Update the product documentation to the latest version. + * No-op if the product documentation is not currently installed. + */ + update(options?: DocUpdateOptions): Promise; + /** + * Uninstall the product documentation. + * No-op if the product documentation is not currently installed. + */ + uninstall(options?: DocUninstallOptions): Promise; + /** + * Returns the overall installation status of the documentation. + */ + getStatus(): Promise; +} + +/** + * Return type for {@link DocumentationManagerAPI.getStatus} + */ +export interface DocGetStatusResponse { + status: InstallationStatus; +} + +/** + * Options for {@link DocumentationManagerAPI.install} + */ +export interface DocInstallOptions { + /** + * When the operation was requested by a user, the request that initiated it. + * + * If not provided, the call will be considered as being done on behalf of system. + */ + request?: KibanaRequest; + /** + * If true, will reinstall the documentation even if already present. + * Defaults to `false` + */ + force?: boolean; + /** + * If true, the returned promise will wait until the update task has completed before resolving. + * Defaults to `false` + */ + wait?: boolean; +} + +/** + * Options for {@link DocumentationManagerAPI.uninstall} + */ +export interface DocUninstallOptions { + /** + * When the operation was requested by a user, the request that initiated it. + * + * If not provided, the call will be considered as being done on behalf of system. + */ + request?: KibanaRequest; + /** + * If true, the returned promise will wait until the update task has completed before resolving. + * Defaults to `false` + */ + wait?: boolean; +} + +/** + * Options for {@link DocumentationManagerAPI.update} + */ +export interface DocUpdateOptions { + /** + * When the operation was requested by a user, the request that initiated it. + * + * If not provided, the call will be considered as being done on behalf of system. + */ + request?: KibanaRequest; + /** + * If true, the returned promise will wait until the update task has completed before resolving. + * Defaults to `false` + */ + wait?: boolean; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.test.ts new file mode 100644 index 0000000000000..e5dabaaa9b7f7 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { InferenceEndpointManager } from './endpoint_manager'; + +jest.mock('./utils'); +import { installElser, getModelInstallStatus, waitUntilModelDeployed } from './utils'; +const installElserMock = installElser as jest.MockedFn; +const getModelInstallStatusMock = getModelInstallStatus as jest.MockedFn< + typeof getModelInstallStatus +>; +const waitUntilModelDeployedMock = waitUntilModelDeployed as jest.MockedFn< + typeof waitUntilModelDeployed +>; + +describe('InferenceEndpointManager', () => { + let logger: MockedLogger; + let esClient: ReturnType; + let endpointManager: InferenceEndpointManager; + + beforeEach(() => { + logger = loggerMock.create(); + esClient = elasticsearchServiceMock.createElasticsearchClient(); + + endpointManager = new InferenceEndpointManager({ esClient, logger }); + }); + + afterEach(() => { + installElserMock.mockReset(); + getModelInstallStatusMock.mockReset(); + waitUntilModelDeployedMock.mockReset(); + }); + + describe('#ensureInternalElserInstalled', () => { + it('installs ELSER if not already installed', async () => { + getModelInstallStatusMock.mockResolvedValue({ installed: true }); + + await endpointManager.ensureInternalElserInstalled(); + + expect(installElserMock).not.toHaveBeenCalled(); + expect(waitUntilModelDeployedMock).toHaveBeenCalledTimes(1); + }); + it('does not install ELSER if already present', async () => { + getModelInstallStatusMock.mockResolvedValue({ installed: false }); + + await endpointManager.ensureInternalElserInstalled(); + + expect(installElserMock).toHaveBeenCalledTimes(1); + expect(waitUntilModelDeployedMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.ts new file mode 100644 index 0000000000000..4f7467501d61d --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/endpoint_manager.ts @@ -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 { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { internalElserInferenceId } from '../../../common/consts'; +import { installElser, getModelInstallStatus, waitUntilModelDeployed } from './utils'; + +export class InferenceEndpointManager { + private readonly log: Logger; + private readonly esClient: ElasticsearchClient; + + constructor({ logger, esClient }: { logger: Logger; esClient: ElasticsearchClient }) { + this.log = logger; + this.esClient = esClient; + } + + async ensureInternalElserInstalled() { + const { installed } = await getModelInstallStatus({ + inferenceId: internalElserInferenceId, + client: this.esClient, + log: this.log, + }); + if (!installed) { + await installElser({ + inferenceId: internalElserInferenceId, + client: this.esClient, + log: this.log, + }); + } + + await waitUntilModelDeployed({ + modelId: internalElserInferenceId, + client: this.esClient, + log: this.log, + }); + } +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/index.ts new file mode 100644 index 0000000000000..e4098ff58fe51 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { InferenceEndpointManager } from './endpoint_manager'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/service.mock.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/service.mock.ts new file mode 100644 index 0000000000000..e9715c4ad2acd --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/service.mock.ts @@ -0,0 +1,20 @@ +/* + * 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 { InferenceEndpointManager } from './endpoint_manager'; + +export type InferenceEndpointManagerMock = jest.Mocked; + +const createMock = (): InferenceEndpointManagerMock => { + return { + ensureInternalElserInstalled: jest.fn(), + } as unknown as InferenceEndpointManagerMock; +}; + +export const inferenceManagerMock = { + create: createMock, +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/get_model_install_status.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/get_model_install_status.ts new file mode 100644 index 0000000000000..be6caa34d0ad1 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/get_model_install_status.ts @@ -0,0 +1,34 @@ +/* + * 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 { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; + +export const getModelInstallStatus = async ({ + inferenceId, + taskType = 'sparse_embedding', + client, +}: { + inferenceId: string; + taskType?: InferenceTaskType; + client: ElasticsearchClient; + log: Logger; +}) => { + const getInferenceRes = await client.inference.get( + { + task_type: taskType, + inference_id: inferenceId, + }, + { ignore: [404] } + ); + + const installed = (getInferenceRes.endpoints ?? []).some( + (endpoint) => endpoint.inference_id === inferenceId + ); + + return { installed }; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/index.ts new file mode 100644 index 0000000000000..089997557f301 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/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 { waitUntilModelDeployed } from './wait_until_model_deployed'; +export { getModelInstallStatus } from './get_model_install_status'; +export { installElser } from './install_elser'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/install_elser.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/install_elser.ts new file mode 100644 index 0000000000000..0e92d765a3d17 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/install_elser.ts @@ -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 { ElasticsearchClient, Logger } from '@kbn/core/server'; + +export const installElser = async ({ + inferenceId, + client, + log, +}: { + inferenceId: string; + client: ElasticsearchClient; + log: Logger; +}) => { + await client.inference.put( + { + task_type: 'sparse_embedding', + inference_id: inferenceId, + inference_config: { + service: 'elasticsearch', + service_settings: { + num_allocations: 1, + num_threads: 1, + model_id: '.elser_model_2', + }, + task_settings: {}, + }, + }, + { requestTimeout: 5 * 60 * 1000 } + ); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/wait_until_model_deployed.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/wait_until_model_deployed.ts new file mode 100644 index 0000000000000..accd9104e09b1 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/inference_endpoint/utils/wait_until_model_deployed.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; + +export const waitUntilModelDeployed = async ({ + modelId, + client, + log, + maxRetries = 20, + delay = 2000, +}: { + modelId: string; + client: ElasticsearchClient; + log: Logger; + maxRetries?: number; + delay?: number; +}) => { + for (let i = 0; i < maxRetries; i++) { + const statsRes = await client.ml.getTrainedModelsStats({ + model_id: modelId, + }); + const deploymentStats = statsRes.trained_model_stats[0]?.deployment_stats; + // @ts-expect-error wrong client types + if (!deploymentStats || deploymentStats.nodes.length === 0) { + log.debug(`ML model [${modelId}] was not deployed - attempt ${i + 1} of ${maxRetries}`); + await sleep(delay); + continue; + } + return; + } + + throw new Error(`Timeout waiting for ML model ${modelId} to be deployed`); +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/index.ts new file mode 100644 index 0000000000000..a9edb7c38fdaa --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { PackageInstaller } from './package_installer'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.mocks.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.mocks.ts new file mode 100644 index 0000000000000..3b7b7c234800f --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.mocks.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const validateArtifactArchiveMock = jest.fn(); +export const fetchArtifactVersionsMock = jest.fn(); +export const createIndexMock = jest.fn(); +export const populateIndexMock = jest.fn(); + +jest.doMock('./steps', () => { + const actual = jest.requireActual('./steps'); + return { + ...actual, + validateArtifactArchive: validateArtifactArchiveMock, + fetchArtifactVersions: fetchArtifactVersionsMock, + createIndex: createIndexMock, + populateIndex: populateIndexMock, + }; +}); + +export const downloadToDiskMock = jest.fn(); +export const openZipArchiveMock = jest.fn(); +export const loadMappingFileMock = jest.fn(); + +jest.doMock('./utils', () => { + const actual = jest.requireActual('./utils'); + return { + ...actual, + downloadToDisk: downloadToDiskMock, + openZipArchive: openZipArchiveMock, + loadMappingFile: loadMappingFileMock, + }; +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts new file mode 100644 index 0000000000000..e68bd0e9c5058 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.test.ts @@ -0,0 +1,255 @@ +/* + * 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 { + downloadToDiskMock, + createIndexMock, + populateIndexMock, + loadMappingFileMock, + openZipArchiveMock, + validateArtifactArchiveMock, + fetchArtifactVersionsMock, +} from './package_installer.test.mocks'; + +import { + getArtifactName, + getProductDocIndexName, + DocumentationProduct, + ProductName, +} from '@kbn/product-doc-common'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { installClientMock } from '../doc_install_status/service.mock'; +import { inferenceManagerMock } from '../inference_endpoint/service.mock'; +import type { ProductInstallState } from '../../../common/install_status'; +import { PackageInstaller } from './package_installer'; + +const artifactsFolder = '/lost'; +const artifactRepositoryUrl = 'https://repository.com'; +const kibanaVersion = '8.16.3'; + +const callOrder = (fn: { mock: { invocationCallOrder: number[] } }): number => { + return fn.mock.invocationCallOrder[0]; +}; + +describe('PackageInstaller', () => { + let logger: MockedLogger; + let esClient: ReturnType; + let productDocClient: ReturnType; + let endpointManager: ReturnType; + + let packageInstaller: PackageInstaller; + + beforeEach(() => { + logger = loggerMock.create(); + esClient = elasticsearchServiceMock.createElasticsearchClient(); + productDocClient = installClientMock.create(); + endpointManager = inferenceManagerMock.create(); + packageInstaller = new PackageInstaller({ + artifactsFolder, + logger, + esClient, + productDocClient, + endpointManager, + artifactRepositoryUrl, + kibanaVersion, + }); + }); + + afterEach(() => { + downloadToDiskMock.mockReset(); + createIndexMock.mockReset(); + populateIndexMock.mockReset(); + loadMappingFileMock.mockReset(); + openZipArchiveMock.mockReset(); + validateArtifactArchiveMock.mockReset(); + fetchArtifactVersionsMock.mockReset(); + }); + + describe('installPackage', () => { + it('calls the steps with the right parameters', async () => { + const zipArchive = { + close: jest.fn(), + }; + openZipArchiveMock.mockResolvedValue(zipArchive); + + const mappings = Symbol('mappings'); + loadMappingFileMock.mockResolvedValue(mappings); + + await packageInstaller.installPackage({ productName: 'kibana', productVersion: '8.16' }); + + const artifactName = getArtifactName({ + productName: 'kibana', + productVersion: '8.16', + }); + const indexName = getProductDocIndexName('kibana'); + expect(endpointManager.ensureInternalElserInstalled).toHaveBeenCalledTimes(1); + + expect(downloadToDiskMock).toHaveBeenCalledTimes(1); + expect(downloadToDiskMock).toHaveBeenCalledWith( + `${artifactRepositoryUrl}/${artifactName}`, + `${artifactsFolder}/${artifactName}` + ); + + expect(openZipArchiveMock).toHaveBeenCalledTimes(1); + expect(openZipArchiveMock).toHaveBeenCalledWith(`${artifactsFolder}/${artifactName}`); + + expect(loadMappingFileMock).toHaveBeenCalledTimes(1); + expect(loadMappingFileMock).toHaveBeenCalledWith(zipArchive); + + expect(createIndexMock).toHaveBeenCalledTimes(1); + expect(createIndexMock).toHaveBeenCalledWith({ + indexName, + mappings, + esClient, + log: logger, + }); + + expect(populateIndexMock).toHaveBeenCalledTimes(1); + expect(populateIndexMock).toHaveBeenCalledWith({ + indexName, + archive: zipArchive, + esClient, + log: logger, + }); + + expect(productDocClient.setInstallationSuccessful).toHaveBeenCalledTimes(1); + expect(productDocClient.setInstallationSuccessful).toHaveBeenCalledWith('kibana', indexName); + + expect(zipArchive.close).toHaveBeenCalledTimes(1); + + expect(productDocClient.setInstallationFailed).not.toHaveBeenCalled(); + }); + + it('executes the steps in the right order', async () => { + await packageInstaller.installPackage({ productName: 'kibana', productVersion: '8.16' }); + + expect(callOrder(endpointManager.ensureInternalElserInstalled)).toBeLessThan( + callOrder(downloadToDiskMock) + ); + expect(callOrder(downloadToDiskMock)).toBeLessThan(callOrder(openZipArchiveMock)); + expect(callOrder(openZipArchiveMock)).toBeLessThan(callOrder(loadMappingFileMock)); + expect(callOrder(loadMappingFileMock)).toBeLessThan(callOrder(createIndexMock)); + expect(callOrder(createIndexMock)).toBeLessThan(callOrder(populateIndexMock)); + expect(callOrder(populateIndexMock)).toBeLessThan( + callOrder(productDocClient.setInstallationSuccessful) + ); + }); + + it('closes the archive and calls setInstallationFailed if the installation fails', async () => { + const zipArchive = { + close: jest.fn(), + }; + openZipArchiveMock.mockResolvedValue(zipArchive); + + populateIndexMock.mockImplementation(async () => { + throw new Error('something bad'); + }); + + await expect( + packageInstaller.installPackage({ productName: 'kibana', productVersion: '8.16' }) + ).rejects.toThrowError(); + + expect(productDocClient.setInstallationSuccessful).not.toHaveBeenCalled(); + + expect(zipArchive.close).toHaveBeenCalledTimes(1); + + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + expect.stringContaining('Error during documentation installation') + ); + + expect(productDocClient.setInstallationFailed).toHaveBeenCalledTimes(1); + expect(productDocClient.setInstallationFailed).toHaveBeenCalledWith( + 'kibana', + 'something bad' + ); + }); + }); + + describe('installALl', () => { + it('installs all the packages to their latest version', async () => { + jest.spyOn(packageInstaller, 'installPackage'); + + fetchArtifactVersionsMock.mockResolvedValue({ + kibana: ['8.15', '8.16'], + elasticsearch: ['8.15'], + }); + + await packageInstaller.installAll({}); + + expect(packageInstaller.installPackage).toHaveBeenCalledTimes(2); + + expect(packageInstaller.installPackage).toHaveBeenCalledWith({ + productName: 'kibana', + productVersion: '8.16', + }); + expect(packageInstaller.installPackage).toHaveBeenCalledWith({ + productName: 'elasticsearch', + productVersion: '8.15', + }); + }); + }); + + describe('ensureUpToDate', () => { + it('updates the installed packages to the latest version', async () => { + fetchArtifactVersionsMock.mockResolvedValue({ + kibana: ['8.15', '8.16'], + security: ['8.15', '8.16'], + elasticsearch: ['8.15'], + }); + + productDocClient.getInstallationStatus.mockResolvedValue({ + kibana: { status: 'installed', version: '8.15' }, + security: { status: 'installed', version: '8.16' }, + elasticsearch: { status: 'uninstalled' }, + } as Record); + + jest.spyOn(packageInstaller, 'installPackage'); + + await packageInstaller.ensureUpToDate({}); + + expect(packageInstaller.installPackage).toHaveBeenCalledTimes(1); + expect(packageInstaller.installPackage).toHaveBeenCalledWith({ + productName: 'kibana', + productVersion: '8.16', + }); + }); + }); + + describe('uninstallPackage', () => { + it('performs the uninstall steps', async () => { + await packageInstaller.uninstallPackage({ productName: 'kibana' }); + + expect(esClient.indices.delete).toHaveBeenCalledTimes(1); + expect(esClient.indices.delete).toHaveBeenCalledWith( + { + index: getProductDocIndexName('kibana'), + }, + expect.objectContaining({ ignore: [404] }) + ); + + expect(productDocClient.setUninstalled).toHaveBeenCalledTimes(1); + expect(productDocClient.setUninstalled).toHaveBeenCalledWith('kibana'); + }); + }); + + describe('uninstallAll', () => { + it('calls uninstall for all packages', async () => { + jest.spyOn(packageInstaller, 'uninstallPackage'); + + await packageInstaller.uninstallAll(); + + expect(packageInstaller.uninstallPackage).toHaveBeenCalledTimes( + Object.keys(DocumentationProduct).length + ); + Object.values(DocumentationProduct).forEach((productName) => { + expect(packageInstaller.uninstallPackage).toHaveBeenCalledWith({ productName }); + }); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts new file mode 100644 index 0000000000000..7739219c15dc6 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/package_installer.ts @@ -0,0 +1,218 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import { + getArtifactName, + getProductDocIndexName, + DocumentationProduct, + type ProductName, +} from '@kbn/product-doc-common'; +import type { ProductDocInstallClient } from '../doc_install_status'; +import type { InferenceEndpointManager } from '../inference_endpoint'; +import { downloadToDisk, openZipArchive, loadMappingFile, type ZipArchive } from './utils'; +import { majorMinor, latestVersion } from './utils/semver'; +import { + validateArtifactArchive, + fetchArtifactVersions, + createIndex, + populateIndex, +} from './steps'; + +interface PackageInstallerOpts { + artifactsFolder: string; + logger: Logger; + esClient: ElasticsearchClient; + productDocClient: ProductDocInstallClient; + endpointManager: InferenceEndpointManager; + artifactRepositoryUrl: string; + kibanaVersion: string; +} + +export class PackageInstaller { + private readonly log: Logger; + private readonly artifactsFolder: string; + private readonly esClient: ElasticsearchClient; + private readonly productDocClient: ProductDocInstallClient; + private readonly endpointManager: InferenceEndpointManager; + private readonly artifactRepositoryUrl: string; + private readonly currentVersion: string; + + constructor({ + artifactsFolder, + logger, + esClient, + productDocClient, + endpointManager, + artifactRepositoryUrl, + kibanaVersion, + }: PackageInstallerOpts) { + this.esClient = esClient; + this.productDocClient = productDocClient; + this.artifactsFolder = artifactsFolder; + this.endpointManager = endpointManager; + this.artifactRepositoryUrl = artifactRepositoryUrl; + this.currentVersion = majorMinor(kibanaVersion); + this.log = logger; + } + + /** + * Make sure that the currently installed doc packages are up to date. + * Will not upgrade products that are not already installed + */ + async ensureUpToDate({}: {}) { + const [repositoryVersions, installStatuses] = await Promise.all([ + fetchArtifactVersions({ + artifactRepositoryUrl: this.artifactRepositoryUrl, + }), + this.productDocClient.getInstallationStatus(), + ]); + + const toUpdate: Array<{ + productName: ProductName; + productVersion: string; + }> = []; + Object.entries(installStatuses).forEach(([productName, productState]) => { + if (productState.status === 'uninstalled') { + return; + } + const availableVersions = repositoryVersions[productName as ProductName]; + if (!availableVersions || !availableVersions.length) { + return; + } + const selectedVersion = selectVersion(this.currentVersion, availableVersions); + if (productState.version !== selectedVersion) { + toUpdate.push({ + productName: productName as ProductName, + productVersion: selectedVersion, + }); + } + }); + + for (const { productName, productVersion } of toUpdate) { + await this.installPackage({ + productName, + productVersion, + }); + } + } + + async installAll({}: {}) { + const repositoryVersions = await fetchArtifactVersions({ + artifactRepositoryUrl: this.artifactRepositoryUrl, + }); + const allProducts = Object.values(DocumentationProduct) as ProductName[]; + for (const productName of allProducts) { + const availableVersions = repositoryVersions[productName]; + if (!availableVersions || !availableVersions.length) { + this.log.warn(`No version found for product [${productName}]`); + continue; + } + const selectedVersion = selectVersion(this.currentVersion, availableVersions); + + await this.installPackage({ + productName, + productVersion: selectedVersion, + }); + } + } + + async installPackage({ + productName, + productVersion, + }: { + productName: ProductName; + productVersion: string; + }) { + this.log.info( + `Starting installing documentation for product [${productName}] and version [${productVersion}]` + ); + + productVersion = majorMinor(productVersion); + + await this.uninstallPackage({ productName }); + + let zipArchive: ZipArchive | undefined; + try { + await this.productDocClient.setInstallationStarted({ + productName, + productVersion, + }); + + await this.endpointManager.ensureInternalElserInstalled(); + + const artifactFileName = getArtifactName({ productName, productVersion }); + const artifactUrl = `${this.artifactRepositoryUrl}/${artifactFileName}`; + const artifactPath = `${this.artifactsFolder}/${artifactFileName}`; + + this.log.debug(`Downloading from [${artifactUrl}] to [${artifactPath}]`); + await downloadToDisk(artifactUrl, artifactPath); + + zipArchive = await openZipArchive(artifactPath); + + validateArtifactArchive(zipArchive); + + const mappings = await loadMappingFile(zipArchive); + + const indexName = getProductDocIndexName(productName); + + await createIndex({ + indexName, + mappings, + esClient: this.esClient, + log: this.log, + }); + + await populateIndex({ + indexName, + archive: zipArchive, + esClient: this.esClient, + log: this.log, + }); + await this.productDocClient.setInstallationSuccessful(productName, indexName); + + this.log.info( + `Documentation installation successful for product [${productName}] and version [${productVersion}]` + ); + } catch (e) { + this.log.error( + `Error during documentation installation of product [${productName}]/[${productVersion}] : ${e.message}` + ); + + await this.productDocClient.setInstallationFailed(productName, e.message); + throw e; + } finally { + zipArchive?.close(); + } + } + + async uninstallPackage({ productName }: { productName: ProductName }) { + const indexName = getProductDocIndexName(productName); + await this.esClient.indices.delete( + { + index: indexName, + }, + { ignore: [404] } + ); + + await this.productDocClient.setUninstalled(productName); + } + + async uninstallAll() { + const allProducts = Object.values(DocumentationProduct); + for (const productName of allProducts) { + await this.uninstallPackage({ productName }); + } + } +} + +const selectVersion = (currentVersion: string, availableVersions: string[]): string => { + return availableVersions.includes(currentVersion) + ? currentVersion + : latestVersion(availableVersions); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts new file mode 100644 index 0000000000000..fca8b5283c300 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { createIndex } from './create_index'; +import { internalElserInferenceId } from '../../../../common/consts'; + +describe('createIndex', () => { + let log: MockedLogger; + let esClient: ElasticsearchClient; + + beforeEach(() => { + log = loggerMock.create(); + esClient = elasticsearchServiceMock.createElasticsearchClient(); + }); + + it('calls esClient.indices.create with the right parameters', async () => { + const mappings: MappingTypeMapping = { + properties: {}, + }; + const indexName = '.some-index'; + + await createIndex({ + indexName, + mappings, + log, + esClient, + }); + + expect(esClient.indices.create).toHaveBeenCalledTimes(1); + expect(esClient.indices.create).toHaveBeenCalledWith({ + index: indexName, + mappings, + settings: { + number_of_shards: 1, + auto_expand_replicas: '0-1', + }, + }); + }); + + it('rewrites the inference_id attribute of semantic_text fields in the mapping', async () => { + const mappings: MappingTypeMapping = { + properties: { + semantic: { + type: 'semantic_text', + inference_id: '.elser', + }, + bool: { + type: 'boolean', + }, + }, + }; + + await createIndex({ + indexName: '.some-index', + mappings, + log, + esClient, + }); + + expect(esClient.indices.create).toHaveBeenCalledWith( + expect.objectContaining({ + mappings: { + properties: { + semantic: { + type: 'semantic_text', + inference_id: internalElserInferenceId, + }, + bool: { + type: 'boolean', + }, + }, + }, + }) + ); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts new file mode 100644 index 0000000000000..decd62e556ba5 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/create_index.ts @@ -0,0 +1,50 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { MappingTypeMapping, MappingProperty } from '@elastic/elasticsearch/lib/api/types'; +import { internalElserInferenceId } from '../../../../common/consts'; + +export const createIndex = async ({ + esClient, + indexName, + mappings, + log, +}: { + esClient: ElasticsearchClient; + indexName: string; + mappings: MappingTypeMapping; + log: Logger; +}) => { + log.debug(`Creating index ${indexName}`); + + overrideInferenceId(mappings, internalElserInferenceId); + + await esClient.indices.create({ + index: indexName, + mappings, + settings: { + number_of_shards: 1, + auto_expand_replicas: '0-1', + }, + }); +}; + +const overrideInferenceId = (mappings: MappingTypeMapping, inferenceId: string) => { + const recursiveOverride = (current: MappingTypeMapping | MappingProperty) => { + if ('type' in current && current.type === 'semantic_text') { + current.inference_id = inferenceId; + } + if ('properties' in current && current.properties) { + for (const prop of Object.values(current.properties)) { + recursiveOverride(prop); + } + } + }; + recursiveOverride(mappings); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.test.ts new file mode 100644 index 0000000000000..805008ccab698 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.test.ts @@ -0,0 +1,129 @@ +/* + * 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 fetch, { Response } from 'node-fetch'; +import { fetchArtifactVersions } from './fetch_artifact_versions'; +import { getArtifactName, DocumentationProduct, ProductName } from '@kbn/product-doc-common'; + +jest.mock('node-fetch'); +const fetchMock = fetch as jest.MockedFn; + +const createResponse = ({ + artifactNames, + truncated = false, +}: { + artifactNames: string[]; + truncated?: boolean; +}) => { + return ` + + kibana-ai-assistant-kb-artifacts + + + ${truncated} + ${artifactNames.map( + (artifactName) => ` + + ${artifactName} + 1728486063097626 + 1 + 2024-10-09T15:01:03.137Z + "e0584955969eccf2a16b8829f768cb1f" + 36781438 + ` + )} + + `; +}; + +const artifactRepositoryUrl = 'https://lost.com'; + +const expectVersions = ( + versions: Partial> +): Record => { + const response = {} as Record; + Object.values(DocumentationProduct).forEach((productName) => { + response[productName] = []; + }); + return { + ...response, + ...versions, + }; +}; + +describe('fetchArtifactVersions', () => { + beforeEach(() => { + fetchMock.mockReset(); + }); + + const mockResponse = (responseText: string) => { + const response = { + text: () => Promise.resolve(responseText), + }; + fetchMock.mockResolvedValue(response as Response); + }; + + it('calls fetch with the right parameters', async () => { + mockResponse(createResponse({ artifactNames: [] })); + + await fetchArtifactVersions({ artifactRepositoryUrl }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith(`${artifactRepositoryUrl}?max-keys=1000`); + }); + + it('returns the list of versions from the repository', async () => { + const artifactNames = [ + getArtifactName({ productName: 'kibana', productVersion: '8.16' }), + getArtifactName({ productName: 'elasticsearch', productVersion: '8.16' }), + ]; + mockResponse(createResponse({ artifactNames })); + + const versions = await fetchArtifactVersions({ artifactRepositoryUrl }); + + expect(versions).toEqual( + expectVersions({ + kibana: ['8.16'], + elasticsearch: ['8.16'], + }) + ); + }); + + it('retrieve all versions for each product', async () => { + const artifactNames = [ + getArtifactName({ productName: 'kibana', productVersion: '8.15' }), + getArtifactName({ productName: 'kibana', productVersion: '8.16' }), + getArtifactName({ productName: 'kibana', productVersion: '8.17' }), + getArtifactName({ productName: 'elasticsearch', productVersion: '8.16' }), + getArtifactName({ productName: 'elasticsearch', productVersion: '9.0' }), + ]; + mockResponse(createResponse({ artifactNames })); + + const versions = await fetchArtifactVersions({ artifactRepositoryUrl }); + + expect(versions).toEqual( + expectVersions({ + kibana: ['8.15', '8.16', '8.17'], + elasticsearch: ['8.16', '9.0'], + }) + ); + }); + + it('throws an error if the response is truncated', async () => { + mockResponse(createResponse({ artifactNames: [], truncated: true })); + + await expect(fetchArtifactVersions({ artifactRepositoryUrl })).rejects.toThrowError( + /bucket content is truncated/ + ); + }); + + it('throws an error if the response is not valid xml', async () => { + mockResponse('some plain text'); + + await expect(fetchArtifactVersions({ artifactRepositoryUrl })).rejects.toThrowError(); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.ts new file mode 100644 index 0000000000000..69c6db2d5d8ae --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/fetch_artifact_versions.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 fetch from 'node-fetch'; +import { parseString } from 'xml2js'; +import { type ProductName, DocumentationProduct, parseArtifactName } from '@kbn/product-doc-common'; + +type ArtifactAvailableVersions = Record; + +export const fetchArtifactVersions = async ({ + artifactRepositoryUrl, +}: { + artifactRepositoryUrl: string; +}): Promise => { + const res = await fetch(`${artifactRepositoryUrl}?max-keys=1000`); + const xml = await res.text(); + return new Promise((resolve, reject) => { + parseString(xml, (err, result: ListBucketResponse) => { + if (err) { + reject(err); + } + + // 6 artifacts per minor stack version means we have a few decades before facing this problem + if (result.ListBucketResult.IsTruncated?.includes('true')) { + throw new Error('bucket content is truncated, cannot retrieve all versions'); + } + + const allowedProductNames: ProductName[] = Object.values(DocumentationProduct); + + const record: ArtifactAvailableVersions = {} as ArtifactAvailableVersions; + allowedProductNames.forEach((product) => { + record[product] = []; + }); + + result.ListBucketResult.Contents?.forEach((contentEntry) => { + const artifactName = contentEntry.Key[0]; + const parsed = parseArtifactName(artifactName); + if (parsed) { + const { productName, productVersion } = parsed; + record[productName]!.push(productVersion); + } + }); + + resolve(record); + }); + }); +}; + +interface ListBucketResponse { + ListBucketResult: { + Name?: string[]; + IsTruncated?: string[]; + Contents?: Array<{ Key: string[] }>; + }; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/index.ts new file mode 100644 index 0000000000000..3c84fc9cccf1a --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { createIndex } from './create_index'; +export { populateIndex } from './populate_index'; +export { validateArtifactArchive } from './validate_artifact_archive'; +export { fetchArtifactVersions } from './fetch_artifact_versions'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.test.ts new file mode 100644 index 0000000000000..2f301f9928e9a --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { times } from 'lodash'; +import { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { internalElserInferenceId } from '../../../../common/consts'; +import type { ZipArchive } from '../utils/zip_archive'; +import { populateIndex } from './populate_index'; + +const createMockArchive = (entries: Record): ZipArchive => { + return { + hasEntry: (entryPath) => Object.keys(entries).includes(entryPath), + getEntryPaths: () => Object.keys(entries), + getEntryContent: async (entryPath) => Buffer.from(entries[entryPath]), + close: () => undefined, + }; +}; + +const createContentFile = (count: number, offset: number = 0): string => { + return times(count) + .map((i) => JSON.stringify({ idx: offset + i })) + .join('\n'); +}; + +describe('populateIndex', () => { + let log: MockedLogger; + let esClient: ReturnType; + + beforeEach(() => { + log = loggerMock.create(); + esClient = elasticsearchServiceMock.createElasticsearchClient(); + }); + + it('calls `esClient.bulk` once per content file', async () => { + const archive = createMockArchive({ + 'content/content-0.ndjson': createContentFile(2), + 'content/content-1.ndjson': createContentFile(2), + }); + + await populateIndex({ + indexName: '.foo', + archive, + log, + esClient, + }); + + expect(esClient.bulk).toHaveBeenCalledTimes(2); + }); + + it('calls `esClient.bulk` with the right payload', async () => { + const archive = createMockArchive({ + 'content/content-0.ndjson': createContentFile(2), + }); + + await populateIndex({ + indexName: '.foo', + archive, + log, + esClient, + }); + + expect(esClient.bulk).toHaveBeenCalledTimes(1); + expect(esClient.bulk).toHaveBeenCalledWith({ + refresh: false, + operations: [ + { index: { _index: '.foo' } }, + { idx: 0 }, + { index: { _index: '.foo' } }, + { idx: 1 }, + ], + }); + }); + + it('rewrites the inference_id of semantic fields', async () => { + const archive = createMockArchive({ + 'content/content-0.ndjson': JSON.stringify({ + semantic: { text: 'foo', inference: { inference_id: '.some-inference' } }, + }), + }); + + await populateIndex({ + indexName: '.foo', + archive, + log, + esClient, + }); + + expect(esClient.bulk).toHaveBeenCalledTimes(1); + expect(esClient.bulk).toHaveBeenCalledWith({ + refresh: false, + operations: [ + { index: { _index: '.foo' } }, + { + semantic: { + inference: { + inference_id: internalElserInferenceId, + }, + text: 'foo', + }, + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts new file mode 100644 index 0000000000000..017757ca90b99 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/populate_index.ts @@ -0,0 +1,84 @@ +/* + * 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 { BulkRequest } from '@elastic/elasticsearch/lib/api/types'; +import type { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import { isArtifactContentFilePath } from '@kbn/product-doc-common'; +import { internalElserInferenceId } from '../../../../common/consts'; +import type { ZipArchive } from '../utils/zip_archive'; + +export const populateIndex = async ({ + esClient, + indexName, + archive, + log, +}: { + esClient: ElasticsearchClient; + indexName: string; + archive: ZipArchive; + log: Logger; +}) => { + log.debug(`Starting populating index ${indexName}`); + + const contentEntries = archive.getEntryPaths().filter(isArtifactContentFilePath); + + for (let i = 0; i < contentEntries.length; i++) { + const entryPath = contentEntries[i]; + log.debug(`Indexing content for entry ${entryPath}`); + const contentBuffer = await archive.getEntryContent(entryPath); + await indexContentFile({ indexName, esClient, contentBuffer }); + } + + log.debug(`Done populating index ${indexName}`); +}; + +const indexContentFile = async ({ + indexName, + contentBuffer, + esClient, +}: { + indexName: string; + contentBuffer: Buffer; + esClient: ElasticsearchClient; +}) => { + const fileContent = contentBuffer.toString('utf-8'); + const lines = fileContent.split('\n'); + + const documents = lines + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + return JSON.parse(line); + }) + .map((doc) => rewriteInferenceId(doc, internalElserInferenceId)); + + const operations = documents.reduce((ops, document) => { + ops!.push(...[{ index: { _index: indexName } }, document]); + return ops; + }, [] as BulkRequest['operations']); + + const response = await esClient.bulk({ + refresh: false, + operations, + }); + + if (response.errors) { + const error = response.items.find((item) => item.index?.error)?.index?.error ?? 'unknown error'; + throw new Error(`Error indexing documents: ${JSON.stringify(error)}`); + } +}; + +const rewriteInferenceId = (document: Record, inferenceId: string) => { + // we don't need to handle nested fields, we don't have any and won't. + Object.values(document).forEach((field) => { + if (field.inference) { + field.inference.inference_id = inferenceId; + } + }); + return document; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.test.ts new file mode 100644 index 0000000000000..607277aaf3466 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { ZipArchive } from '../utils/zip_archive'; +import { validateArtifactArchive } from './validate_artifact_archive'; + +const createMockArchive = (entryPaths: string[]): ZipArchive => { + return { + hasEntry: (entryPath) => entryPaths.includes(entryPath), + getEntryPaths: () => entryPaths, + getEntryContent: () => { + throw new Error('non implemented'); + }, + close: () => undefined, + }; +}; + +describe('validateArtifactArchive', () => { + it('validates that the archive contains all the mandatory files', () => { + const archive = createMockArchive([ + 'manifest.json', + 'mappings.json', + 'content/content-1.ndjson', + ]); + + const validation = validateArtifactArchive(archive); + + expect(validation).toEqual({ valid: true }); + }); + + it('does not validate if the archive does not contain a manifest', () => { + const archive = createMockArchive(['something.txt']); + + const validation = validateArtifactArchive(archive); + + expect(validation).toMatchInlineSnapshot(` + Object { + "error": "Manifest file not found", + "valid": false, + } + `); + }); + + it('does not validate if the archive does not contain mappings', () => { + const archive = createMockArchive(['manifest.json']); + + const validation = validateArtifactArchive(archive); + + expect(validation).toMatchInlineSnapshot(` + Object { + "error": "Mapping file not found", + "valid": false, + } + `); + }); + + it('does not validate if the archive does not contain content files', () => { + const archive = createMockArchive(['manifest.json', 'mappings.json']); + + const validation = validateArtifactArchive(archive); + + expect(validation).toMatchInlineSnapshot(` + Object { + "error": "No content files were found", + "valid": false, + } + `); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.ts new file mode 100644 index 0000000000000..471d7c080c481 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/steps/validate_artifact_archive.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isArtifactContentFilePath } from '@kbn/product-doc-common'; +import type { ZipArchive } from '../utils/zip_archive'; + +type ValidationResult = { valid: true } | { valid: false; error: string }; + +export const validateArtifactArchive = (archive: ZipArchive): ValidationResult => { + if (!archive.hasEntry('manifest.json')) { + return { valid: false, error: 'Manifest file not found' }; + } + if (!archive.hasEntry('mappings.json')) { + return { valid: false, error: 'Mapping file not found' }; + } + if (!archive.getEntryPaths().some(isArtifactContentFilePath)) { + return { valid: false, error: 'No content files were found' }; + } + return { valid: true }; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.test.ts new file mode 100644 index 0000000000000..9d42be652d74d --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import type { ArtifactManifest } from '@kbn/product-doc-common'; +import type { ZipArchive } from './zip_archive'; +import { loadManifestFile, loadMappingFile } from './archive_accessors'; + +const createMockArchive = (entries: Record): ZipArchive => { + return { + hasEntry: (entryPath) => Object.keys(entries).includes(entryPath), + getEntryPaths: () => Object.keys(entries), + getEntryContent: async (entryPath) => Buffer.from(entries[entryPath]), + close: () => undefined, + }; +}; + +describe('loadManifestFile', () => { + it('parses the manifest from the archive', async () => { + const manifest: ArtifactManifest = { + formatVersion: '1.0.0', + productName: 'kibana', + productVersion: '8.16', + }; + const archive = createMockArchive({ 'manifest.json': JSON.stringify(manifest) }); + + const parsedManifest = await loadManifestFile(archive); + + expect(parsedManifest).toEqual(manifest); + }); + + it('throws if the archive does not contain the manifest', async () => { + const archive = createMockArchive({}); + + await expect(loadManifestFile(archive)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not load archive file: \\"manifest.json\\" not found in archive"` + ); + }); + + it('throws if the manifest cannot be parsed', async () => { + const archive = createMockArchive({ 'manifest.json': '{}}}{' }); + + await expect(loadManifestFile(archive)).rejects.toThrowError(); + }); +}); + +describe('loadMappingFile', () => { + it('parses the manifest from the archive', async () => { + const mappings: MappingTypeMapping = { + properties: { + foo: { type: 'text' }, + }, + }; + const archive = createMockArchive({ 'mappings.json': JSON.stringify(mappings) }); + + const parsedMappings = await loadMappingFile(archive); + + expect(parsedMappings).toEqual(mappings); + }); + + it('throws if the archive does not contain the manifest', async () => { + const archive = createMockArchive({}); + + await expect(loadMappingFile(archive)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not load archive file: \\"mappings.json\\" not found in archive"` + ); + }); + + it('throws if the manifest cannot be parsed', async () => { + const archive = createMockArchive({ 'mappings.json': '{}}}{' }); + + await expect(loadMappingFile(archive)).rejects.toThrowError(); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.ts new file mode 100644 index 0000000000000..a4ec4f4418f3c --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/archive_accessors.ts @@ -0,0 +1,33 @@ +/* + * 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 { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types'; +import type { ArtifactManifest } from '@kbn/product-doc-common'; +import type { ZipArchive } from './zip_archive'; + +const manifestEntryPath = 'manifest.json'; +const mappingsEntryPath = 'mappings.json'; + +export const loadManifestFile = async (archive: ZipArchive): Promise => { + return await parseEntryContent(manifestEntryPath, archive); +}; + +export const loadMappingFile = async (archive: ZipArchive): Promise => { + return await parseEntryContent(mappingsEntryPath, archive); +}; + +const parseEntryContent = async (entryPath: string, archive: ZipArchive): Promise => { + if (!archive.hasEntry(entryPath)) { + throw new Error(`Could not load archive file: "${entryPath}" not found in archive`); + } + try { + const buffer = await archive.getEntryContent(entryPath); + return JSON.parse(buffer.toString('utf-8')); + } catch (e) { + throw new Error(`Could not parse archive file: ${e}`); + } +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/download.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/download.ts new file mode 100644 index 0000000000000..ea5357792ef5f --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/download.ts @@ -0,0 +1,23 @@ +/* + * 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 { createWriteStream } from 'fs'; +import { mkdir } from 'fs/promises'; +import Path from 'path'; +import fetch from 'node-fetch'; + +export const downloadToDisk = async (fileUrl: string, filePath: string) => { + const dirPath = Path.dirname(filePath); + await mkdir(dirPath, { recursive: true }); + const res = await fetch(fileUrl); + const fileStream = createWriteStream(filePath); + await new Promise((resolve, reject) => { + res.body.pipe(fileStream); + res.body.on('error', reject); + fileStream.on('finish', resolve); + }); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/index.ts new file mode 100644 index 0000000000000..a612a8c6e9f46 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/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 { downloadToDisk } from './download'; +export { openZipArchive, type ZipArchive } from './zip_archive'; +export { loadManifestFile, loadMappingFile } from './archive_accessors'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.test.ts new file mode 100644 index 0000000000000..9bc20f2eecdbd --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { latestVersion, majorMinor } from './semver'; + +describe('majorMinor', () => { + it('returns the version in a {major.minor} format', () => { + expect(majorMinor('9.17.5')).toEqual('9.17'); + }); + it('ignores qualifiers', () => { + expect(majorMinor('10.42.9000-snap')).toEqual('10.42'); + }); + it('accepts {major.minor} format as input', () => { + expect(majorMinor('8.16')).toEqual('8.16'); + }); +}); + +describe('latestVersion', () => { + it('returns the highest version from the list', () => { + expect(latestVersion(['7.16.3', '8.1.4', '6.14.2'])).toEqual('8.1.4'); + }); + it('accepts versions in a {major.minor} format', () => { + expect(latestVersion(['9.16', '9.3'])).toEqual('9.16'); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.ts new file mode 100644 index 0000000000000..b4e38215af90e --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/semver.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Semver from 'semver'; + +export const latestVersion = (versions: string[]): string => { + let latest: string = versions[0]; + for (let i = 1; i < versions.length; i++) { + const current = versions[i]; + if (Semver.gt(Semver.coerce(current)!, Semver.coerce(latest)!)) { + latest = current; + } + } + return latest; +}; + +export const majorMinor = (version: string): string => { + const parsed = Semver.coerce(version); + if (!parsed) { + throw new Error(`Not a valid semver version: [${version}]`); + } + return `${parsed.major}.${parsed.minor}`; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/test_data/test_archive_1.zip b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/test_data/test_archive_1.zip new file mode 100644 index 0000000000000000000000000000000000000000..fce195d2c4db26c8590ec97f45dc9dfc3fcf8346 GIT binary patch literal 800 zcmWIWW@h1H00EW;|H!p|Div%%HVAVu$S{6KKJgobc3FlUtVrDpZQZmq# zR2(K5B20>^@{a^M6%N>ezROE3E=f(%2YYRIHedQ4AV%{VssZbSK3(Sk8Uez>2m>JI z#3S4UGOQTWO)zx<-i%Cg%(y~b0_;X$*fK0>1Tm50fE5x47>-1khZ*+B=6S))gT@5V zJWx#FF%L5akjv#`n9z0fnhJgZ-fMKX{g6ueX eU>HITgM~aK)!;Ii6_m^vSb=aFP-_V&3K#%85|UK_ literal 0 HcmV?d00001 diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.test.ts new file mode 100644 index 0000000000000..71cd5891c5e5d --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Path from 'path'; +import { openZipArchive, ZipArchive } from './zip_archive'; + +const ZIP_PATH = Path.resolve(__dirname, './test_data/test_archive_1.zip'); + +describe('ZipArchive', () => { + let archive: ZipArchive; + + beforeAll(async () => { + archive = await openZipArchive(ZIP_PATH); + }); + + afterAll(() => { + archive?.close(); + }); + + test('#getEntryPaths returns the path of all entries', () => { + expect(archive.getEntryPaths().sort()).toEqual([ + 'nested/', + 'nested/nested_1.txt', + 'text_1.txt', + 'text_2.txt', + 'text_3.txt', + ]); + }); + + test('#hasEntry returns true if the entry exists, false otherwise', () => { + expect(archive.hasEntry('nested/nested_1.txt')).toBe(true); + expect(archive.hasEntry('not_an_entry')).toBe(false); + }); + + test('#getEntryContent returns the content of the entry', async () => { + const buffer = await archive.getEntryContent('text_1.txt'); + expect(buffer.toString('utf-8')).toEqual('text_1'); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.ts new file mode 100644 index 0000000000000..dbc4ec1b3e41f --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/package_installer/utils/zip_archive.ts @@ -0,0 +1,91 @@ +/* + * 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 yauzl from 'yauzl'; + +export interface ZipArchive { + hasEntry(entryPath: string): boolean; + getEntryPaths(): string[]; + getEntryContent(entryPath: string): Promise; + close(): void; +} + +export const openZipArchive = async (archivePath: string): Promise => { + return new Promise((resolve, reject) => { + const entries: yauzl.Entry[] = []; + yauzl.open(archivePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => { + if (err || !zipFile) { + return reject(err ?? 'No zip file'); + } + + zipFile!.on('entry', (entry) => { + entries.push(entry); + zipFile.readEntry(); + }); + + zipFile.on('end', () => { + const archive = new ZipArchiveImpl(entries, zipFile); + resolve(archive); + }); + + zipFile.on('close', () => {}); + + zipFile.readEntry(); + }); + }); +}; + +class ZipArchiveImpl implements ZipArchive { + private readonly zipFile: yauzl.ZipFile; + private readonly entries: Map; + + constructor(entries: yauzl.Entry[], zipFile: yauzl.ZipFile) { + this.zipFile = zipFile; + this.entries = new Map(entries.map((entry) => [entry.fileName, entry])); + } + + hasEntry(entryPath: string) { + return this.entries.has(entryPath); + } + + getEntryPaths() { + return [...this.entries.keys()]; + } + + getEntryContent(entryPath: string) { + const foundEntry = this.entries.get(entryPath); + if (!foundEntry) { + throw new Error(`Entry ${entryPath} not found in archive`); + } + return getZipEntryContent(this.zipFile, foundEntry); + } + + close() { + this.zipFile.close(); + } +} + +const getZipEntryContent = async (zipFile: yauzl.ZipFile, entry: yauzl.Entry): Promise => { + return new Promise((resolve, reject) => { + zipFile.openReadStream(entry, (err, readStream) => { + if (err) { + return reject(err); + } else { + const chunks: Buffer[] = []; + readStream!.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + readStream!.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + readStream!.on('error', () => { + reject(); + }); + } + }); + }); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/index.ts new file mode 100644 index 0000000000000..3e5ac95ae4edf --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/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 { SearchService } from './search_service'; +export type { DocSearchOptions, DocSearchResult, DocSearchResponse, SearchApi } from './types'; diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/perform_semantic_search.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/perform_search.ts similarity index 78% rename from x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/perform_semantic_search.ts rename to x-pack/plugins/ai_infra/product_doc_base/server/services/search/perform_search.ts index 373a6b8755429..03c3b72f86f92 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/perform_semantic_search.ts +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/perform_search.ts @@ -5,29 +5,27 @@ * 2.0. */ -import type { Client } from '@elastic/elasticsearch'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { ProductDocumentationAttributes } from '@kbn/product-doc-common'; // https://search-labs.elastic.co/search-labs/blog/elser-rag-search-for-relevance -export const performSemanticSearch = async ({ +export const performSearch = async ({ searchQuery, + size, index, client, }: { searchQuery: string; - index: string; - client: Client; + size: number; + index: string | string[]; + client: ElasticsearchClient; }) => { - const results = await client.search({ + const results = await client.search({ index, - size: 3, + size, query: { bool: { - filter: { - bool: { - must: [{ term: { version: '8.15' } }], - }, - }, should: [ { multi_match: { @@ -37,7 +35,7 @@ export const performSemanticSearch = async ({ fields: [ 'content_title', 'content_body.text', - 'ai_subtitle.text', + 'ai_subtitle', 'ai_summary.text', 'ai_questions_answered.text', 'ai_tags', @@ -65,12 +63,6 @@ export const performSemanticSearch = async ({ query: searchQuery, }, }, - { - semantic: { - field: 'ai_subtitle', - query: searchQuery, - }, - }, { semantic: { field: 'ai_summary', diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.test.ts new file mode 100644 index 0000000000000..c8053ca981e71 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { loggerMock, type MockedLogger } from '@kbn/logging-mocks'; +import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { SearchService } from './search_service'; +import { getIndicesForProductNames } from './utils'; + +import { performSearch } from './perform_search'; +jest.mock('./perform_search'); +const performSearchMock = performSearch as jest.MockedFn; + +describe('SearchService', () => { + let logger: MockedLogger; + let esClient: ReturnType; + let service: SearchService; + + beforeEach(() => { + logger = loggerMock.create(); + esClient = elasticsearchServiceMock.createElasticsearchClient(); + service = new SearchService({ logger, esClient }); + + performSearchMock.mockResolvedValue([]); + }); + + afterEach(() => { + performSearchMock.mockReset(); + }); + + describe('#search', () => { + it('calls `performSearch` with the right parameters', async () => { + await service.search({ + query: 'What is Kibana?', + products: ['kibana'], + max: 42, + }); + + expect(performSearchMock).toHaveBeenCalledTimes(1); + expect(performSearchMock).toHaveBeenCalledWith({ + searchQuery: 'What is Kibana?', + size: 42, + index: getIndicesForProductNames(['kibana']), + client: esClient, + }); + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.ts new file mode 100644 index 0000000000000..a0b1e4fd4a836 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/search_service.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 type { Logger } from '@kbn/logging'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import { getIndicesForProductNames, mapResult } from './utils'; +import { performSearch } from './perform_search'; +import type { DocSearchOptions, DocSearchResponse } from './types'; + +export class SearchService { + private readonly log: Logger; + private readonly esClient: ElasticsearchClient; + + constructor({ logger, esClient }: { logger: Logger; esClient: ElasticsearchClient }) { + this.log = logger; + this.esClient = esClient; + } + + async search(options: DocSearchOptions): Promise { + const { query, max = 3, products } = options; + this.log.debug(`performing search - query=[${query}]`); + const results = await performSearch({ + searchQuery: query, + size: max, + index: getIndicesForProductNames(products), + client: this.esClient, + }); + + return { + results: results.map(mapResult), + }; + } +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/types.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/types.ts new file mode 100644 index 0000000000000..fb474bbf4deab --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ProductName } from '@kbn/product-doc-common'; + +export interface DocSearchOptions { + query: string; + max?: number; + products?: ProductName[]; +} + +export interface DocSearchResult { + title: string; + content: string; + url: string; + productName: ProductName; +} + +export interface DocSearchResponse { + results: DocSearchResult[]; +} + +export type SearchApi = (options: DocSearchOptions) => Promise; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts new file mode 100644 index 0000000000000..0293d086d4f13 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { productDocIndexPattern, getProductDocIndexName } from '@kbn/product-doc-common'; +import { getIndicesForProductNames } from './get_indices_for_product_names'; + +describe('getIndicesForProductNames', () => { + it('returns the index pattern when product names are not specified', () => { + expect(getIndicesForProductNames(undefined)).toEqual(productDocIndexPattern); + expect(getIndicesForProductNames([])).toEqual(productDocIndexPattern); + }); + it('returns individual index names when product names are specified', () => { + expect(getIndicesForProductNames(['kibana', 'elasticsearch'])).toEqual([ + getProductDocIndexName('kibana'), + getProductDocIndexName('elasticsearch'), + ]); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.ts new file mode 100644 index 0000000000000..e97ed9cea3611 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/get_indices_for_product_names.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 { + productDocIndexPattern, + getProductDocIndexName, + type ProductName, +} from '@kbn/product-doc-common'; + +export const getIndicesForProductNames = ( + productNames: ProductName[] | undefined +): string | string[] => { + if (!productNames || !productNames.length) { + return productDocIndexPattern; + } + return productNames.map(getProductDocIndexName); +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/index.ts new file mode 100644 index 0000000000000..1a6a2eaa24a99 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/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 { getIndicesForProductNames } from './get_indices_for_product_names'; +export { mapResult } from './map_result'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.test.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.test.ts new file mode 100644 index 0000000000000..56e8ce4875cc5 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { ProductDocumentationAttributes } from '@kbn/product-doc-common'; +import { mapResult } from './map_result'; + +const createHit = ( + attrs: ProductDocumentationAttributes +): SearchHit => { + return { + _index: '.foo', + _source: attrs, + }; +}; + +describe('mapResult', () => { + it('returns the expected shape', () => { + const input = createHit({ + content_title: 'content_title', + content_body: { text: 'content_body' }, + product_name: 'kibana', + root_type: 'documentation', + slug: 'foo.html', + url: 'http://lost.com/foo.html', + version: '8.16', + ai_subtitle: 'ai_subtitle', + ai_summary: { text: 'ai_summary' }, + ai_questions_answered: { text: ['question A'] }, + ai_tags: ['foo', 'bar', 'test'], + }); + + const output = mapResult(input); + + expect(output).toEqual({ + content: 'content_body', + productName: 'kibana', + title: 'content_title', + url: 'http://lost.com/foo.html', + }); + }); +}); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.ts b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.ts new file mode 100644 index 0000000000000..f4f66b2111827 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/services/search/utils/map_result.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import type { ProductDocumentationAttributes } from '@kbn/product-doc-common'; +import type { DocSearchResult } from '../types'; + +export const mapResult = (docHit: SearchHit): DocSearchResult => { + return { + title: docHit._source!.content_title, + content: docHit._source!.content_body.text, + url: docHit._source!.url, + productName: docHit._source!.product_name, + }; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.ts new file mode 100644 index 0000000000000..d971561914ff1 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/ensure_up_to_date.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 type { Logger } from '@kbn/logging'; +import type { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import type { InternalServices } from '../types'; +import { isTaskCurrentlyRunningError } from './utils'; + +export const ENSURE_DOC_UP_TO_DATE_TASK_TYPE = 'ProductDocBase:EnsureUpToDate'; +export const ENSURE_DOC_UP_TO_DATE_TASK_ID = 'ProductDocBase:EnsureUpToDate'; + +export const registerEnsureUpToDateTaskDefinition = ({ + getServices, + taskManager, +}: { + getServices: () => InternalServices; + taskManager: TaskManagerSetupContract; +}) => { + taskManager.registerTaskDefinitions({ + [ENSURE_DOC_UP_TO_DATE_TASK_TYPE]: { + title: 'Ensure product documentation up to date task', + timeout: '10m', + maxAttempts: 3, + createTaskRunner: (context) => { + return { + async run() { + const { packageInstaller } = getServices(); + return packageInstaller.ensureUpToDate({}); + }, + }; + }, + stateSchemaByVersion: {}, + }, + }); +}; + +export const scheduleEnsureUpToDateTask = async ({ + taskManager, + logger, +}: { + taskManager: TaskManagerStartContract; + logger: Logger; +}) => { + try { + await taskManager.ensureScheduled({ + id: ENSURE_DOC_UP_TO_DATE_TASK_ID, + taskType: ENSURE_DOC_UP_TO_DATE_TASK_TYPE, + params: {}, + state: {}, + scope: ['productDoc'], + }); + + await taskManager.runSoon(ENSURE_DOC_UP_TO_DATE_TASK_ID); + + logger.info(`Task ${ENSURE_DOC_UP_TO_DATE_TASK_ID} scheduled to run soon`); + } catch (e) { + if (!isTaskCurrentlyRunningError(e)) { + throw e; + } + } + + return ENSURE_DOC_UP_TO_DATE_TASK_ID; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/tasks/index.ts b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/index.ts new file mode 100644 index 0000000000000..0b5833055fd8b --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; +import type { InternalServices } from '../types'; +import { registerEnsureUpToDateTaskDefinition } from './ensure_up_to_date'; +import { registerInstallAllTaskDefinition } from './install_all'; +import { registerUninstallAllTaskDefinition } from './uninstall_all'; + +export const registerTaskDefinitions = ({ + getServices, + taskManager, +}: { + getServices: () => InternalServices; + taskManager: TaskManagerSetupContract; +}) => { + registerEnsureUpToDateTaskDefinition({ getServices, taskManager }); + registerInstallAllTaskDefinition({ getServices, taskManager }); + registerUninstallAllTaskDefinition({ getServices, taskManager }); +}; + +export { scheduleEnsureUpToDateTask, ENSURE_DOC_UP_TO_DATE_TASK_ID } from './ensure_up_to_date'; +export { scheduleInstallAllTask, INSTALL_ALL_TASK_ID } from './install_all'; +export { scheduleUninstallAllTask, UNINSTALL_ALL_TASK_ID } from './uninstall_all'; +export { waitUntilTaskCompleted, getTaskStatus } from './utils'; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/tasks/install_all.ts b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/install_all.ts new file mode 100644 index 0000000000000..0d2cc48fb06bb --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/install_all.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 type { Logger } from '@kbn/logging'; +import type { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import type { InternalServices } from '../types'; +import { isTaskCurrentlyRunningError } from './utils'; + +export const INSTALL_ALL_TASK_TYPE = 'ProductDocBase:InstallAll'; +export const INSTALL_ALL_TASK_ID = 'ProductDocBase:InstallAll'; + +export const registerInstallAllTaskDefinition = ({ + getServices, + taskManager, +}: { + getServices: () => InternalServices; + taskManager: TaskManagerSetupContract; +}) => { + taskManager.registerTaskDefinitions({ + [INSTALL_ALL_TASK_TYPE]: { + title: 'Install all product documentation artifacts', + timeout: '10m', + maxAttempts: 3, + createTaskRunner: (context) => { + return { + async run() { + const { packageInstaller } = getServices(); + return packageInstaller.installAll({}); + }, + }; + }, + stateSchemaByVersion: {}, + }, + }); +}; + +export const scheduleInstallAllTask = async ({ + taskManager, + logger, +}: { + taskManager: TaskManagerStartContract; + logger: Logger; +}) => { + try { + await taskManager.ensureScheduled({ + id: INSTALL_ALL_TASK_ID, + taskType: INSTALL_ALL_TASK_TYPE, + params: {}, + state: {}, + scope: ['productDoc'], + }); + + await taskManager.runSoon(INSTALL_ALL_TASK_ID); + + logger.info(`Task ${INSTALL_ALL_TASK_ID} scheduled to run soon`); + } catch (e) { + if (!isTaskCurrentlyRunningError(e)) { + throw e; + } + } + + return INSTALL_ALL_TASK_ID; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/tasks/uninstall_all.ts b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/uninstall_all.ts new file mode 100644 index 0000000000000..6a88fec205ddd --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/uninstall_all.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 type { Logger } from '@kbn/logging'; +import type { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import type { InternalServices } from '../types'; +import { isTaskCurrentlyRunningError } from './utils'; + +export const UNINSTALL_ALL_TASK_TYPE = 'ProductDocBase:UninstallAll'; +export const UNINSTALL_ALL_TASK_ID = 'ProductDocBase:UninstallAll'; + +export const registerUninstallAllTaskDefinition = ({ + getServices, + taskManager, +}: { + getServices: () => InternalServices; + taskManager: TaskManagerSetupContract; +}) => { + taskManager.registerTaskDefinitions({ + [UNINSTALL_ALL_TASK_TYPE]: { + title: 'Uninstall all product documentation artifacts', + timeout: '10m', + maxAttempts: 3, + createTaskRunner: (context) => { + return { + async run() { + const { packageInstaller } = getServices(); + return packageInstaller.uninstallAll(); + }, + }; + }, + stateSchemaByVersion: {}, + }, + }); +}; + +export const scheduleUninstallAllTask = async ({ + taskManager, + logger, +}: { + taskManager: TaskManagerStartContract; + logger: Logger; +}) => { + try { + await taskManager.ensureScheduled({ + id: UNINSTALL_ALL_TASK_ID, + taskType: UNINSTALL_ALL_TASK_TYPE, + params: {}, + state: {}, + scope: ['productDoc'], + }); + + await taskManager.runSoon(UNINSTALL_ALL_TASK_ID); + + logger.info(`Task ${UNINSTALL_ALL_TASK_ID} scheduled to run soon`); + } catch (e) { + if (!isTaskCurrentlyRunningError(e)) { + throw e; + } + } + + return UNINSTALL_ALL_TASK_ID; +}; diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/tasks/utils.ts b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/utils.ts new file mode 100644 index 0000000000000..e32ea02a11b0c --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/tasks/utils.ts @@ -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 { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; + +export const getTaskStatus = async ({ + taskManager, + taskId, +}: { + taskManager: TaskManagerStartContract; + taskId: string; +}) => { + try { + const taskInstance = await taskManager.get(taskId); + return taskInstance.status; + } catch (e) { + // not found means the task was completed and the entry removed + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return 'not_scheduled'; + } + throw e; + } +}; + +export const isTaskCurrentlyRunningError = (err: Error): boolean => { + return err.message?.includes('currently running'); +}; + +export const waitUntilTaskCompleted = async ({ + taskManager, + taskId, + timeout = 120_000, + interval = 5_000, +}: { + taskManager: TaskManagerStartContract; + taskId: string; + timeout?: number; + interval?: number; +}): Promise => { + const start = Date.now(); + const max = start + timeout; + let now = start; + while (now < max) { + try { + const taskInstance = await taskManager.get(taskId); + const { status } = taskInstance; + if (status === 'idle' || status === 'claiming' || status === 'running') { + await sleep(interval); + now = Date.now(); + } else { + return; + } + } catch (e) { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + // not found means the task was completed and the entry removed + return; + } + } + } + + throw new Error(`Timeout waiting for task ${taskId} to complete.`); +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/x-pack/plugins/ai_infra/product_doc_base/server/types.ts b/x-pack/plugins/ai_infra/product_doc_base/server/types.ts new file mode 100644 index 0000000000000..f00943b696708 --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/server/types.ts @@ -0,0 +1,44 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; +import type { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import type { SearchApi } from './services/search'; +import type { ProductDocInstallClient } from './services/doc_install_status'; +import type { PackageInstaller } from './services/package_installer'; +import type { DocumentationManager, DocumentationManagerAPI } from './services/doc_manager'; + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ProductDocBaseSetupDependencies { + taskManager: TaskManagerSetupContract; +} + +export interface ProductDocBaseStartDependencies { + licensing: LicensingPluginStart; + taskManager: TaskManagerStartContract; +} + +export interface ProductDocBaseSetupContract {} + +export interface ProductDocBaseStartContract { + search: SearchApi; + management: DocumentationManagerAPI; +} + +export interface InternalServices { + logger: Logger; + installClient: ProductDocInstallClient; + packageInstaller: PackageInstaller; + documentationManager: DocumentationManager; + licensing: LicensingPluginStart; + taskManager: TaskManagerStartContract; +} diff --git a/x-pack/plugins/ai_infra/product_doc_base/tsconfig.json b/x-pack/plugins/ai_infra/product_doc_base/tsconfig.json new file mode 100644 index 0000000000000..9a2d1969556bf --- /dev/null +++ b/x-pack/plugins/ai_infra/product_doc_base/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": [ + "../../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*", + "scripts/**/*", + ".storybook/**/*" + ], + "exclude": ["target/**/*", ".storybook/**/*.js"], + "kbn_references": [ + "@kbn/core", + "@kbn/logging", + "@kbn/config-schema", + "@kbn/product-doc-common", + "@kbn/core-saved-objects-server", + "@kbn/utils", + "@kbn/core-http-browser", + "@kbn/logging-mocks", + "@kbn/licensing-plugin", + "@kbn/task-manager-plugin", + ] +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index 361d13e6d77f2..f693fa53c06cc 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -77,7 +77,7 @@ export class ObservabilityAIAssistantPlugin privileges: { all: { app: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID, 'kibana'], - api: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID, 'ai_assistant'], + api: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID, 'ai_assistant', 'manage_llm_product_doc'], catalogue: [OBSERVABILITY_AI_ASSISTANT_FEATURE_ID], savedObject: { all: [ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc index efc948503b0c0..957ca0272c087 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/kibana.jsonc @@ -32,7 +32,8 @@ "alerting", "features", "inference", - "logsDataAccess" + "logsDataAccess", + "llmTasks" ], "optionalPlugins": [ "cloud" diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/documentation.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/documentation.ts new file mode 100644 index 0000000000000..00072e0c79c48 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/documentation.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DocumentationProduct } from '@kbn/product-doc-common'; +import { FunctionVisibility } from '@kbn/observability-ai-assistant-plugin/common'; +import type { FunctionRegistrationParameters } from '.'; + +export const RETRIEVE_DOCUMENTATION_NAME = 'retrieve_elastic_doc'; + +export async function registerDocumentationFunction({ + functions, + resources, + pluginsStart: { llmTasks }, +}: FunctionRegistrationParameters) { + const isProductDocAvailable = (await llmTasks.retrieveDocumentationAvailable()) ?? false; + + functions.registerInstruction(({ availableFunctionNames }) => { + return availableFunctionNames.includes(RETRIEVE_DOCUMENTATION_NAME) + ? `When asked questions about the Elastic stack or products, You should use the ${RETRIEVE_DOCUMENTATION_NAME} function before answering, + to retrieve documentation related to the question. Consider that the documentation returned by the function + is always more up to date and accurate than any own internal knowledge you might have.` + : undefined; + }); + + functions.registerFunction( + { + name: RETRIEVE_DOCUMENTATION_NAME, + visibility: isProductDocAvailable + ? FunctionVisibility.AssistantOnly + : FunctionVisibility.Internal, + description: `Use this function to retrieve documentation about Elastic products. + You can retrieve documentation about the Elastic stack, such as Kibana and Elasticsearch, + or for Elastic solutions, such as Elastic Security, Elastic Observability or Elastic Enterprise Search + `, + parameters: { + type: 'object', + properties: { + query: { + description: `The query to use to retrieve documentation + Examples: + - "How to enable TLS for Elasticsearch?" + - "What is Kibana Lens?"`, + type: 'string' as const, + }, + product: { + description: `If specified, will filter the products to retrieve documentation for + Possible options are: + - "kibana": Kibana product + - "elasticsearch": Elasticsearch product + - "observability": Elastic Observability solution + - "security": Elastic Security solution + If not specified, will search against all products + `, + type: 'string' as const, + enum: Object.values(DocumentationProduct), + }, + }, + required: ['query'], + } as const, + }, + async ({ arguments: { query, product }, connectorId, useSimulatedFunctionCalling }) => { + const response = await llmTasks!.retrieveDocumentation({ + searchTerm: query, + products: product ? [product] : undefined, + max: 3, + connectorId, + request: resources.request, + functionCalling: useSimulatedFunctionCalling ? 'simulated' : 'native', + }); + + return { + content: { + documents: response.documents, + }, + }; + } + ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/index.ts index 7554164a55a69..ba876ad9457bc 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/functions/index.ts @@ -12,6 +12,7 @@ import { registerLensFunction } from './lens'; import { registerVisualizeESQLFunction } from './visualize_esql'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; import { registerChangesFunction } from './changes'; +import { registerDocumentationFunction } from './documentation'; export type FunctionRegistrationParameters = Omit< Parameters[0], @@ -24,4 +25,5 @@ export const registerFunctions = async (registrationParameters: FunctionRegistra registerVisualizeESQLFunction(registrationParameters); registerAlertsFunction(registrationParameters); registerChangesFunction(registrationParameters); + await registerDocumentationFunction(registrationParameters); }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts index fc39e0b7fb24e..a1196be6a829a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/server/types.ts @@ -37,6 +37,7 @@ import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plu import type { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; import type { InferenceServerStart, InferenceServerSetup } from '@kbn/inference-plugin/server'; import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/server'; +import type { LlmTasksPluginStart } from '@kbn/llm-tasks-plugin/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilityAIAssistantAppServerStart {} @@ -57,6 +58,7 @@ export interface ObservabilityAIAssistantAppPluginStartDependencies { serverless?: ServerlessPluginStart; inference: InferenceServerStart; logsDataAccess: LogsDataAccessPluginStart; + llmTasks: LlmTasksPluginStart; } export interface ObservabilityAIAssistantAppPluginSetupDependencies { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json index 6608799caaf61..e0a520fb574c7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json @@ -70,6 +70,8 @@ "@kbn/logs-data-access-plugin", "@kbn/ai-assistant-common", "@kbn/inference-common", + "@kbn/llm-tasks-plugin", + "@kbn/product-doc-common", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc index cda6fdf0192fa..c228f147dbfc3 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/kibana.jsonc @@ -21,10 +21,11 @@ "optionalPlugins": [ "home", "serverless", + "productDocBase" ], "requiredBundles": [ "kibanaReact", - "logsDataAccess", + "logsDataAccess" ] } } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/constants.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/constants.ts index a680da5ed3f93..3bfe3dff3f9f4 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/constants.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/constants.ts @@ -9,6 +9,9 @@ export const REACT_QUERY_KEYS = { GET_GENAI_CONNECTORS: 'get_genai_connectors', GET_KB_ENTRIES: 'get_kb_entries', GET_KB_USER_INSTRUCTIONS: 'get_kb_user_instructions', + GET_PRODUCT_DOC_STATUS: 'get_product_doc_status', + INSTALL_PRODUCT_DOC: 'install_product_doc', + UNINSTALL_PRODUCT_DOC: 'uninstall_product_doc', CREATE_KB_ENTRIES: 'create_kb_entry', IMPORT_KB_ENTRIES: 'import_kb_entry', }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts new file mode 100644 index 0000000000000..ef95d51f78d49 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_get_product_doc_status.ts @@ -0,0 +1,32 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { REACT_QUERY_KEYS } from '../constants'; +import { useKibana } from './use_kibana'; + +export function useGetProductDocStatus() { + const { productDocBase } = useKibana().services; + + const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({ + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], + queryFn: async () => { + return productDocBase!.installation.getStatus(); + }, + keepPreviousData: false, + refetchOnWindowFocus: false, + }); + + return { + status: data, + refetch, + isLoading, + isRefetching, + isSuccess, + isError, + }; +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts new file mode 100644 index 0000000000000..cb32efa7e3908 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_install_product_doc.ts @@ -0,0 +1,57 @@ +/* + * 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 { useMutation, useQueryClient } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import type { PerformInstallResponse } from '@kbn/product-doc-base-plugin/common/http_api/installation'; +import { REACT_QUERY_KEYS } from '../constants'; +import { useKibana } from './use_kibana'; + +type ServerError = IHttpFetchError; + +export function useInstallProductDoc() { + const { + productDocBase, + notifications: { toasts }, + } = useKibana().services; + const queryClient = useQueryClient(); + + return useMutation( + [REACT_QUERY_KEYS.INSTALL_PRODUCT_DOC], + () => { + return productDocBase!.installation.install(); + }, + { + onSuccess: () => { + toasts.addSuccess( + i18n.translate( + 'xpack.observabilityAiAssistantManagement.kb.installProductDoc.successNotification', + { + defaultMessage: 'The Elastic documentation was successfully installed', + } + ) + ); + + queryClient.invalidateQueries({ + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], + refetchType: 'all', + }); + }, + onError: (error) => { + toasts.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate( + 'xpack.observabilityAiAssistantManagement.kb.installProductDoc.errorNotification', + { + defaultMessage: 'Something went wrong while installing the Elastic documentation', + } + ), + }); + }, + } + ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts new file mode 100644 index 0000000000000..4aa3b5423faa1 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/hooks/use_uninstall_product_doc.ts @@ -0,0 +1,57 @@ +/* + * 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 { useMutation, useQueryClient } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import type { UninstallResponse } from '@kbn/product-doc-base-plugin/common/http_api/installation'; +import { REACT_QUERY_KEYS } from '../constants'; +import { useKibana } from './use_kibana'; + +type ServerError = IHttpFetchError; + +export function useUninstallProductDoc() { + const { + productDocBase, + notifications: { toasts }, + } = useKibana().services; + const queryClient = useQueryClient(); + + return useMutation( + [REACT_QUERY_KEYS.UNINSTALL_PRODUCT_DOC], + () => { + return productDocBase!.installation.uninstall(); + }, + { + onSuccess: () => { + toasts.addSuccess( + i18n.translate( + 'xpack.observabilityAiAssistantManagement.kb.uninstallProductDoc.successNotification', + { + defaultMessage: 'The Elastic documentation was successfully uninstalled', + } + ) + ); + + queryClient.invalidateQueries({ + queryKey: [REACT_QUERY_KEYS.GET_PRODUCT_DOC_STATUS], + refetchType: 'all', + }); + }, + onError: (error) => { + toasts.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate( + 'xpack.observabilityAiAssistantManagement.kb.uninstallProductDoc.errorNotification', + { + defaultMessage: 'Something went wrong while uninstalling the Elastic documentation', + } + ), + }); + }, + } + ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts index b7c6bb089663a..67b294a5fef36 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/plugin.ts @@ -10,6 +10,7 @@ import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/publ import type { ManagementSetup } from '@kbn/management-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import type { ProductDocBasePluginStart } from '@kbn/product-doc-base-plugin/public'; import type { ObservabilityAIAssistantPublicSetup, @@ -31,6 +32,7 @@ export interface SetupDependencies { export interface StartDependencies { observabilityAIAssistant: ObservabilityAIAssistantPublicStart; serverless?: ServerlessPluginStart; + productDocBase?: ProductDocBasePluginStart; } export interface ConfigSchema { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx new file mode 100644 index 0000000000000..668e363d071ee --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/product_doc_entry.tsx @@ -0,0 +1,171 @@ +/* + * 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, { useEffect, useState, useCallback, useMemo } from 'react'; +import { + EuiButton, + EuiDescribedFormGroup, + EuiFormRow, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../hooks/use_kibana'; +import { useGetProductDocStatus } from '../../../hooks/use_get_product_doc_status'; +import { useInstallProductDoc } from '../../../hooks/use_install_product_doc'; +import { useUninstallProductDoc } from '../../../hooks/use_uninstall_product_doc'; + +export function ProductDocEntry() { + const { overlays } = useKibana().services; + + const [isInstalled, setInstalled] = useState(true); + const [isInstalling, setInstalling] = useState(false); + + const { mutateAsync: installProductDoc } = useInstallProductDoc(); + const { mutateAsync: uninstallProductDoc } = useUninstallProductDoc(); + const { status, isLoading: isStatusLoading } = useGetProductDocStatus(); + + useEffect(() => { + if (status) { + setInstalled(status.overall === 'installed'); + } + }, [status]); + + const onClickInstall = useCallback(() => { + setInstalling(true); + installProductDoc().then( + () => { + setInstalling(false); + setInstalled(true); + }, + () => { + setInstalling(false); + setInstalled(false); + } + ); + }, [installProductDoc]); + + const onClickUninstall = useCallback(() => { + overlays + .openConfirm( + i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.productDocUninstallConfirmText', + { + defaultMessage: `Are you sure you want to uninstall the Elastic documentation?`, + } + ), + { + title: i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.productDocUninstallConfirmTitle', + { + defaultMessage: `Uninstalling Elastic documentation`, + } + ), + } + ) + .then((confirmed) => { + if (confirmed) { + uninstallProductDoc().then(() => { + setInstalling(false); + setInstalled(false); + }); + } + }); + }, [overlays, uninstallProductDoc]); + + const content = useMemo(() => { + if (isStatusLoading) { + return <>; + } + if (isInstalling) { + return ( + + + + + + + ); + } + if (isInstalled) { + return ( + + + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.installProductDocInstalledLabel', + { defaultMessage: 'Installed' } + )} + + + + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.uninstallProductDocButtonLabel', + { defaultMessage: 'Uninstall' } + )} + + + + ); + } + return ( + + + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.installProductDocButtonLabel', + { defaultMessage: 'Install' } + )} + + + + ); + }, [isInstalled, isInstalling, isStatusLoading, onClickInstall, onClickUninstall]); + + return ( + + {i18n.translate('xpack.observabilityAiAssistantManagement.settingsPage.productDocLabel', { + defaultMessage: 'Elastic documentation', + })} + + } + description={ +

+ + {i18n.translate('xpack.observabilityAiAssistantManagement.settingsPage.techPreview', { + defaultMessage: '[technical preview] ', + })} + + {i18n.translate( + 'xpack.observabilityAiAssistantManagement.settingsPage.productDocDescription', + { + defaultMessage: + "Install Elastic documentation to improve the assistant's efficiency.", + } + )} +

+ } + > + {content} +
+ ); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx index 831ba9ff58054..00c3fb76ae66a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/public/routes/components/settings_tab/settings_tab.tsx @@ -11,10 +11,12 @@ import { i18n } from '@kbn/i18n'; import { useAppContext } from '../../../hooks/use_app_context'; import { useKibana } from '../../../hooks/use_kibana'; import { UISettings } from './ui_settings'; +import { ProductDocEntry } from './product_doc_entry'; export function SettingsTab() { const { application: { navigateToApp }, + productDocBase, } = useKibana().services; const { config } = useAppContext(); @@ -108,6 +110,7 @@ export function SettingsTab() { + {productDocBase ? : undefined} ); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json index bc5cf69357dce..7b78d52c64806 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_management/tsconfig.json @@ -26,7 +26,8 @@ "@kbn/logs-data-access-plugin", "@kbn/core-plugins-browser", "@kbn/ai-assistant", - "@kbn/core-plugins-server" + "@kbn/core-plugins-server", + "@kbn/product-doc-base-plugin" ], "exclude": [ "target/**/*" diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 88ef256b353e6..a6bf7e7e9d5f2 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -49,6 +49,9 @@ export default function ({ getService }: FtrProviderContext) { 'Fleet-Usage-Logger', 'Fleet-Usage-Sender', 'ML:saved-objects-sync', + 'ProductDocBase:EnsureUpToDate', + 'ProductDocBase:InstallAll', + 'ProductDocBase:UninstallAll', 'SLO:ORPHAN_SUMMARIES-CLEANUP-TASK', 'Synthetics:Clean-Up-Package-Policies', 'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects', diff --git a/yarn.lock b/yarn.lock index c02ae16affe05..b7ef7370c62b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5550,6 +5550,10 @@ version "0.0.0" uid "" +"@kbn/llm-tasks-plugin@link:x-pack/plugins/ai_infra/llm_tasks": + version "0.0.0" + uid "" + "@kbn/locator-examples-plugin@link:examples/locator_examples": version "0.0.0" uid "" @@ -6026,6 +6030,14 @@ version "0.0.0" uid "" +"@kbn/product-doc-base-plugin@link:x-pack/plugins/ai_infra/product_doc_base": + version "0.0.0" + uid "" + +"@kbn/product-doc-common@link:x-pack/packages/ai-infra/product-doc-common": + version "0.0.0" + uid "" + "@kbn/profiling-data-access-plugin@link:x-pack/plugins/observability_solution/profiling_data_access": version "0.0.0" uid "" From 086a4485ac07c0d0f492b7e20186c44368e58764 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 04:12:35 +1100 Subject: [PATCH 20/42] [8.x] Fix flashing banner when creating pipeline (#199786) (#200760) # Backport This will backport the following commits from `main` to `8.x`: - [Fix flashing banner when creating pipeline (#199786)](https://github.com/elastic/kibana/pull/199786) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Sonia Sanz Vivas --- .../application/components/pipeline_form/pipeline_form.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 80c43af7b7d4d..b70e767de29b4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -175,7 +175,7 @@ export const PipelineForm: React.FunctionComponent = ({
{/* Request error */} @@ -244,7 +244,6 @@ export const PipelineForm: React.FunctionComponent = ({ - {/* ES request flyout */} {isRequestVisible ? ( Date: Wed, 20 Nov 2024 04:19:54 +1100 Subject: [PATCH 21/42] [8.x] [APM] Unskip feature flag test (#200596) (#200763) # Backport This will backport the following commits from `main` to `8.x`: - [[APM] Unskip feature flag test (#200596)](https://github.com/elastic/kibana/pull/200596) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Carlos Crespo --- .../apm_api_integration/common/apm_api_supertest.ts | 2 +- .../observability/apm_api_integration/feature_flags.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts index 19f102335d99f..3b05b5d08d29d 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/common/apm_api_supertest.ts @@ -46,7 +46,7 @@ export function createApmApiClient(st: supertest.Agent) { .set('Content-type', 'multipart/form-data'); for (const field of fields) { - await formDataRequest.field(field[0], field[1]); + void formDataRequest.field(field[0], field[1]); } res = await formDataRequest; diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts index 88096d6258e27..15af0d68d8db7 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/feature_flags.ts @@ -77,9 +77,7 @@ export default function ({ getService }: APMFtrContextProvider) { const svlUserManager = getService('svlUserManager'); const svlCommonApi = getService('svlCommonApi'); - // https://github.com/elastic/kibana/pull/190690 - // skipping since "rejects requests to list source maps" fails with 400 - describe.skip('apm feature flags', () => { + describe('apm feature flags', () => { let roleAuthc: RoleCredentials; let internalReqHeader: InternalRequestHeader; From 106dfd6c5930a2a5386fa2afb438655ce53b767b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 04:22:51 +1100 Subject: [PATCH 22/42] [8.x] Obs AI Assistant Fetch user instructions using user_id (#200137) (#200759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Obs AI Assistant Fetch user instructions using user_id (#200137)](https://github.com/elastic/kibana/pull/200137) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Arturo Lidueña --- .../server/service/util/get_access_query.ts | 4 +- .../tests/conversations/index.spec.ts | 147 ++++++++++-------- 2 files changed, 89 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_access_query.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_access_query.ts index f6f099d5200a8..6b654731a264b 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_access_query.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/util/get_access_query.ts @@ -19,7 +19,9 @@ export function getAccessQuery({ bool: { should: [ { term: { public: true } }, - ...(user ? [{ term: { 'user.name': user.name } }] : []), + ...(user + ? [{ term: user.id ? { 'user.id': user.id } : { 'user.name': user.name } }] + : []), ], minimum_should_match: 1, }, diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts index de780d2f46b0e..6d509a77b42f7 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts @@ -9,6 +9,8 @@ import expect from '@kbn/expect'; import { MessageRole } from '@kbn/observability-ai-assistant-plugin/common'; import { ChatFeedback } from '@kbn/observability-ai-assistant-plugin/public/analytics/schemas/chat_feedback'; import { pick } from 'lodash'; +import { parse as parseCookie } from 'tough-cookie'; +import { kbnTestConfig } from '@kbn/test'; import { createLlmProxy, isFunctionTitleRequest, @@ -17,12 +19,15 @@ import { import { interceptRequest } from '../../common/intercept_request'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { editor } from '../../../observability_ai_assistant_api_integration/common/users/users'; + export default function ApiTest({ getService, getPageObjects }: FtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); const ui = getService('observabilityAIAssistantUI'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const retry = getService('retry'); const log = getService('log'); const telemetry = getService('kibana_ebt_ui'); @@ -35,6 +40,20 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte const flyoutService = getService('flyout'); + async function login(username: string, password: string | undefined) { + const response = await supertestWithoutAuth + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password }, + }) + .expect(200); + return parseCookie(response.headers['set-cookie'][0])!; + } + async function deleteConversations() { const response = await observabilityAIAssistantAPIClient.editor({ endpoint: 'POST /internal/observability_ai_assistant/conversations', @@ -66,78 +85,84 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte } async function createOldConversation() { - await observabilityAIAssistantAPIClient.editor({ - endpoint: 'POST /internal/observability_ai_assistant/conversation', - params: { - body: { - conversation: { - messages: [ - { - '@timestamp': '2024-04-18T14:28:50.118Z', - message: { - role: MessageRole.System, - content: - 'You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems. You can help them visualise and analyze data, investigate their systems, perform root cause analysis or identify optimisation opportunities.\n\nIt\'s very important to not assume what the user is meaning. Ask them for clarification if needed.\n\nIf you are unsure about which function should be used and with what arguments, ask the user for clarification or confirmation.\n\nIn KQL ("kqlFilter")) escaping happens with double quotes, not single quotes. Some characters that need escaping are: \':()\\ /". Always put a field value in double quotes. Best: service.name:"opbeans-go". Wrong: service.name:opbeans-go. This is very important!\n\nYou can use Github-flavored Markdown in your responses. If a function returns an array, consider using a Markdown table to format the response.\n\nNote that ES|QL (the Elasticsearch Query Language which is a new piped language) is the preferred query language.\n\nYou MUST use the "query" function when the user wants to:\n- visualize data\n- run any arbitrary query\n- breakdown or filter ES|QL queries that are displayed on the current page\n- convert queries from another language to ES|QL\n- asks general questions about ES|QL\n\nDO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries or explain anything about the ES|QL query language yourself.\nDO NOT UNDER ANY CIRCUMSTANCES try to correct an ES|QL query yourself - always use the "query" function for this.\n\nDO NOT UNDER ANY CIRCUMSTANCES USE ES|QL syntax (`service.name == "foo"`) with "kqlFilter" (`service.name:"foo"`).\n\nEven if the "context" function was used before that, follow it up with the "query" function. If a query fails, do not attempt to correct it yourself. Again you should call the "query" function,\neven if it has been called before.\n\nWhen the "visualize_query" function has been called, a visualization has been displayed to the user. DO NOT UNDER ANY CIRCUMSTANCES follow up a "visualize_query" function call with your own visualization attempt.\nIf the "execute_query" function has been called, summarize these results for the user. The user does not see a visualization in this case.\n\nYou MUST use the get_dataset_info function function before calling the "query" or "changes" function.\n\nIf a function requires an index, you MUST use the results from the dataset info functions.\n\n\n\nThe user is able to change the language which they want you to reply in on the settings page of the AI Assistant for Observability, which can be found in the Stack Management app under the option AI Assistants.\nIf the user asks how to change the language, reply in the same language the user asked in.You do not have a working memory. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base.', - }, + const { password } = kbnTestConfig.getUrlParts(); + const sessionCookie = await login(editor.username, password); + const endpoint = '/internal/observability_ai_assistant/conversation'; + const cookie = sessionCookie.cookieString(); + const params = { + body: { + conversation: { + messages: [ + { + '@timestamp': '2024-04-18T14:28:50.118Z', + message: { + role: MessageRole.System, + content: + 'You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems. You can help them visualise and analyze data, investigate their systems, perform root cause analysis or identify optimisation opportunities.\n\nIt\'s very important to not assume what the user is meaning. Ask them for clarification if needed.\n\nIf you are unsure about which function should be used and with what arguments, ask the user for clarification or confirmation.\n\nIn KQL ("kqlFilter")) escaping happens with double quotes, not single quotes. Some characters that need escaping are: \':()\\ /". Always put a field value in double quotes. Best: service.name:"opbeans-go". Wrong: service.name:opbeans-go. This is very important!\n\nYou can use Github-flavored Markdown in your responses. If a function returns an array, consider using a Markdown table to format the response.\n\nNote that ES|QL (the Elasticsearch Query Language which is a new piped language) is the preferred query language.\n\nYou MUST use the "query" function when the user wants to:\n- visualize data\n- run any arbitrary query\n- breakdown or filter ES|QL queries that are displayed on the current page\n- convert queries from another language to ES|QL\n- asks general questions about ES|QL\n\nDO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries or explain anything about the ES|QL query language yourself.\nDO NOT UNDER ANY CIRCUMSTANCES try to correct an ES|QL query yourself - always use the "query" function for this.\n\nDO NOT UNDER ANY CIRCUMSTANCES USE ES|QL syntax (`service.name == "foo"`) with "kqlFilter" (`service.name:"foo"`).\n\nEven if the "context" function was used before that, follow it up with the "query" function. If a query fails, do not attempt to correct it yourself. Again you should call the "query" function,\neven if it has been called before.\n\nWhen the "visualize_query" function has been called, a visualization has been displayed to the user. DO NOT UNDER ANY CIRCUMSTANCES follow up a "visualize_query" function call with your own visualization attempt.\nIf the "execute_query" function has been called, summarize these results for the user. The user does not see a visualization in this case.\n\nYou MUST use the get_dataset_info function function before calling the "query" or "changes" function.\n\nIf a function requires an index, you MUST use the results from the dataset info functions.\n\n\n\nThe user is able to change the language which they want you to reply in on the settings page of the AI Assistant for Observability, which can be found in the Stack Management app under the option AI Assistants.\nIf the user asks how to change the language, reply in the same language the user asked in.You do not have a working memory. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base.', }, - { - '@timestamp': '2024-04-18T14:29:01.615Z', - message: { - content: 'What are SLOs?', - role: MessageRole.User, - }, - }, - { - '@timestamp': '2024-04-18T14:29:01.876Z', - message: { - role: MessageRole.Assistant, - content: '', - function_call: { - name: 'context', - arguments: '{}', - trigger: MessageRole.Assistant, - }, - }, + }, + { + '@timestamp': '2024-04-18T14:29:01.615Z', + message: { + content: 'What are SLOs?', + role: MessageRole.User, }, - { - '@timestamp': '2024-04-18T14:29:01.876Z', - message: { - content: - '{"screen_description":"The user is looking at http://localhost:5601/ftw/app/observabilityAIAssistant/conversations/new. The current time range is 2024-04-18T14:13:49.815Z - 2024-04-18T14:28:49.815Z.","learnings":[]}', + }, + { + '@timestamp': '2024-04-18T14:29:01.876Z', + message: { + role: MessageRole.Assistant, + content: '', + function_call: { name: 'context', - role: MessageRole.User, + arguments: '{}', + trigger: MessageRole.Assistant, }, }, - { - '@timestamp': '2024-04-18T14:29:22.945Z', - message: { - content: - "SLOs, or Service Level Objectives, are a key part of the Site Reliability Engineering (SRE) methodology. They are a target value or range of values for a service level that is measured by an SLI (Service Level Indicator). \n\nAn SLO is a goal for how often and how much you want your service to meet a particular SLI. For example, you might have an SLO that your service should be up and running 99.9% of the time. \n\nSLOs are important because they set clear expectations for your team and your users about the level of service you aim to provide. They also help you make decisions about where to focus your efforts: if you're meeting your SLOs, you can focus on building new features; if you're not meeting your SLOs, you need to focus on improving reliability. \n\nIn Elastic Observability, you can define and monitor your SLOs to ensure your services are meeting their targets.", - function_call: { - name: '', - arguments: '', - trigger: MessageRole.Assistant, - }, - role: MessageRole.Assistant, - }, + }, + { + '@timestamp': '2024-04-18T14:29:01.876Z', + message: { + content: + '{"screen_description":"The user is looking at http://localhost:5601/ftw/app/observabilityAIAssistant/conversations/new. The current time range is 2024-04-18T14:13:49.815Z - 2024-04-18T14:28:49.815Z.","learnings":[]}', + name: 'context', + role: MessageRole.User, }, - ], - conversation: { - title: 'My old conversation', - token_count: { - completion: 1, - prompt: 1, - total: 2, + }, + { + '@timestamp': '2024-04-18T14:29:22.945Z', + message: { + content: + "SLOs, or Service Level Objectives, are a key part of the Site Reliability Engineering (SRE) methodology. They are a target value or range of values for a service level that is measured by an SLI (Service Level Indicator). \n\nAn SLO is a goal for how often and how much you want your service to meet a particular SLI. For example, you might have an SLO that your service should be up and running 99.9% of the time. \n\nSLOs are important because they set clear expectations for your team and your users about the level of service you aim to provide. They also help you make decisions about where to focus your efforts: if you're meeting your SLOs, you can focus on building new features; if you're not meeting your SLOs, you need to focus on improving reliability. \n\nIn Elastic Observability, you can define and monitor your SLOs to ensure your services are meeting their targets.", + function_call: { + name: '', + arguments: '', + trigger: MessageRole.Assistant, + }, + role: MessageRole.Assistant, }, }, - '@timestamp': '2024-04-18T14:29:22.948', - public: false, - numeric_labels: {}, - labels: {}, + ], + conversation: { + title: 'My old conversation', + token_count: { + completion: 1, + prompt: 1, + total: 2, + }, }, + '@timestamp': '2024-04-18T14:29:22.948', + public: false, + numeric_labels: {}, + labels: {}, }, }, - }); + }; + await supertestWithoutAuth + .post(endpoint) + .set('kbn-xsrf', 'xxx') + .set('Cookie', cookie) + .send(params.body); } describe('Conversations', () => { From e942bb00476a60a1d98f30a7378ecfba9f0879be Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 04:47:14 +1100 Subject: [PATCH 23/42] [8.x] [Security Solution][Bidirectional Integrations Banner][Crowdstrike][SentinelOne] Banner for bidirectional integrations (#200625) (#200768) # Backport This will backport the following commits from `main` to `8.x`: - [[Security Solution][Bidirectional Integrations Banner][Crowdstrike][SentinelOne] Banner for bidirectional integrations (#200625)](https://github.com/elastic/kibana/pull/200625) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Candace Park <56409205+parkiino@users.noreply.github.com> --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + ...idirectional_integrations_callout.test.tsx | 50 ++++++++++++++ .../bidirectional_integrations_callout.tsx | 66 +++++++++++++++++++ .../epm/screens/detail/components/index.tsx | 2 + .../epm/screens/detail/overview/overview.tsx | 39 ++++++++++- 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.tsx diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index dc135a5b21d74..4297d945f8804 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -472,6 +472,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D securitySolution: { artifactControl: `${SECURITY_SOLUTION_DOCS}artifact-control.html`, avcResults: `${ELASTIC_WEBSITE_URL}blog/elastic-av-comparatives-business-security-test`, + bidirectionalIntegrations: `${SECURITY_SOLUTION_DOCS}third-party-actions.html`, trustedApps: `${SECURITY_SOLUTION_DOCS}trusted-apps-ov.html`, eventFilters: `${SECURITY_SOLUTION_DOCS}event-filters.html`, blocklist: `${SECURITY_SOLUTION_DOCS}blocklist.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 7fc395ff8a90b..4c629ee2d68f7 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -342,6 +342,7 @@ export interface DocLinks { readonly aiAssistant: string; readonly artifactControl: string; readonly avcResults: string; + readonly bidirectionalIntegrations: string; readonly trustedApps: string; readonly eventFilters: string; readonly eventMerging: string; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.test.tsx new file mode 100644 index 0000000000000..6dd9ff0e5f9b3 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { type RenderResult } from '@testing-library/react'; + +import { createFleetTestRendererMock } from '../../../../../../../mock'; + +import { + BidirectionalIntegrationsBanner, + type BidirectionalIntegrationsBannerProps, +} from './bidirectional_integrations_callout'; + +jest.mock('react-use/lib/useLocalStorage'); + +describe('BidirectionalIntegrationsBanner', () => { + let formProps: BidirectionalIntegrationsBannerProps; + let renderResult: RenderResult; + + beforeEach(() => { + formProps = { + onDismiss: jest.fn(), + }; + + const renderer = createFleetTestRendererMock(); + + renderResult = renderer.render(); + }); + + it('should render bidirectional integrations banner', () => { + expect(renderResult.getByTestId('bidirectionalIntegrationsCallout')).toBeInTheDocument(); + }); + + it('should contain a link to documentation', () => { + const docLink = renderResult.getByTestId('bidirectionalIntegrationDocLink'); + + expect(docLink).toBeInTheDocument(); + expect(docLink.getAttribute('href')).toContain('third-party-actions.html'); + }); + + it('should call `onDismiss` callback when user clicks dismiss', () => { + renderResult.getByTestId('euiDismissCalloutButton').click(); + + expect(formProps.onDismiss).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.tsx new file mode 100644 index 0000000000000..5f8375d2e7baa --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/bidirectional_integrations_callout.tsx @@ -0,0 +1,66 @@ +/* + * 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 } from 'react'; +import styled from 'styled-components'; +import { EuiCallOut, EuiLink, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +const AccentCallout = styled(EuiCallOut)` + .euiCallOutHeader__title { + color: ${(props) => props.theme.eui.euiColorAccent}; + } + background-color: ${(props) => props.theme.eui.euiPanelBackgroundColorModifiers.accent}; +`; + +export interface BidirectionalIntegrationsBannerProps { + onDismiss: () => void; +} +export const BidirectionalIntegrationsBanner = memo( + ({ onDismiss }) => { + const { docLinks } = useKibana().services; + + const bannerTitle = ( + + + + ); + + return ( + + + + + ), + }} + /> + + ); + } +); +BidirectionalIntegrationsBanner.displayName = 'BidirectionalIntegrationsBanner'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx index 4aa1a543897c9..cb1fc1b396e42 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx @@ -6,7 +6,9 @@ */ export { BackLink } from './back_link'; export { AddIntegrationButton } from './add_integration_button'; +export { CloudPostureThirdPartySupportCallout } from './cloud_posture_third_party_support_callout'; export { UpdateIcon } from './update_icon'; export { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; export { IconPanel, LoadingIconPanel } from './icon_panel'; export { KeepPoliciesUpToDateSwitch } from './keep_policies_up_to_date_switch'; +export { BidirectionalIntegrationsBanner } from './bidirectional_integrations_callout'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx index e96e74b1bb96c..83cde5745071a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx @@ -42,7 +42,10 @@ import { SideBarColumn } from '../../../components/side_bar_column'; import type { FleetStartServices } from '../../../../../../../plugin'; -import { CloudPostureThirdPartySupportCallout } from '../components/cloud_posture_third_party_support_callout'; +import { + CloudPostureThirdPartySupportCallout, + BidirectionalIntegrationsBanner, +} from '../components'; import { Screenshots } from './screenshots'; import { Readme } from './readme'; @@ -172,6 +175,8 @@ export const OverviewPage: React.FC = memo( const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId); const isPrerelease = isPackagePrerelease(packageInfo.version); const isElasticDefend = packageInfo.name === 'endpoint'; + const isSentinelOne = packageInfo.name === 'sentinel_one'; + const isCrowdStrike = packageInfo.name === 'crowdstrike'; const [markdown, setMarkdown] = useState(undefined); const [selectedItemId, setSelectedItem] = useState(undefined); const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false); @@ -296,11 +301,27 @@ export const OverviewPage: React.FC = memo( const [showAVCBanner, setShowAVCBanner] = useState( storage.get('securitySolution.showAvcBanner') ?? true ); - const onBannerDismiss = useCallback(() => { + const [showCSResponseSupportBanner, setShowCSResponseSupportBanner] = useState( + storage.get('fleet.showCSResponseSupportBanner') ?? true + ); + const [showSOReponseSupportBanner, setShowSOResponseSupportBanner] = useState( + storage.get('fleet.showSOReponseSupportBanner') ?? true + ); + const onAVCBannerDismiss = useCallback(() => { setShowAVCBanner(false); storage.set('securitySolution.showAvcBanner', false); }, [storage]); + const onCSResponseSupportBannerDismiss = useCallback(() => { + setShowCSResponseSupportBanner(false); + storage.set('fleet.showCSResponseSupportBanner', false); + }, [storage]); + + const onSOResponseSupportBannerDismiss = useCallback(() => { + setShowSOResponseSupportBanner(false); + storage.set('fleet.showSOReponseSupportBanner', false); + }, [storage]); + return ( @@ -317,7 +338,19 @@ export const OverviewPage: React.FC = memo( {isUnverified && } {useIsStillYear2024() && isElasticDefend && showAVCBanner && ( <> - + + + + )} + {isCrowdStrike && showCSResponseSupportBanner && ( + <> + + + + )} + {isSentinelOne && showSOReponseSupportBanner && ( + <> + )} From 5c707377dca5efa4168cdae3c871bb5fad254c42 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 04:50:48 +1100 Subject: [PATCH 24/42] [8.x] [ML] Single Metric Viewer embeddable: fix job refetch on error (#199726) (#200767) # Backport This will backport the following commits from `main` to `8.x`: - [[ML] Single Metric Viewer embeddable: fix job refetch on error (#199726)](https://github.com/elastic/kibana/pull/199726) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Melissa Alvarez --- .../single_metric_viewer.tsx | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx b/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx index 9d2ba88493774..39f22e2e988db 100644 --- a/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx +++ b/x-pack/plugins/ml/public/shared_components/single_metric_viewer/single_metric_viewer.tsx @@ -20,6 +20,7 @@ import type { MlJob, MlJobStats } from '@elastic/elasticsearch/lib/api/types'; import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; import type { TimeRangeBounds } from '@kbn/ml-time-buckets'; import usePrevious from 'react-use/lib/usePrevious'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { tz } from 'moment'; import { pick, throttle } from 'lodash'; import type { MlDependencies } from '../../application/app'; @@ -43,10 +44,17 @@ interface AppStateZoom { to?: string; } -const errorMessage = i18n.translate('xpack.ml.singleMetricViewerEmbeddable.errorMessage"', { +const basicErrorMessage = i18n.translate('xpack.ml.singleMetricViewerEmbeddable.errorMessage"', { defaultMessage: 'Unable to load the ML single metric viewer data', }); +const jobNotFoundErrorMessage = i18n.translate( + 'xpack.ml.singleMetricViewerEmbeddable.jobNotFoundErrorMessage"', + { + defaultMessage: 'No known job with the selected id', + } +); + export type SingleMetricViewerSharedComponent = FC; /** @@ -72,7 +80,7 @@ export interface SingleMetricViewerProps { */ lastRefresh?: number; onRenderComplete?: () => void; - onError?: (error: Error) => void; + onError?: (error?: Error) => void; onForecastIdChange?: (forecastId: string | undefined) => void; uuid: string; } @@ -112,6 +120,7 @@ const SingleMetricViewerWrapper: FC = ({ const [selectedJobWrapper, setSelectedJobWrapper] = useState< { job: MlJob; stats: MlJobStats } | undefined >(); + const [errorEncountered, setErrorEncountered] = useState(); const isMounted = useMountedState(); const { mlApi, mlTimeSeriesExplorerService, toastNotificationService } = mlServices; @@ -125,6 +134,16 @@ const SingleMetricViewerWrapper: FC = ({ const previousRefresh = usePrevious(lastRefresh ?? 0); + useEffect( + function resetErrorOnJobChange() { + // Calling onError to clear any previous error + setErrorEncountered(undefined); + onError?.(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedJobId] + ); + useEffect( function setUpSelectedJob() { async function fetchSelectedJob() { @@ -136,19 +155,26 @@ const SingleMetricViewerWrapper: FC = ({ ]); setSelectedJobWrapper({ job: jobs[0], stats: jobStats[0] }); } catch (e) { + const error = extractErrorProperties(e); + // Could get 404 because job has been deleted and also avoid infinite refetches on any error + setErrorEncountered(error.statusCode); if (onError) { - onError(new Error(errorMessage)); + onError( + new Error(errorEncountered === 404 ? jobNotFoundErrorMessage : basicErrorMessage) + ); } } } } - if (isMounted() === false) { + if (isMounted() === false || errorEncountered !== undefined) { return; } fetchSelectedJob(); }, - [selectedJobId, mlApi, isMounted, onError] + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedJobId, isMounted, errorEncountered] ); + // eslint-disable-next-line react-hooks/exhaustive-deps const resizeHandler = useCallback( throttle((e: { width: number; height: number }) => { From 1a6efcc3d4411188b99be6f3cee464f280b89cbb Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 19 Nov 2024 13:14:37 -0500 Subject: [PATCH 25/42] [8.x] [Fleet] Use metering API in serverless (#200063) (#200663) # Backport This will backport the following commits from `main` to `8.x`: - [[Fleet] Use metering API in serverless (#200063)](https://github.com/elastic/kibana/pull/200063) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- config/serverless.yml | 1 + x-pack/plugins/fleet/common/types/index.ts | 1 + x-pack/plugins/fleet/server/config.ts | 3 + .../server/routes/data_streams/handlers.ts | 50 +++++-- .../fleet/server/services/data_streams.ts | 21 +++ .../test_suites/security/fleet/fleet.ts | 126 ++++++++++++++++++ 6 files changed, 193 insertions(+), 9 deletions(-) diff --git a/config/serverless.yml b/config/serverless.yml index 7eca5cae871c3..27580c3ce3da2 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -7,6 +7,7 @@ xpack.fleet.internal.disableILMPolicies: true xpack.fleet.internal.activeAgentsSoftLimit: 25000 xpack.fleet.internal.onlyAllowAgentUpgradeToKnownVersions: true xpack.fleet.internal.retrySetupOnBoot: true +xpack.fleet.internal.useMeteringApi: true ## Fine-tune the feature privileges. xpack.features.overrides: diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 647a8b917d0c0..f77ea38dc7b5f 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -65,6 +65,7 @@ export interface FleetConfigType { disableBundledPackagesCache?: boolean; }; internal?: { + useMeteringApi?: boolean; disableILMPolicies: boolean; fleetServerStandalone: boolean; onlyAllowAgentUpgradeToKnownVersions: boolean; diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 746498221de55..8035f389db488 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -203,6 +203,9 @@ export const config: PluginConfigDescriptor = { internal: schema.maybe( schema.object({ + useMeteringApi: schema.boolean({ + defaultValue: false, + }), disableILMPolicies: schema.boolean({ defaultValue: false, }), diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index f9021f344c69b..7cbc9d9274032 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { Dictionary } from 'lodash'; import { keyBy, keys, merge } from 'lodash'; import type { RequestHandler } from '@kbn/core/server'; import pMap from 'p-map'; @@ -13,9 +14,13 @@ import { KibanaSavedObjectType } from '../../../common/types'; import type { GetDataStreamsResponse } from '../../../common/types'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; import { defaultFleetErrorHandler } from '../../errors'; +import type { MeteringStats } from '../../services/data_streams'; import { dataStreamService } from '../../services/data_streams'; import { getDataStreamsQueryMetadata } from './get_data_streams_query_metadata'; +import type { IndicesDataStreamsStatsDataStreamsStatsItem } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ByteSizeValue } from '@kbn/config-schema'; +import { appContextService } from '../../services'; const MANAGED_BY = 'fleet'; const LEGACY_MANAGED_BY = 'ingest-manager'; @@ -51,10 +56,22 @@ export const getListHandler: RequestHandler = async (context, request, response) }; try { + const useMeteringApi = appContextService.getConfig()?.internal?.useMeteringApi; + // Get matching data streams, their stats, and package SOs - const [dataStreamsInfo, dataStreamStats, packageSavedObjects] = await Promise.all([ + const [ + dataStreamsInfo, + dataStreamStatsOrUndefined, + dataStreamMeteringStatsorUndefined, + packageSavedObjects, + ] = await Promise.all([ dataStreamService.getAllFleetDataStreams(esClient), - dataStreamService.getAllFleetDataStreamsStats(esClient), + useMeteringApi + ? undefined + : dataStreamService.getAllFleetDataStreamsStats(elasticsearch.client.asSecondaryAuthUser), + useMeteringApi + ? dataStreamService.getAllFleetMeteringStats(elasticsearch.client.asSecondaryAuthUser) + : undefined, getPackageSavedObjects(savedObjects.client), ]); @@ -67,13 +84,24 @@ export const getListHandler: RequestHandler = async (context, request, response) const dataStreamsInfoByName = keyBy(filteredDataStreamsInfo, 'name'); - const filteredDataStreamsStats = dataStreamStats.filter( - (dss) => !!dataStreamsInfoByName[dss.data_stream] - ); - const dataStreamsStatsByName = keyBy(filteredDataStreamsStats, 'data_stream'); + let dataStreamsStatsByName: Dictionary = {}; + if (dataStreamStatsOrUndefined) { + const filteredDataStreamsStats = dataStreamStatsOrUndefined.filter( + (dss) => !!dataStreamsInfoByName[dss.data_stream] + ); + dataStreamsStatsByName = keyBy(filteredDataStreamsStats, 'data_stream'); + } + let dataStreamsMeteringStatsByName: Dictionary = {}; + if (dataStreamMeteringStatsorUndefined) { + dataStreamsMeteringStatsByName = keyBy(dataStreamMeteringStatsorUndefined, 'name'); + } // Combine data stream info - const dataStreams = merge(dataStreamsInfoByName, dataStreamsStatsByName); + const dataStreams = merge( + dataStreamsInfoByName, + dataStreamsStatsByName, + dataStreamsMeteringStatsByName + ); const dataStreamNames = keys(dataStreams); // Map package SOs @@ -132,10 +160,14 @@ export const getListHandler: RequestHandler = async (context, request, response) package: dataStream._meta?.package?.name || '', package_version: '', last_activity_ms: dataStream.maximum_timestamp, // overridden below if maxIngestedTimestamp agg returns a result - size_in_bytes: dataStream.store_size_bytes, + size_in_bytes: dataStream.store_size_bytes || dataStream.size_in_bytes, // `store_size` should be available from ES due to ?human=true flag // but fallback to bytes just in case - size_in_bytes_formatted: dataStream.store_size || `${dataStream.store_size_bytes}b`, + size_in_bytes_formatted: + dataStream.store_size || + new ByteSizeValue( + dataStream.store_size_bytes || dataStream.size_in_bytes || 0 + ).toString(), dashboards: [], serviceDetails: null, }; diff --git a/x-pack/plugins/fleet/server/services/data_streams.ts b/x-pack/plugins/fleet/server/services/data_streams.ts index 6dd60a4e0be1e..68076c3309ab2 100644 --- a/x-pack/plugins/fleet/server/services/data_streams.ts +++ b/x-pack/plugins/fleet/server/services/data_streams.ts @@ -10,6 +10,15 @@ import type { ElasticsearchClient } from '@kbn/core/server'; const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*,synthetics-*-*,profiling-*'; +export interface MeteringStatsResponse { + datastreams: MeteringStats[]; +} +export interface MeteringStats { + name: string; + num_docs: number; + size_in_bytes: number; +} + class DataStreamService { public async getAllFleetDataStreams(esClient: ElasticsearchClient) { const { data_streams: dataStreamsInfo } = await esClient.indices.getDataStream({ @@ -19,6 +28,18 @@ class DataStreamService { return dataStreamsInfo; } + public async getAllFleetMeteringStats(esClient: ElasticsearchClient) { + const res = await esClient.transport.request({ + path: `/_metering/stats`, + method: 'GET', + querystring: { + human: true, + }, + }); + + return res.datastreams ?? []; + } + public async getAllFleetDataStreamsStats(esClient: ElasticsearchClient) { const { data_streams: dataStreamStats } = await esClient.indices.dataStreamsStats({ name: DATA_STREAM_INDEX_PATTERN, diff --git a/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts b/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts index 45db1f2e530dc..d812e43dfa62a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts @@ -17,6 +17,7 @@ export default function (ctx: FtrProviderContext) { const svlCommonApi = ctx.getService('svlCommonApi'); const supertestWithoutAuth = ctx.getService('supertestWithoutAuth'); const svlUserManager = ctx.getService('svlUserManager'); + const es = ctx.getService('es'); let roleAuthc: RoleCredentials; describe('fleet', function () { @@ -112,5 +113,130 @@ export default function (ctx: FtrProviderContext) { }); expect(status).toBe(200); }); + + describe('datastreams API', () => { + before(async () => { + await es.index({ + refresh: 'wait_for', + index: 'logs-nginx.access-default', + document: { + agent: { + name: 'docker-fleet-agent', + id: 'ef5e274d-4b53-45e6-943a-a5bcf1a6f523', + ephemeral_id: '34369a4a-4f24-4a39-9758-85fc2429d7e2', + type: 'filebeat', + version: '8.5.0', + }, + nginx: { + access: { + remote_ip_list: ['127.0.0.1'], + }, + }, + log: { + file: { + path: '/tmp/service_logs/access.log', + }, + offset: 0, + }, + elastic_agent: { + id: 'ef5e274d-4b53-45e6-943a-a5bcf1a6f523', + version: '8.5.0', + snapshot: false, + }, + source: { + address: '127.0.0.1', + ip: '127.0.0.1', + }, + url: { + path: '/server-status', + original: '/server-status', + }, + tags: ['nginx-access'], + input: { + type: 'log', + }, + '@timestamp': new Date().toISOString(), + _tmp: {}, + ecs: { + version: '8.11.0', + }, + related: { + ip: ['127.0.0.1'], + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'nginx.access', + }, + host: { + hostname: 'docker-fleet-agent', + os: { + kernel: '5.15.49-linuxkit', + codename: 'focal', + name: 'Ubuntu', + family: 'debian', + type: 'linux', + version: '20.04.5 LTS (Focal Fossa)', + platform: 'ubuntu', + }, + containerized: false, + ip: ['172.18.0.7'], + name: 'docker-fleet-agent', + id: '66392b0697b84641af8006d87aeb89f1', + mac: ['02-42-AC-12-00-07'], + architecture: 'x86_64', + }, + http: { + request: { + method: 'GET', + }, + response: { + status_code: 200, + body: { + bytes: 97, + }, + }, + version: '1.1', + }, + event: { + agent_id_status: 'verified', + ingested: '2022-12-09T10:39:40Z', + created: '2022-12-09T10:39:38.896Z', + kind: 'event', + timezone: '+00:00', + category: ['web'], + type: ['access'], + dataset: 'nginx.access', + outcome: 'success', + }, + user_agent: { + original: 'curl/7.64.0', + name: 'curl', + device: { + name: 'Other', + }, + version: '7.64.0', + }, + }, + }); + }); + + after(async () => { + await es.transport.request({ + path: `/_data_stream/logs-nginx.access-default`, + method: 'delete', + }); + }); + + it('it works', async () => { + const { body, status } = await supertestWithoutAuth + .get('/api/fleet/data_streams') + .set(svlCommonApi.getInternalRequestHeader()) + .set(roleAuthc.apiKeyHeader); + + expect(status).toBe(200); + expect(body.data_streams?.[0]?.index).toBe('logs-nginx.access-default'); + }); + }); }); } From fffc526cedf1e9b7f9cde20448f180efdb4c371b Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 19 Nov 2024 18:20:57 +0000 Subject: [PATCH 26/42] skip flaky suite (#193092) --- .../components/actions_log_users_filter.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_users_filter.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_users_filter.test.tsx index 535c0114426dd..d7ca78b5b838e 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_users_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_users_filter.test.tsx @@ -15,7 +15,8 @@ import { import { ActionsLogUsersFilter } from './actions_log_users_filter'; import { MANAGEMENT_PATH } from '../../../../../common/constants'; -describe('Users filter', () => { +// FLAKY: https://github.com/elastic/kibana/issues/193092 +describe.skip('Users filter', () => { let render: ( props?: React.ComponentProps ) => ReturnType; From c7d5a8db76addf630540d4cb1b3ea4a8342ef760 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 05:23:09 +1100 Subject: [PATCH 27/42] [8.x] [Infra] Fix deprecated usage of dataview `title` (#200751) (#200773) # Backport This will backport the following commits from `main` to `8.x`: - [[Infra] Fix deprecated usage of dataview `title` (#200751)](https://github.com/elastic/kibana/pull/200751) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: jennypavlova --- .../infra/public/pages/logs/settings/validation_errors.ts | 8 ++++---- .../inventory_view/hooks/use_waffle_filters.test.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/validation_errors.ts b/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/validation_errors.ts index e6b375efdaab7..b769bb68f8d2a 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/validation_errors.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/logs/settings/validation_errors.ts @@ -91,7 +91,7 @@ export const validateIndexPatternIsTimeBased = (indexPattern: DataView): FormVal : [ { type: 'missing_timestamp_field' as const, - indexPatternTitle: indexPattern.title, + indexPatternTitle: indexPattern.getIndexPattern(), }, ]; @@ -104,14 +104,14 @@ export const validateIndexPatternHasStringMessageField = ( return [ { type: 'missing_message_field' as const, - indexPatternTitle: indexPattern.title, + indexPatternTitle: indexPattern.getIndexPattern(), }, ]; } else if (messageField.type !== KBN_FIELD_TYPES.STRING) { return [ { type: 'invalid_message_field_type' as const, - indexPatternTitle: indexPattern.title, + indexPatternTitle: indexPattern.getIndexPattern(), }, ]; } else { @@ -124,7 +124,7 @@ export const validateIndexPatternIsntRollup = (indexPattern: DataView): FormVali ? [ { type: 'rollup_index_pattern' as const, - indexPatternTitle: indexPattern.title, + indexPatternTitle: indexPattern.getIndexPattern(), }, ] : []; diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts index 87aaad06abc96..7fe7b8b3fe18c 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts +++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -21,11 +21,11 @@ jest.mock('react-router-dom', () => ({ const mockDataView = { id: 'mock-id', - title: 'mock-title', timeFieldName: TIMESTAMP_FIELD, isPersisted: () => false, getName: () => 'mock-data-view', toSpec: () => ({}), + getIndexPattern: () => 'mock-title', } as jest.Mocked; jest.mock('../../../../containers/metrics_source', () => ({ From cbe6d4ac8bc6823cf6a6260246efb491193a69ad Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 19 Nov 2024 18:22:47 +0000 Subject: [PATCH 28/42] skip flaky suite (#200091) --- x-pack/test/functional/apps/aiops/change_point_detection.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/aiops/change_point_detection.ts b/x-pack/test/functional/apps/aiops/change_point_detection.ts index c0ac744e687b5..3f80d9e12e1ea 100644 --- a/x-pack/test/functional/apps/aiops/change_point_detection.ts +++ b/x-pack/test/functional/apps/aiops/change_point_detection.ts @@ -18,7 +18,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // aiops lives in the ML UI so we need some related services. const ml = getService('ml'); - describe('change point detection', function () { + // FLAKY: https://github.com/elastic/kibana/issues/200091 + describe.skip('change point detection', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createDataViewIfNeeded('ft_ecommerce', 'order_date'); From 12031faada82b19cb687a0329632b9c212e0d222 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 05:38:46 +1100 Subject: [PATCH 29/42] [8.x] [Mappings Editor] Add support for synthetic _source (#199854) (#200776) # Backport This will backport the following commits from `main` to `8.x`: - [[Mappings Editor] Add support for synthetic _source (#199854)](https://github.com/elastic/kibana/pull/199854) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Elena Stoeva <59341489+ElenaStoeva@users.noreply.github.com> --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + .../helpers/mappings_editor.helpers.tsx | 1 + .../mappings_editor.test.tsx | 102 ++++++++++++- .../configuration_form/configuration_form.tsx | 65 +++++--- .../configuration_form_schema.tsx | 9 +- .../configuration_serialization.test.ts | 144 ++++++++++++++++++ .../source_field_section/constants.ts | 15 ++ .../source_field_section/i18n_texts.ts | 56 +++++++ .../source_field_section/index.ts | 1 + .../source_field_section.tsx | 122 +++++++++++++-- .../mappings_editor/lib/utils.test.ts | 1 + .../mappings_editor/mappings_editor.tsx | 44 +++++- .../mappings_state_context.tsx | 1 + .../components/mappings_editor/reducer.ts | 6 + .../components/mappings_editor/types/state.ts | 4 +- .../components/wizard_steps/step_mappings.tsx | 5 +- .../wizard_steps/step_mappings_container.tsx | 19 ++- .../template_form/template_form.tsx | 5 +- .../application/services/documentation.ts | 6 + .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 22 files changed, 562 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_serialization.test.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts create mode 100644 x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 4297d945f8804..b2323b07e38d1 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -430,6 +430,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D mappingSimilarity: `${ELASTICSEARCH_DOCS}similarity.html`, mappingSourceFields: `${ELASTICSEARCH_DOCS}mapping-source-field.html`, mappingSourceFieldsDisable: `${ELASTICSEARCH_DOCS}mapping-source-field.html#disable-source-field`, + mappingSyntheticSourceFields: `${ELASTICSEARCH_DOCS}mapping-source-field.html#synthetic-source`, mappingStore: `${ELASTICSEARCH_DOCS}mapping-store.html`, mappingSubobjects: `${ELASTICSEARCH_DOCS}subobjects.html`, mappingTermVector: `${ELASTICSEARCH_DOCS}term-vector.html`, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index 094fd40ab1e32..349a724043169 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -459,6 +459,7 @@ export type TestSubjects = | 'advancedConfiguration.dynamicMappingsToggle.input' | 'advancedConfiguration.metaField' | 'advancedConfiguration.routingRequiredToggle.input' + | 'sourceValueField' | 'sourceField.includesField' | 'sourceField.excludesField' | 'dynamicTemplatesEditor' diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index 685aa4963edc4..ee3b3e72e7c19 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -28,7 +28,24 @@ describe('Mappings editor: core', () => { let onChangeHandler: jest.Mock = jest.fn(); let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); let testBed: MappingsEditorTestBed; - const appDependencies = { plugins: { ml: { mlApi: {} } } }; + let hasEnterpriseLicense = true; + const mockLicenseCheck = jest.fn((type: any) => hasEnterpriseLicense); + const appDependencies = { + plugins: { + ml: { mlApi: {} }, + licensing: { + license$: { + subscribe: jest.fn((callback: any) => { + callback({ + isActive: true, + hasAtLeast: mockLicenseCheck, + }); + return { unsubscribe: jest.fn() }; + }), + }, + }, + }, + }; beforeAll(() => { jest.useFakeTimers({ legacyFakeTimers: true }); @@ -456,6 +473,11 @@ describe('Mappings editor: core', () => { updatedMappings = { ...updatedMappings, dynamic: false, + // The "enabled": true is removed as this is the default in Es + _source: { + includes: defaultMappings._source.includes, + excludes: defaultMappings._source.excludes, + }, }; delete updatedMappings.date_detection; delete updatedMappings.dynamic_date_formats; @@ -463,6 +485,84 @@ describe('Mappings editor: core', () => { expect(data).toEqual(updatedMappings); }); + + describe('props.indexMode sets the correct default value of _source field', () => { + it("defaults to 'stored' with 'standard' index mode prop", async () => { + await act(async () => { + testBed = setup( + { + value: { ...defaultMappings, _source: undefined }, + onChange: onChangeHandler, + indexMode: 'standard', + }, + ctx + ); + }); + testBed.component.update(); + + const { + actions: { selectTab }, + find, + } = testBed; + + await selectTab('advanced'); + + // Check that the stored option is selected + expect(find('sourceValueField').prop('value')).toBe('stored'); + }); + + ['logsdb', 'time_series'].forEach((indexMode) => { + it(`defaults to 'synthetic' with ${indexMode} index mode prop on enterprise license`, async () => { + hasEnterpriseLicense = true; + await act(async () => { + testBed = setup( + { + value: { ...defaultMappings, _source: undefined }, + onChange: onChangeHandler, + indexMode, + }, + ctx + ); + }); + testBed.component.update(); + + const { + actions: { selectTab }, + find, + } = testBed; + + await selectTab('advanced'); + + // Check that the synthetic option is selected + expect(find('sourceValueField').prop('value')).toBe('synthetic'); + }); + + it(`defaults to 'standard' with ${indexMode} index mode prop on basic license`, async () => { + hasEnterpriseLicense = false; + await act(async () => { + testBed = setup( + { + value: { ...defaultMappings, _source: undefined }, + onChange: onChangeHandler, + indexMode, + }, + ctx + ); + }); + testBed.component.update(); + + const { + actions: { selectTab }, + find, + } = testBed; + + await selectTab('advanced'); + + // Check that the stored option is selected + expect(find('sourceValueField').prop('value')).toBe('stored'); + }); + }); + }); }); describe('multi-fields support', () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx index e3e32c55aada0..00ce2d02a1baa 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form.tsx @@ -5,17 +5,21 @@ * 2.0. */ -import React, { useEffect, useRef, useCallback } from 'react'; +import React, { useEffect, useRef } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { FormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { useAppContext } from '../../../../app_context'; import { useForm, Form } from '../../shared_imports'; import { GenericObject, MappingsConfiguration } from '../../types'; import { MapperSizePluginId } from '../../constants'; import { useDispatch } from '../../mappings_state_context'; import { DynamicMappingSection } from './dynamic_mapping_section'; -import { SourceFieldSection } from './source_field_section'; +import { + SourceFieldSection, + STORED_SOURCE_OPTION, + SYNTHETIC_SOURCE_OPTION, + DISABLED_SOURCE_OPTION, +} from './source_field_section'; import { MetaFieldSection } from './meta_field_section'; import { RoutingSection } from './routing_section'; import { MapperSizePluginSection } from './mapper_size_plugin_section'; @@ -28,7 +32,14 @@ interface Props { esNodesPlugins: string[]; } -const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => { +interface SerializedSourceField { + enabled?: boolean; + mode?: string; + includes?: string[]; + excludes?: string[]; +} + +export const formSerializer = (formData: GenericObject) => { const { dynamicMapping, sourceField, metaField, _routing, _size, subobjects } = formData; const dynamic = dynamicMapping?.enabled @@ -37,12 +48,30 @@ const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => { ? 'strict' : dynamicMapping?.enabled; + const _source = + sourceField?.option === SYNTHETIC_SOURCE_OPTION + ? { mode: SYNTHETIC_SOURCE_OPTION } + : sourceField?.option === DISABLED_SOURCE_OPTION + ? { enabled: false } + : sourceField?.option === STORED_SOURCE_OPTION + ? { + mode: 'stored', + includes: sourceField?.includes, + excludes: sourceField?.excludes, + } + : sourceField?.includes || sourceField?.excludes + ? { + includes: sourceField?.includes, + excludes: sourceField?.excludes, + } + : undefined; + const serialized = { dynamic, numeric_detection: dynamicMapping?.numeric_detection, date_detection: dynamicMapping?.date_detection, dynamic_date_formats: dynamicMapping?.dynamic_date_formats, - _source: sourceFieldMode ? { mode: sourceFieldMode } : sourceField, + _source: _source as SerializedSourceField, _meta: metaField, _routing, _size, @@ -52,7 +81,7 @@ const formSerializer = (formData: GenericObject, sourceFieldMode?: string) => { return serialized; }; -const formDeserializer = (formData: GenericObject) => { +export const formDeserializer = (formData: GenericObject) => { const { dynamic, /* eslint-disable @typescript-eslint/naming-convention */ @@ -60,11 +89,7 @@ const formDeserializer = (formData: GenericObject) => { date_detection, dynamic_date_formats, /* eslint-enable @typescript-eslint/naming-convention */ - _source: { enabled, includes, excludes } = {} as { - enabled?: boolean; - includes?: string[]; - excludes?: string[]; - }, + _source: { enabled, mode, includes, excludes } = {} as SerializedSourceField, _meta, _routing, // For the Mapper Size plugin @@ -81,7 +106,14 @@ const formDeserializer = (formData: GenericObject) => { dynamic_date_formats, }, sourceField: { - enabled, + option: + mode === 'stored' + ? STORED_SOURCE_OPTION + : mode === 'synthetic' + ? SYNTHETIC_SOURCE_OPTION + : enabled === false + ? DISABLED_SOURCE_OPTION + : undefined, includes, excludes, }, @@ -99,14 +131,9 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) = const isMounted = useRef(false); - const serializerCallback = useCallback( - (formData: FormData) => formSerializer(formData, value?._source?.mode), - [value?._source?.mode] - ); - const { form } = useForm({ schema: configurationFormSchema, - serializer: serializerCallback, + serializer: formSerializer, deserializer: formDeserializer, defaultValue: value, id: 'configurationForm', @@ -165,7 +192,7 @@ export const ConfigurationForm = React.memo(({ value, esNodesPlugins }: Props) = - {enableMappingsSourceFieldSection && !value?._source?.mode && ( + {enableMappingsSourceFieldSection && ( <> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index a9913b9474b36..ff93e717ce090 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -75,12 +75,9 @@ export const configurationFormSchema: FormSchema = { }, }, sourceField: { - enabled: { - label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.sourceFieldLabel', { - defaultMessage: 'Enable _source field', - }), - type: FIELD_TYPES.TOGGLE, - defaultValue: true, + option: { + type: FIELD_TYPES.SUPER_SELECT, + defaultValue: 'stored', }, includes: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.includeSourceFieldsLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_serialization.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_serialization.test.ts new file mode 100644 index 0000000000000..5bf4ad3b9ee57 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_serialization.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 { formSerializer, formDeserializer } from './configuration_form'; +import { + STORED_SOURCE_OPTION, + SYNTHETIC_SOURCE_OPTION, + DISABLED_SOURCE_OPTION, +} from './source_field_section'; + +describe('Template serialization', () => { + describe('serialization of _source parameter', () => { + describe('deserializeTemplate()', () => { + test(`correctly deserializes 'stored' mode`, () => { + expect( + formDeserializer({ + _source: { + mode: 'stored', + includes: ['hello'], + excludes: ['world'], + }, + }) + ).toHaveProperty('sourceField', { + option: STORED_SOURCE_OPTION, + includes: ['hello'], + excludes: ['world'], + }); + }); + + test(`correctly deserializes 'enabled' property set to true`, () => { + expect( + formDeserializer({ + _source: { + enabled: true, + includes: ['hello'], + excludes: ['world'], + }, + }) + ).toHaveProperty('sourceField', { + includes: ['hello'], + excludes: ['world'], + }); + }); + + test(`correctly deserializes 'enabled' property set to false`, () => { + expect( + formDeserializer({ + _source: { + enabled: false, + }, + }) + ).toHaveProperty('sourceField', { + option: DISABLED_SOURCE_OPTION, + }); + }); + + test(`correctly deserializes 'synthetic' mode`, () => { + expect( + formDeserializer({ + _source: { + mode: 'synthetic', + }, + }) + ).toHaveProperty('sourceField', { + option: SYNTHETIC_SOURCE_OPTION, + }); + }); + + test(`correctly deserializes undefined mode and enabled properties with includes or excludes fields`, () => { + expect( + formDeserializer({ + _source: { + includes: ['hello'], + excludes: ['world'], + }, + }) + ).toHaveProperty('sourceField', { + includes: ['hello'], + excludes: ['world'], + }); + }); + }); + + describe('serializeTemplate()', () => { + test(`correctly serializes 'stored' option`, () => { + expect( + formSerializer({ + sourceField: { + option: STORED_SOURCE_OPTION, + includes: ['hello'], + excludes: ['world'], + }, + }) + ).toHaveProperty('_source', { + mode: 'stored', + includes: ['hello'], + excludes: ['world'], + }); + }); + + test(`correctly serializes 'disabled' option`, () => { + expect( + formSerializer({ + sourceField: { + option: DISABLED_SOURCE_OPTION, + }, + }) + ).toHaveProperty('_source', { + enabled: false, + }); + }); + + test(`correctly serializes 'synthetic' option`, () => { + expect( + formSerializer({ + sourceField: { + option: SYNTHETIC_SOURCE_OPTION, + }, + }) + ).toHaveProperty('_source', { + mode: 'synthetic', + }); + }); + + test(`correctly serializes undefined option with includes or excludes fields`, () => { + expect( + formSerializer({ + sourceField: { + includes: ['hello'], + excludes: ['world'], + }, + }) + ).toHaveProperty('_source', { + includes: ['hello'], + excludes: ['world'], + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts new file mode 100644 index 0000000000000..9e4390846fa81 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const STORED_SOURCE_OPTION = 'stored'; +export const DISABLED_SOURCE_OPTION = 'disabled'; +export const SYNTHETIC_SOURCE_OPTION = 'synthetic'; + +export type SourceOptionKey = + | typeof STORED_SOURCE_OPTION + | typeof DISABLED_SOURCE_OPTION + | typeof SYNTHETIC_SOURCE_OPTION; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts new file mode 100644 index 0000000000000..447c45b7b099d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/i18n_texts.ts @@ -0,0 +1,56 @@ +/* + * 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 { + STORED_SOURCE_OPTION, + DISABLED_SOURCE_OPTION, + SYNTHETIC_SOURCE_OPTION, + SourceOptionKey, +} from './constants'; + +export const sourceOptionLabels: Record = { + [STORED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.storedSourceFieldsLabel', + { + defaultMessage: 'Stored _source', + } + ), + [DISABLED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.disabledSourceFieldsLabel', + { + defaultMessage: 'Disabled _source', + } + ), + [SYNTHETIC_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.syntheticSourceFieldsLabel', + { + defaultMessage: 'Synthetic _source', + } + ), +}; + +export const sourceOptionDescriptions: Record = { + [STORED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.storedSourceFieldsDescription', + { + defaultMessage: 'Stores content in _source field for future retrieval', + } + ), + [DISABLED_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.disabledSourceFieldsDescription', + { + defaultMessage: 'Strongly discouraged, will impact downstream functionality', + } + ), + [SYNTHETIC_SOURCE_OPTION]: i18n.translate( + 'xpack.idxMgmt.mappingsEditor.configuration.syntheticSourceFieldsDescription', + { + defaultMessage: 'Reconstructs source content to save on disk usage', + } + ), +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts index df921a036c909..cb5f2afef6d0b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/index.ts @@ -6,3 +6,4 @@ */ export { SourceFieldSection } from './source_field_section'; +export * from './constants'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx index 236dfe98119ca..2e8f9fb88f87d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/source_field_section/source_field_section.tsx @@ -5,18 +5,61 @@ * 2.0. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut } from '@elastic/eui'; +import { EuiLink, EuiSpacer, EuiComboBox, EuiFormRow, EuiCallOut, EuiText } from '@elastic/eui'; +import { useMappingsState } from '../../../mappings_state_context'; import { documentationService } from '../../../../../services/documentation'; -import { UseField, FormDataProvider, FormRow, ToggleField } from '../../../shared_imports'; +import { UseField, FormDataProvider, FormRow, SuperSelectField } from '../../../shared_imports'; import { ComboBoxOption } from '../../../types'; +import { sourceOptionLabels, sourceOptionDescriptions } from './i18n_texts'; +import { + STORED_SOURCE_OPTION, + DISABLED_SOURCE_OPTION, + SYNTHETIC_SOURCE_OPTION, + SourceOptionKey, +} from './constants'; export const SourceFieldSection = () => { - const renderWarning = () => ( + const state = useMappingsState(); + + const renderOptionDropdownDisplay = (option: SourceOptionKey) => ( + + {sourceOptionLabels[option]} + +

{sourceOptionDescriptions[option]}

+
+
+ ); + + const sourceValueOptions = [ + { + value: STORED_SOURCE_OPTION, + inputDisplay: sourceOptionLabels[STORED_SOURCE_OPTION], + dropdownDisplay: renderOptionDropdownDisplay(STORED_SOURCE_OPTION), + 'data-test-subj': 'storedSourceFieldOption', + }, + ]; + + if (state.hasEnterpriseLicense) { + sourceValueOptions.push({ + value: SYNTHETIC_SOURCE_OPTION, + inputDisplay: sourceOptionLabels[SYNTHETIC_SOURCE_OPTION], + dropdownDisplay: renderOptionDropdownDisplay(SYNTHETIC_SOURCE_OPTION), + 'data-test-subj': 'syntheticSourceFieldOption', + }); + } + sourceValueOptions.push({ + value: DISABLED_SOURCE_OPTION, + inputDisplay: sourceOptionLabels[DISABLED_SOURCE_OPTION], + dropdownDisplay: renderOptionDropdownDisplay(DISABLED_SOURCE_OPTION), + 'data-test-subj': 'disabledSourceFieldOption', + }); + + const renderDisableWarning = () => ( {

@@ -45,13 +88,13 @@ export const SourceFieldSection = () => {

@@ -70,6 +113,44 @@ export const SourceFieldSection = () => { ); + const renderSyntheticWarning = () => ( + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText', + { + defaultMessage: '_source', + } + )} + + ), + learnMoreLink: ( + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + } + iconType="warning" + color="warning" + /> + ); + const renderFormFields = () => (

@@ -155,21 +236,34 @@ export const SourceFieldSection = () => { }} /> - + } > - + {(formData) => { - const { - sourceField: { enabled }, - } = formData; + const { sourceField } = formData; - if (enabled === undefined) { + if (sourceField?.option === undefined) { return null; } - return enabled ? renderFormFields() : renderWarning(); + return sourceField?.option === STORED_SOURCE_OPTION + ? renderFormFields() + : sourceField?.option === DISABLED_SOURCE_OPTION + ? renderDisableWarning() + : renderSyntheticWarning(); }} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts index 58b40293f64f2..872c62bc6f7a7 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.test.ts @@ -421,6 +421,7 @@ describe('utils', () => { selectedDataTypes: ['Boolean'], }, inferenceToModelIdMap: {}, + hasEnterpriseLicense: true, mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 }, }; test('returns list of matching fields with search term', () => { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index e1f5306899db3..cc87c3cd614e3 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -9,6 +9,9 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; +import { ILicense } from '@kbn/licensing-plugin/common/types'; +import { useAppContext } from '../../app_context'; +import { IndexMode } from '../../../../common/types/data_streams'; import { DocumentFields, RuntimeFieldsList, @@ -32,6 +35,7 @@ import { DocLinksStart } from './shared_imports'; import { DocumentFieldsHeader } from './components/document_fields/document_fields_header'; import { SearchResult } from './components/document_fields/search_fields'; import { parseMappings } from '../../shared/parse_mappings'; +import { LOGSDB_INDEX_MODE, TIME_SERIES_MODE } from '../../../../common/constants'; type TabName = 'fields' | 'runtimeFields' | 'advanced' | 'templates'; @@ -52,10 +56,14 @@ export interface Props { docLinks: DocLinksStart; /** List of plugins installed in the cluster nodes */ esNodesPlugins: string[]; + indexMode?: IndexMode; } export const MappingsEditor = React.memo( - ({ onChange, value, docLinks, indexSettings, esNodesPlugins }: Props) => { + ({ onChange, value, docLinks, indexSettings, esNodesPlugins, indexMode }: Props) => { + const { + plugins: { licensing }, + } = useAppContext(); const { parsedDefaultValue, multipleMappingsDeclared } = useMemo( () => parseMappings(value), [value] @@ -120,6 +128,40 @@ export const MappingsEditor = React.memo( [dispatch] ); + const [isLicenseCheckComplete, setIsLicenseCheckComplete] = useState(false); + useEffect(() => { + const subscription = licensing?.license$.subscribe((license: ILicense) => { + dispatch({ + type: 'hasEnterpriseLicense.update', + value: license.isActive && license.hasAtLeast('enterprise'), + }); + setIsLicenseCheckComplete(true); + }); + + return () => subscription?.unsubscribe(); + }, [dispatch, licensing]); + + useEffect(() => { + if ( + isLicenseCheckComplete && + !state.configuration.defaultValue._source && + (indexMode === LOGSDB_INDEX_MODE || indexMode === TIME_SERIES_MODE) + ) { + if (state.hasEnterpriseLicense) { + dispatch({ + type: 'configuration.save', + value: { ...state.configuration.defaultValue, _source: { mode: 'synthetic' } } as any, + }); + } + } + }, [ + indexMode, + dispatch, + state.configuration, + state.hasEnterpriseLicense, + isLicenseCheckComplete, + ]); + const tabToContentMap = { fields: ( = ({ childr selectedDataTypes: [], }, inferenceToModelIdMap: {}, + hasEnterpriseLicense: false, mappingViewFields: { byId: {}, rootLevelFields: [], aliases: {}, maxNestedDepth: 0 }, }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts index 626ee0e839a8a..ecb9648c34d00 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/reducer.ts @@ -629,5 +629,11 @@ export const reducer = (state: State, action: Action): State => { inferenceToModelIdMap: action.value.inferenceToModelIdMap, }; } + case 'hasEnterpriseLicense.update': { + return { + ...state, + hasEnterpriseLicense: action.value, + }; + } } }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts index f40fe420eb3be..43b3a7dde3b16 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/state.ts @@ -108,6 +108,7 @@ export interface State { }; templates: TemplatesFormState; inferenceToModelIdMap?: InferenceToModelIdMap; + hasEnterpriseLicense: boolean; mappingViewFields: NormalizedFields; // state of the incoming index mappings, separate from the editor state above } @@ -140,6 +141,7 @@ export type Action = | { type: 'fieldsJsonEditor.update'; value: { json: { [key: string]: any }; isValid: boolean } } | { type: 'search:update'; value: string } | { type: 'validity:update'; value: boolean } - | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } }; + | { type: 'filter:update'; value: { selectedOptions: EuiSelectableOption[] } } + | { type: 'hasEnterpriseLicense.update'; value: boolean }; export type Dispatch = (action: Action) => void; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx index 62685a05f7ff9..a239971c1bf82 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx @@ -16,6 +16,7 @@ import { EuiText, } from '@elastic/eui'; +import { IndexMode } from '../../../../../../common/types/data_streams'; import { Forms } from '../../../../../shared_imports'; import { useAppContext } from '../../../../app_context'; import { @@ -33,10 +34,11 @@ interface Props { esNodesPlugins: string[]; defaultValue?: { [key: string]: any }; indexSettings?: IndexSettings; + indexMode?: IndexMode; } export const StepMappings: React.FunctionComponent = React.memo( - ({ defaultValue = {}, onChange, indexSettings, esDocsBase, esNodesPlugins }) => { + ({ defaultValue = {}, onChange, indexSettings, esDocsBase, esNodesPlugins, indexMode }) => { const [mappings, setMappings] = useState(defaultValue); const { docLinks } = useAppContext(); @@ -115,6 +117,7 @@ export const StepMappings: React.FunctionComponent = React.memo( indexSettings={indexSettings} docLinks={docLinks} esNodesPlugins={esNodesPlugins} + indexMode={indexMode} /> diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 50b763ce0d06a..1b8a6bac2a4d8 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -7,6 +7,8 @@ import React from 'react'; +import { WizardContent } from '../../../template_form/template_form'; +import { TemplateDeserialized } from '../../../../../../common'; import { Forms } from '../../../../../shared_imports'; import { useLoadNodesPlugins } from '../../../../services'; import { CommonWizardSteps } from './types'; @@ -14,15 +16,29 @@ import { StepMappings } from './step_mappings'; interface Props { esDocsBase: string; + getTemplateData?: (wizardContent: WizardContent) => TemplateDeserialized; } -export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { +export const StepMappingsContainer: React.FunctionComponent = ({ + esDocsBase, + getTemplateData, +}) => { const { defaultValue, updateContent, getSingleContentData } = Forms.useContent< CommonWizardSteps, 'mappings' >('mappings'); const { data: esNodesPlugins } = useLoadNodesPlugins(); + const { getData } = Forms.useMultiContentContext(); + + let indexMode; + if (getTemplateData) { + const wizardContent = getData(); + // Build the current template object, providing the wizard content data + const template = getTemplateData(wizardContent); + indexMode = template?.indexMode; + } + return ( = ({ esDocsBa indexSettings={getSingleContentData('settings')} esDocsBase={esDocsBase} esNodesPlugins={esNodesPlugins ?? []} + indexMode={indexMode} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 53b53a6ebdeee..1f3d2a22874d3 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -349,7 +349,10 @@ export const TemplateForm = ({ - + diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index 58aba69351883..62b7defd78db1 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -55,6 +55,7 @@ class DocumentationService { private mappingSimilarity: string = ''; private mappingSourceFields: string = ''; private mappingSourceFieldsDisable: string = ''; + private mappingSyntheticSourceFields: string = ''; private mappingStore: string = ''; private mappingSubobjects: string = ''; private mappingTermVector: string = ''; @@ -115,6 +116,7 @@ class DocumentationService { this.mappingSimilarity = links.elasticsearch.mappingSimilarity; this.mappingSourceFields = links.elasticsearch.mappingSourceFields; this.mappingSourceFieldsDisable = links.elasticsearch.mappingSourceFieldsDisable; + this.mappingSyntheticSourceFields = links.elasticsearch.mappingSyntheticSourceFields; this.mappingStore = links.elasticsearch.mappingStore; this.mappingSubobjects = links.elasticsearch.mappingSubobjects; this.mappingTermVector = links.elasticsearch.mappingTermVector; @@ -215,6 +217,10 @@ class DocumentationService { return this.mappingSourceFieldsDisable; } + public getMappingSyntheticSourceFieldLink() { + return this.mappingSyntheticSourceFields; + } + public getNullValueLink() { return this.mappingNullValue; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 88a3e85ece298..8260e5c53bd8e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -22939,7 +22939,6 @@ "xpack.idxMgmt.mappingsEditor.configuration.numericFieldLabel": "Mapper les chaînes numériques en tant que nombres", "xpack.idxMgmt.mappingsEditor.configuration.routingLabel": "Demander une valeur _routing pour les opérations CRUD", "xpack.idxMgmt.mappingsEditor.configuration.sizeLabel": "Indexer la taille du champ _source en octets", - "xpack.idxMgmt.mappingsEditor.configuration.sourceFieldLabel": "Activer le champ _source", "xpack.idxMgmt.mappingsEditor.configuration.sourceFieldPathComboBoxHelpText": "Accepte un chemin d'accès au champ, y compris les caractères génériques.", "xpack.idxMgmt.mappingsEditor.configuration.subobjectsLabel": "Autoriser les objets à contenir d'autres sous-objets", "xpack.idxMgmt.mappingsEditor.configuration.throwErrorsForUnmappedFieldsLabel": "Lever une exception lorsqu'un document contient un champ non mappé", @@ -23082,7 +23081,6 @@ "xpack.idxMgmt.mappingsEditor.dimsFieldLabel": "Dimensions", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1": "La désactivation de {source} réduit la surcharge de stockage dans l'index, mais cela a un coût. Cette action désactive également des fonctionnalités essentielles, comme la capacité à réindexer ou à déboguer les requêtes en affichant le document d'origine.", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1.sourceText": "_source", - "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2": "Découvrez-en plus sur les alternatives à la désactivation du champ {source}.", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText": "_source", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutTitle": "Faites preuve de prudence lorsque vous désactivez le champ _source", "xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsAriaLabel": "Rechercher dans les champs mappés", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aa7f66dde6817..38185735b9d3b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -22911,7 +22911,6 @@ "xpack.idxMgmt.mappingsEditor.configuration.numericFieldLabel": "数字の文字列の数値としてのマッピング", "xpack.idxMgmt.mappingsEditor.configuration.routingLabel": "CRUD操作のためのRequire _routing値", "xpack.idxMgmt.mappingsEditor.configuration.sizeLabel": "_sourceフィールドサイズ(バイト)にインデックスを作成", - "xpack.idxMgmt.mappingsEditor.configuration.sourceFieldLabel": "_sourceフィールドの有効化", "xpack.idxMgmt.mappingsEditor.configuration.sourceFieldPathComboBoxHelpText": "ワイルドカードを含め、フィールドへのパスを受け入れます。", "xpack.idxMgmt.mappingsEditor.configuration.subobjectsLabel": "オブジェクトがさらにサブオブジェクトを保持することを許可", "xpack.idxMgmt.mappingsEditor.configuration.throwErrorsForUnmappedFieldsLabel": "ドキュメントがマッピングされていないフィールドを含む場合に例外を選択する", @@ -23054,7 +23053,6 @@ "xpack.idxMgmt.mappingsEditor.dimsFieldLabel": "次元", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1": "{source}を無効にすることで、インデックス内のストレージオーバーヘッドが削減されますが、これにはコストがかかります。これはまた、元のドキュメントを表示して、再インデックスやクエリーのデバッグといった重要な機能を無効にします。", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1.sourceText": "_source", - "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2": "{source}フィールドを無効にするための代替方法の詳細", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText": "_source", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutTitle": "_source fieldを無効にする際は慎重に行う", "xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsAriaLabel": "マッピングされたフィールドの検索", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index beeaabe1752f3..03662564cefe5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22963,7 +22963,6 @@ "xpack.idxMgmt.mappingsEditor.configuration.numericFieldLabel": "将数值字符串映射为数字", "xpack.idxMgmt.mappingsEditor.configuration.routingLabel": "CRUD 操作需要 _routing 值", "xpack.idxMgmt.mappingsEditor.configuration.sizeLabel": "索引 _source 字段大小(字节)", - "xpack.idxMgmt.mappingsEditor.configuration.sourceFieldLabel": "启用 _source 字段", "xpack.idxMgmt.mappingsEditor.configuration.sourceFieldPathComboBoxHelpText": "接受字段的路径,包括通配符。", "xpack.idxMgmt.mappingsEditor.configuration.subobjectsLabel": "允许对象存放更多子对象", "xpack.idxMgmt.mappingsEditor.configuration.throwErrorsForUnmappedFieldsLabel": "文档包含未映射字段时引发异常", @@ -23106,7 +23105,6 @@ "xpack.idxMgmt.mappingsEditor.dimsFieldLabel": "维度数", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1": "禁用 {source} 可降低索引内的存储开销,这有一定的代价。其还禁用重要的功能,如通过查看原始文档来重新索引或调试查询的功能。", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription1.sourceText": "_source", - "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2": "详细了解禁用 {source} 字段的备选方式。", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutDescription2.sourceText": "_source", "xpack.idxMgmt.mappingsEditor.disabledSourceFieldCallOutTitle": "禁用 _source 字段时要十分谨慎", "xpack.idxMgmt.mappingsEditor.documentFields.searchFieldsAriaLabel": "搜索映射的字段", From ef4c6a7319440cc179a6988b7f09e48c5a75766c Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 19 Nov 2024 14:03:12 -0500 Subject: [PATCH 30/42] [8.x] Enabling Full FTR, Integration, and Unit tests to the FIPS Test Pipeline (#192632) (#200780) # Backport This will backport the following commits from `main` to `8.x`: - [Enabling Full FTR, Integration, and Unit tests to the FIPS Test Pipeline (#192632)](https://github.com/elastic/kibana/pull/192632) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .buildkite/pipelines/fips.yml | 7 +- .buildkite/scripts/steps/fips/smoke_test.sh | 24 -- .../scripts/steps/test/jest_parallel.sh | 9 +- .../src/security_service.test.ts | 26 +- .../tsconfig.json | 1 + .../src/create_root.ts | 24 +- .../tsconfig.json | 1 + packages/kbn-es/src/install/install_source.ts | 4 +- packages/kbn-test/src/es/test_es_cluster.ts | 6 +- .../config/check_dynamic_config.test.ts | 229 +++++++++--------- .../config/config_deprecation.test.ts | 21 +- .../default_route_provider_config.test.ts | 110 +++++---- .../version_compatibility.test.ts | 17 +- .../integration_tests/node/migrator.test.ts | 4 +- .../group3/multiple_es_nodes.test.ts | 167 +++++++------ .../migrations/group3/read_batch_size.test.ts | 51 ++-- .../serverless/migrations/smoke.test.ts | 39 +-- .../build/lib/integration_tests/fs.test.ts | 10 +- .../adapters/es/integration_tests/es.test.ts | 7 +- .../file_client/create_es_file_client.ts | 9 +- .../integration_tests/es_file_client.test.ts | 15 +- .../file_hash_transform.test.ts | 40 +-- .../group5/dashboard_panel_listing.ts | 3 +- .../http/ssl_with_p12/index.js | 3 +- .../http/ssl_with_p12_intermediate/index.js | 3 +- .../axios_utils_connection.test.ts | 51 ++-- .../plugins/fleet/.storybook/smoke.test.tsx | 32 ++- .../services/preconfiguration/outputs.ts | 1 - .../read_only_view.ts | 1 + 29 files changed, 520 insertions(+), 395 deletions(-) delete mode 100755 .buildkite/scripts/steps/fips/smoke_test.sh diff --git a/.buildkite/pipelines/fips.yml b/.buildkite/pipelines/fips.yml index 09ae10496456f..08997ede831c9 100644 --- a/.buildkite/pipelines/fips.yml +++ b/.buildkite/pipelines/fips.yml @@ -40,14 +40,15 @@ steps: machineType: n2-standard-2 preemptible: true - - command: .buildkite/scripts/steps/fips/smoke_test.sh - label: 'Pick Smoke Test Group Run Order' + - command: .buildkite/scripts/steps/test/pick_test_group_run_order.sh + label: 'Pick Test Group Run Order' depends_on: build timeout_in_minutes: 10 env: FTR_CONFIGS_SCRIPT: '.buildkite/scripts/steps/test/ftr_configs.sh' FTR_EXTRA_ARGS: '$FTR_EXTRA_ARGS' - LIMIT_CONFIG_TYPE: 'functional' + JEST_UNIT_SCRIPT: '.buildkite/scripts/steps/test/jest.sh' + JEST_INTEGRATION_SCRIPT: '.buildkite/scripts/steps/test/jest_integration.sh' retry: automatic: - exit_status: '*' diff --git a/.buildkite/scripts/steps/fips/smoke_test.sh b/.buildkite/scripts/steps/fips/smoke_test.sh deleted file mode 100755 index 685bb111ff81a..0000000000000 --- a/.buildkite/scripts/steps/fips/smoke_test.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Limit the FTR configs for now to avoid running all the tests. Once we're -# ready to utilize the full FTR suite in FIPS mode, we can remove this file and -# call pick_test_group_run_order.sh directly in .buildkite/pipelines/fips.yml. -configs=( - "x-pack/test/reporting_functional/reporting_and_security.config.ts" - "x-pack/test/saved_object_api_integration/security_and_spaces/config_trial.ts" - "x-pack/test/alerting_api_integration/security_and_spaces/group1/config.ts" - "x-pack/test/alerting_api_integration/security_and_spaces/group2/config.ts" - "x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts" - "x-pack/test/alerting_api_integration/security_and_spaces/group4/config.ts" - "x-pack/test/functional/apps/saved_objects_management/config.ts" - "x-pack/test/functional/apps/user_profiles/config.ts" - "x-pack/test/functional/apps/security/config.ts" -) - -printf -v FTR_CONFIG_PATTERNS '%s,' "${configs[@]}" -FTR_CONFIG_PATTERNS="${FTR_CONFIG_PATTERNS%,}" -export FTR_CONFIG_PATTERNS - -.buildkite/scripts/steps/test/pick_test_group_run_order.sh diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index 2a7cf780f5787..648c3b225141d 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -60,7 +60,14 @@ while read -r config; do # --trace-warnings to debug # Node.js process-warning detected: # Warning: Closing file descriptor 24 on garbage collection - cmd="NODE_OPTIONS=\"--max-old-space-size=12288 --trace-warnings\" node ./scripts/jest --config=\"$config\" $parallelism --coverage=false --passWithNoTests" + cmd="NODE_OPTIONS=\"--max-old-space-size=12288 --trace-warnings" + + if [ "${KBN_ENABLE_FIPS:-}" == "true" ]; then + cmd=$cmd" --enable-fips --openssl-config=$HOME/nodejs.cnf" + fi + + cmd=$cmd"\" node ./scripts/jest --config=\"$config\" $parallelism --coverage=false --passWithNoTests" + echo "actual full command is:" echo "$cmd" echo "" diff --git a/packages/core/security/core-security-server-internal/src/security_service.test.ts b/packages/core/security/core-security-server-internal/src/security_service.test.ts index 75539e9954ac0..0ff1e59db71ec 100644 --- a/packages/core/security/core-security-server-internal/src/security_service.test.ts +++ b/packages/core/security/core-security-server-internal/src/security_service.test.ts @@ -16,17 +16,32 @@ import { loggerMock, MockedLogger } from '@kbn/logging-mocks'; import { mockCoreContext } from '@kbn/core-base-server-mocks'; import type { CoreSecurityDelegateContract } from '@kbn/core-security-server'; import { SecurityService } from './security_service'; +import { configServiceMock } from '@kbn/config-mocks'; +import { getFips } from 'crypto'; const createStubInternalContract = (): CoreSecurityDelegateContract => { return Symbol('stubContract') as unknown as CoreSecurityDelegateContract; }; -describe('SecurityService', () => { +describe('SecurityService', function () { let coreContext: ReturnType; + let configService: ReturnType; let service: SecurityService; beforeEach(() => { - coreContext = mockCoreContext.create(); + const mockConfig = { + xpack: { + security: { + experimental: { + fipsMode: { + enabled: !!getFips(), + }, + }, + }, + }, + }; + configService = configServiceMock.create({ getConfig$: mockConfig }); + coreContext = mockCoreContext.create({ configService }); service = new SecurityService(coreContext); convertSecurityApiMock.mockReset(); @@ -51,8 +66,11 @@ describe('SecurityService', () => { describe('#isEnabled', () => { it('should return boolean', () => { const { fips } = service.setup(); - - expect(fips.isEnabled()).toBe(false); + if (getFips() === 0) { + expect(fips.isEnabled()).toBe(false); + } else { + expect(fips.isEnabled()).toBe(true); + } }); }); }); diff --git a/packages/core/security/core-security-server-internal/tsconfig.json b/packages/core/security/core-security-server-internal/tsconfig.json index e1812dc77cf49..11128461daf4e 100644 --- a/packages/core/security/core-security-server-internal/tsconfig.json +++ b/packages/core/security/core-security-server-internal/tsconfig.json @@ -22,5 +22,6 @@ "@kbn/core-base-server-mocks", "@kbn/config", "@kbn/core-logging-server-mocks", + "@kbn/config-mocks", ] } diff --git a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts index 9dda533d41065..672dab2e2d70e 100644 --- a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts +++ b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts @@ -12,10 +12,12 @@ import loadJsonFile from 'load-json-file'; import { defaultsDeep } from 'lodash'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; +import { set } from '@kbn/safer-lodash-set'; import { getPackages } from '@kbn/repo-packages'; import { ToolingLog } from '@kbn/tooling-log'; import { REPO_ROOT } from '@kbn/repo-info'; +import { getFips } from 'crypto'; import { createTestEsCluster, CreateTestEsClusterOptions, @@ -58,6 +60,17 @@ export function createRootWithSettings( pkg.version = customKibanaVersion; } + /* + * Most of these integration tests expect OSS to default to true, but FIPS + * requires the security plugin to be enabled + */ + let oss = true; + if (getFips() === 1) { + set(settings, 'xpack.security.experimental.fipsMode.enabled', true); + oss = false; + delete cliArgs.oss; + } + const env = Env.createDefault( REPO_ROOT, { @@ -67,10 +80,10 @@ export function createRootWithSettings( watch: false, basePath: false, runExamples: false, - oss: true, disableOptimizer: true, cache: true, dist: false, + oss, ...cliArgs, }, repoPackages: getPackages(REPO_ROOT), @@ -237,7 +250,13 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = settings.es?.license ?? 'basic'; + let license = settings.es?.license ?? 'basic'; + + if (getFips() === 1) { + // Set license to 'trial' if Node is running in FIPS mode + license = 'trial'; + } + const usersToBeAdded = settings.users ?? []; if (usersToBeAdded.length > 0) { if (license !== 'trial') { @@ -274,6 +293,7 @@ export function createTestServers({ hosts: es.getHostUrls(), username: kibanaServerTestUser.username, password: kibanaServerTestUser.password, + ...(getFips() ? kbnSettings.elasticsearch : {}), }; } diff --git a/packages/core/test-helpers/core-test-helpers-kbn-server/tsconfig.json b/packages/core/test-helpers/core-test-helpers-kbn-server/tsconfig.json index 85d14bb04ab59..65ca0ccdfca0b 100644 --- a/packages/core/test-helpers/core-test-helpers-kbn-server/tsconfig.json +++ b/packages/core/test-helpers/core-test-helpers-kbn-server/tsconfig.json @@ -20,6 +20,7 @@ "@kbn/repo-packages", "@kbn/es", "@kbn/dev-utils", + "@kbn/safer-lodash-set", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-es/src/install/install_source.ts b/packages/kbn-es/src/install/install_source.ts index 7dfbe8d7bd5b3..9a7e8f166791a 100644 --- a/packages/kbn-es/src/install/install_source.ts +++ b/packages/kbn-es/src/install/install_source.ts @@ -84,7 +84,7 @@ async function sourceInfo(cwd: string, license: string, log: ToolingLog = defaul log.info('on %s at %s', chalk.bold(branch), chalk.bold(sha)); log.info('%s locally modified file(s)', chalk.bold(status.modified.length)); - const etag = crypto.createHash('md5').update(branch); + const etag = crypto.createHash('sha256').update(branch); etag.update(sha); // for changed files, use last modified times in hash calculation @@ -92,7 +92,7 @@ async function sourceInfo(cwd: string, license: string, log: ToolingLog = defaul etag.update(fs.statSync(path.join(cwd, file.path)).mtime.toString()); }); - const cwdHash = crypto.createHash('md5').update(cwd).digest('hex').substr(0, 8); + const cwdHash = crypto.createHash('sha256').update(cwd).digest('hex').substr(0, 8); const basename = `${branch}-${task}-${cwdHash}`; const filename = `${basename}.${ext}`; diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 620147e1fa7ab..20c54e044e46f 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -21,6 +21,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import { REPO_ROOT } from '@kbn/repo-info'; import type { ArtifactLicense } from '@kbn/es'; import type { ServerlessOptions } from '@kbn/es/src/utils'; +import { getFips } from 'crypto'; import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix'; import { esTestConfig } from './es_test_config'; @@ -200,12 +201,15 @@ export function createTestEsCluster< const esArgs = assignArgs(defaultEsArgs, customEsArgs); + // Use 'trial' license if FIPS mode is enabled, otherwise use the provided license or default to 'basic' + const testLicense: ArtifactLicense = getFips() === 1 ? 'trial' : license ? license : 'basic'; + const config = { version: esVersion, installPath: Path.resolve(basePath, clusterName), sourcePath: Path.resolve(REPO_ROOT, '../elasticsearch'), + license: testLicense, password, - license, basePath, esArgs, resources: files, diff --git a/src/core/server/integration_tests/config/check_dynamic_config.test.ts b/src/core/server/integration_tests/config/check_dynamic_config.test.ts index eaffd56ed1b16..38250076385b9 100644 --- a/src/core/server/integration_tests/config/check_dynamic_config.test.ts +++ b/src/core/server/integration_tests/config/check_dynamic_config.test.ts @@ -16,124 +16,135 @@ import { request, } from '@kbn/core-test-helpers-kbn-server'; import { PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH } from '@kbn/core-plugins-server-internal/src/constants'; - -describe('PUT /internal/core/_settings', () => { - let esServer: TestElasticsearchUtils; - let root: Root; - - const loggerName = 'my-test-logger'; - - beforeAll(async () => { - const settings = { - coreApp: { allowDynamicConfigOverrides: true }, - logging: { - loggers: [{ name: loggerName, level: 'error', appenders: ['console'] }], - }, - }; - const { startES, startKibana } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - kbn: settings, - }, +import { getFips } from 'crypto'; + +if (getFips() === 0) { + describe('PUT /internal/core/_settings', () => { + let esServer: TestElasticsearchUtils; + let root: Root; + + const loggerName = 'my-test-logger'; + + beforeAll(async () => { + const settings = { + coreApp: { allowDynamicConfigOverrides: true }, + logging: { + loggers: [{ name: loggerName, level: 'error', appenders: ['console'] }], + }, + server: { restrictInternalApis: false }, + }; + const { startES, startKibana } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + kbn: settings, + }, + }); + + esServer = await startES(); + + const kbnUtils = await startKibana(); + root = kbnUtils.root; + + // eslint-disable-next-line dot-notation + root['server'].configService.addDynamicConfigPaths('logging', ['loggers']); // just for the sake of being able to change something easy to test }); - esServer = await startES(); - - const kbnUtils = await startKibana(); - root = kbnUtils.root; - - // eslint-disable-next-line dot-notation - root['server'].configService.addDynamicConfigPaths('logging', ['loggers']); // just for the sake of being able to change something easy to test - }); + afterAll(async () => { + await root?.shutdown(); + await esServer?.stop(); + }); - afterAll(async () => { - await root?.shutdown(); - await esServer?.stop(); - }); + test('should update the log level', async () => { + const logger = root.logger.get(loggerName); + expect(logger.isLevelEnabled('info')).toBe(false); + await request + .put(root, '/internal/core/_settings') + .set('Elastic-Api-Version', '1') + .send({ 'logging.loggers': [{ name: loggerName, level: 'debug', appenders: ['console'] }] }) + .expect(200); + expect(logger.isLevelEnabled('info')).toBe(true); + }); - test('should update the log level', async () => { - const logger = root.logger.get(loggerName); - expect(logger.isLevelEnabled('info')).toBe(false); - await request - .put(root, '/internal/core/_settings') - .set('Elastic-Api-Version', '1') - .send({ 'logging.loggers': [{ name: loggerName, level: 'debug', appenders: ['console'] }] }) - .expect(200); - expect(logger.isLevelEnabled('info')).toBe(true); + test('should remove the setting', async () => { + const logger = root.logger.get(loggerName); + expect(logger.isLevelEnabled('info')).toBe(true); // still true from the previous test + await request + .put(root, '/internal/core/_settings') + .set('Elastic-Api-Version', '1') + .send({ 'logging.loggers': null }) + .expect(200); + expect(logger.isLevelEnabled('info')).toBe(false); + }); }); - test('should remove the setting', async () => { - const logger = root.logger.get(loggerName); - expect(logger.isLevelEnabled('info')).toBe(true); // still true from the previous test - await request - .put(root, '/internal/core/_settings') - .set('Elastic-Api-Version', '1') - .send({ 'logging.loggers': null }) - .expect(200); - expect(logger.isLevelEnabled('info')).toBe(false); - }); -}); - -describe('checking all opted-in dynamic config settings', () => { - let root: Root; - - beforeAll(async () => { - const settings = { - logging: { - loggers: [{ name: 'root', level: 'info', appenders: ['console'] }], - }, - }; - - set(settings, PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH, true); - - root = createRootWithCorePlugins(settings, { - basePath: false, - cache: false, - dev: true, - disableOptimizer: true, - silent: false, - dist: false, - oss: false, - runExamples: false, - watch: false, + describe('checking all opted-in dynamic config settings', () => { + let root: Root; + + beforeAll(async () => { + const settings = { + logging: { + loggers: [{ name: 'root', level: 'info', appenders: ['console'] }], + }, + server: { + restrictInternalApis: false, + }, + }; + + set(settings, PLUGIN_SYSTEM_ENABLE_ALL_PLUGINS_CONFIG_PATH, true); + + root = createRootWithCorePlugins(settings, { + basePath: false, + cache: false, + dev: true, + disableOptimizer: true, + silent: false, + dist: false, + oss: false, + runExamples: false, + watch: false, + }); + + await root.preboot(); + await root.setup(); }); - await root.preboot(); - await root.setup(); - }); + afterAll(async () => { + if (root) { + await root.shutdown(); + } + }); - afterAll(async () => { - if (root) { - await root.shutdown(); + function getListOfDynamicConfigPaths(): string[] { + // eslint-disable-next-line dot-notation + return [...root['server']['configService']['dynamicPaths'].entries()] + .flatMap(([configPath, dynamicConfigKeys]) => { + return dynamicConfigKeys.map( + (dynamicConfigKey: string) => `${configPath}.${dynamicConfigKey}` + ); + }) + .sort(); } - }); - function getListOfDynamicConfigPaths(): string[] { - // eslint-disable-next-line dot-notation - return [...root['server']['configService']['dynamicPaths'].entries()] - .flatMap(([configPath, dynamicConfigKeys]) => { - return dynamicConfigKeys.map( - (dynamicConfigKey: string) => `${configPath}.${dynamicConfigKey}` - ); - }) - .sort(); - } - - /** - * This test is meant to fail when any setting is flagged as capable - * of dynamic configuration {@link PluginConfigDescriptor.dynamicConfig}. - * - * Please, add your settings to the list with a comment of why it's required to be dynamic. - * - * The intent is to trigger a code review from the Core and Security teams to discuss potential issues. - */ - test('detecting all the settings that have opted-in for dynamic in-memory updates', () => { - expect(getListOfDynamicConfigPaths()).toStrictEqual([ - // Making testing easier by having the ability of overriding the feature flags without the need to restart - 'feature_flags.overrides', - // We need this for enriching our Perf tests with more valuable data regarding the steps of the test - // Also helpful in Cloud & Serverless testing because we can't control the labels in those offerings - 'telemetry.labels', - ]); + /** + * This test is meant to fail when any setting is flagged as capable + * of dynamic configuration {@link PluginConfigDescriptor.dynamicConfig}. + * + * Please, add your settings to the list with a comment of why it's required to be dynamic. + * + * The intent is to trigger a code review from the Core and Security teams to discuss potential issues. + */ + test('detecting all the settings that have opted-in for dynamic in-memory updates', () => { + expect(getListOfDynamicConfigPaths()).toStrictEqual([ + // Making testing easier by having the ability of overriding the feature flags without the need to restart + 'feature_flags.overrides', + // We need this for enriching our Perf tests with more valuable data regarding the steps of the test + // Also helpful in Cloud & Serverless testing because we can't control the labels in those offerings + 'telemetry.labels', + ]); + }); + }); +} else { + it('is running in FIPS mode, skipping tests since they fail due to FIPS overrides', () => { + expect(getFips()).toBe(1); }); -}); +} diff --git a/src/core/server/integration_tests/config/config_deprecation.test.ts b/src/core/server/integration_tests/config/config_deprecation.test.ts index 277a6080c377f..42425f10a4c97 100644 --- a/src/core/server/integration_tests/config/config_deprecation.test.ts +++ b/src/core/server/integration_tests/config/config_deprecation.test.ts @@ -10,6 +10,7 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { mockLoggingSystem } from './config_deprecation.test.mocks'; import { createRoot } from '@kbn/core-test-helpers-kbn-server'; +import { getFips } from 'crypto'; describe('configuration deprecations', () => { let root: ReturnType; @@ -24,13 +25,19 @@ describe('configuration deprecations', () => { } }); - it('should not log deprecation warnings for default configuration', async () => { - root = createRoot(); + if (getFips() === 0) { + it('should not log deprecation warnings for default configuration', async () => { + root = createRoot(); - await root.preboot(); - await root.setup(); + await root.preboot(); + await root.setup(); - const logs = loggingSystemMock.collect(mockLoggingSystem); - expect(logs.warn.flat()).toHaveLength(0); - }); + const logs = loggingSystemMock.collect(mockLoggingSystem); + expect(logs.warn.flat()).toHaveLength(0); + }); + } else { + it('fips is enabled and the default configuration has been overridden', () => { + expect(getFips()).toBe(1); + }); + } }); diff --git a/src/core/server/integration_tests/core_app/default_route_provider_config.test.ts b/src/core/server/integration_tests/core_app/default_route_provider_config.test.ts index 8aabf184a2323..a3338e7d45468 100644 --- a/src/core/server/integration_tests/core_app/default_route_provider_config.test.ts +++ b/src/core/server/integration_tests/core_app/default_route_provider_config.test.ts @@ -14,73 +14,81 @@ import { request, type TestElasticsearchUtils, } from '@kbn/core-test-helpers-kbn-server'; +import { getFips } from 'crypto'; describe('default route provider', () => { let esServer: TestElasticsearchUtils; let root: Root; - beforeAll(async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), + if (getFips() === 0) { + beforeAll(async () => { + const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + }); + esServer = await startES(); + root = createRootWithCorePlugins({ + server: { + basePath: '/hello', + restrictInternalApis: false, + }, + }); + + await root.preboot(); + await root.setup(); + await root.start(); }); - esServer = await startES(); - root = createRootWithCorePlugins({ - server: { - basePath: '/hello', - }, + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); }); - await root.preboot(); - await root.setup(); - await root.start(); - }); + it('redirects to the configured default route respecting basePath', async function () { + const { status, header } = await request.get(root, '/'); - afterAll(async () => { - await esServer.stop(); - await root.shutdown(); - }); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/app/home', + }); + }); - it('redirects to the configured default route respecting basePath', async function () { - const { status, header } = await request.get(root, '/'); + it('ignores invalid values', async function () { + const invalidRoutes = [ + 'http://not-your-kibana.com', + '///example.com', + '//example.com', + ' //example.com', + ]; - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/hello/app/home', - }); - }); + for (const url of invalidRoutes) { + await request + .post(root, '/internal/kibana/settings/defaultRoute') + .send({ value: url }) + .expect(400); + } - it('ignores invalid values', async function () { - const invalidRoutes = [ - 'http://not-your-kibana.com', - '///example.com', - '//example.com', - ' //example.com', - ]; + const { status, header } = await request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/app/home', + }); + }); - for (const url of invalidRoutes) { + it('consumes valid values', async function () { await request .post(root, '/internal/kibana/settings/defaultRoute') - .send({ value: url }) - .expect(400); - } + .send({ value: '/valid' }) + .expect(200); - const { status, header } = await request.get(root, '/'); - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/hello/app/home', + const { status, header } = await request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/hello/valid', + }); }); - }); - - it('consumes valid values', async function () { - await request - .post(root, '/internal/kibana/settings/defaultRoute') - .send({ value: '/valid' }) - .expect(200); - - const { status, header } = await request.get(root, '/'); - expect(status).toEqual(302); - expect(header).toMatchObject({ - location: '/hello/valid', + } else { + it('should have fips enabled, the overrides prevent these tests from working', () => { + expect(getFips()).toBe(1); }); - }); + } }); diff --git a/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts b/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts index d63895220ceb1..c2cb6fccc3cd0 100644 --- a/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts +++ b/src/core/server/integration_tests/elasticsearch/version_compatibility.test.ts @@ -17,6 +17,7 @@ import { firstValueFrom, Subject } from 'rxjs'; import { CliArgs } from '@kbn/config'; import Semver from 'semver'; import { unsafeConsole } from '@kbn/security-hardening'; +import { getFips } from 'crypto'; function nextMinor() { return Semver.inc(esTestConfig.getVersion(), 'minor') || '10.0.0'; @@ -130,9 +131,15 @@ describe('Version Compatibility', () => { ); }); - it('should ignore version mismatch when running on serverless mode and complete startup', async () => { - await expect( - startServers({ customKibanaVersion: nextMinor(), cliArgs: { serverless: true } }) - ).resolves.toBeUndefined(); - }); + if (getFips() === 0) { + it('should ignore version mismatch when running on serverless mode and complete startup', async () => { + await expect( + startServers({ customKibanaVersion: nextMinor(), cliArgs: { serverless: true } }) + ).resolves.toBeUndefined(); + }); + } else { + it('fips is enabled, serverless doesnt like the config overrides', () => { + expect(getFips()).toBe(1); + }); + } }); diff --git a/src/core/server/integration_tests/node/migrator.test.ts b/src/core/server/integration_tests/node/migrator.test.ts index cd4ab1cd8c74e..f899d7da5cde0 100644 --- a/src/core/server/integration_tests/node/migrator.test.ts +++ b/src/core/server/integration_tests/node/migrator.test.ts @@ -16,6 +16,7 @@ import { ToolingLog } from '@kbn/tooling-log'; import { createTestEsCluster, kibanaServerTestUser } from '@kbn/test'; import { observeLines } from '@kbn/stdio-dev-helpers'; import { REPO_ROOT } from '@kbn/repo-info'; +import { getFips } from 'crypto'; describe('migrator-only node', () => { const log = new ToolingLog({ writeTo: process.stdout, level: 'debug' }); @@ -30,6 +31,7 @@ describe('migrator-only node', () => { let logsSub: undefined | Rx.Subscription; try { await es.start(); + const isFipsEnabled = getFips(); proc = ChildProcess.spawn( process.execPath, @@ -42,7 +44,7 @@ describe('migrator-only node', () => { '--no-optimizer', '--no-base-path', '--no-watch', - '--oss', + isFipsEnabled ? '--xpack.security.experimental.fipsMode.enabled=true' : '--oss', ], { stdio: ['pipe', 'pipe', 'pipe'] } ); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts index 490dea4c06be6..6898962077b9c 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/multiple_es_nodes.test.ts @@ -18,6 +18,7 @@ import { } from '@kbn/core-test-helpers-kbn-server'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { Root } from '@kbn/core-root-server-internal'; +import { getFips } from 'crypto'; const LOG_FILE_PREFIX = 'migration_test_multiple_es_nodes'; @@ -114,89 +115,95 @@ describe('migration v2', () => { } }); - it('migrates saved objects normally with multiple ES nodes', async () => { - const { startES } = createTestServers({ - adjustTimeout: (t: number) => jest.setTimeout(t), - settings: { - es: { - license: 'basic', - clusterName: 'es-test-cluster', - nodes: [ - { - name: 'node-01', - // original SO (5000 total; 2500 of type `foo` + 2500 of type `bar`): - // [ - // { id: 'foo:1', type: 'foo', foo: { status: 'not_migrated_1' } }, - // { id: 'bar:1', type: 'bar', bar: { status: 'not_migrated_1' } }, - // { id: 'foo:2', type: 'foo', foo: { status: 'not_migrated_2' } }, - // { id: 'bar:2', type: 'bar', bar: { status: 'not_migrated_2' } }, - // ]; - dataArchive: Path.join(__dirname, '..', 'archives', '7.13.0_5k_so_node_01.zip'), - }, - { - name: 'node-02', - dataArchive: Path.join(__dirname, '..', 'archives', '7.13.0_5k_so_node_02.zip'), - }, - ], + if (getFips() === 0) { + it('migrates saved objects normally with multiple ES nodes', async () => { + const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + clusterName: 'es-test-cluster', + nodes: [ + { + name: 'node-01', + // original SO (5000 total; 2500 of type `foo` + 2500 of type `bar`): + // [ + // { id: 'foo:1', type: 'foo', foo: { status: 'not_migrated_1' } }, + // { id: 'bar:1', type: 'bar', bar: { status: 'not_migrated_1' } }, + // { id: 'foo:2', type: 'foo', foo: { status: 'not_migrated_2' } }, + // { id: 'bar:2', type: 'bar', bar: { status: 'not_migrated_2' } }, + // ]; + dataArchive: Path.join(__dirname, '..', 'archives', '7.13.0_5k_so_node_01.zip'), + }, + { + name: 'node-02', + dataArchive: Path.join(__dirname, '..', 'archives', '7.13.0_5k_so_node_02.zip'), + }, + ], + }, }, - }, - }); - - esServer = await startES(); - - root = createRoot({ - logFileName: Path.join(__dirname, `${LOG_FILE_PREFIX}.log`), - hosts: esServer.hosts, - }); - - await root.preboot(); - const setup = await root.setup(); - setup.savedObjects.registerType({ - name: 'foo', - hidden: false, - mappings: { properties: { status: { type: 'text' } } }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => { - if (doc.attributes?.status) { - doc.attributes.status = doc.attributes.status.replace('not_migrated', 'migrated'); - } - return doc; + }); + + esServer = await startES(); + + root = createRoot({ + logFileName: Path.join(__dirname, `${LOG_FILE_PREFIX}.log`), + hosts: esServer.hosts, + }); + + await root.preboot(); + const setup = await root.setup(); + setup.savedObjects.registerType({ + name: 'foo', + hidden: false, + mappings: { properties: { status: { type: 'text' } } }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => { + if (doc.attributes?.status) { + doc.attributes.status = doc.attributes.status.replace('not_migrated', 'migrated'); + } + return doc; + }, }, - }, - }); - setup.savedObjects.registerType({ - name: 'bar', - hidden: false, - mappings: { properties: { status: { type: 'text' } } }, - namespaceType: 'agnostic', - migrations: { - '7.14.0': (doc) => { - if (doc.attributes?.status) { - doc.attributes.status = doc.attributes.status.replace('not_migrated', 'migrated'); - } - return doc; + }); + setup.savedObjects.registerType({ + name: 'bar', + hidden: false, + mappings: { properties: { status: { type: 'text' } } }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => { + if (doc.attributes?.status) { + doc.attributes.status = doc.attributes.status.replace('not_migrated', 'migrated'); + } + return doc; + }, }, - }, + }); + + await root.start(); + const esClient = esServer.es.getClient(); + + const migratedFooDocs = await fetchDocs(esClient, migratedIndexAlias, 'foo'); + expect(migratedFooDocs.length).toBe(2500); + migratedFooDocs.forEach((doc, i) => { + expect(doc.id).toBe(`foo:${i}`); + expect(doc.foo.status).toBe(`migrated_${i}`); + expect(doc.typeMigrationVersion).toBe('7.14.0'); + }); + + const migratedBarDocs = await fetchDocs(esClient, migratedIndexAlias, 'bar'); + expect(migratedBarDocs.length).toBe(2500); + migratedBarDocs.forEach((doc, i) => { + expect(doc.id).toBe(`bar:${i}`); + expect(doc.bar.status).toBe(`migrated_${i}`); + expect(doc.typeMigrationVersion).toBe('7.14.0'); + }); }); - - await root.start(); - const esClient = esServer.es.getClient(); - - const migratedFooDocs = await fetchDocs(esClient, migratedIndexAlias, 'foo'); - expect(migratedFooDocs.length).toBe(2500); - migratedFooDocs.forEach((doc, i) => { - expect(doc.id).toBe(`foo:${i}`); - expect(doc.foo.status).toBe(`migrated_${i}`); - expect(doc.typeMigrationVersion).toBe('7.14.0'); - }); - - const migratedBarDocs = await fetchDocs(esClient, migratedIndexAlias, 'bar'); - expect(migratedBarDocs.length).toBe(2500); - migratedBarDocs.forEach((doc, i) => { - expect(doc.id).toBe(`bar:${i}`); - expect(doc.bar.status).toBe(`migrated_${i}`); - expect(doc.typeMigrationVersion).toBe('7.14.0'); + } else { + it('skips the test when running in FIPS mode since the data archives cause the es nodes to run with a basic license', () => { + expect(getFips()).toBe(1); }); - }); + } }); diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts index df809d8c4c173..9f970ed234d71 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/read_batch_size.test.ts @@ -15,6 +15,7 @@ import { } from '@kbn/core-test-helpers-kbn-server'; import { clearLog, readLog, startElasticsearch } from '../kibana_migrator_test_kit'; import { delay } from '../test_utils'; +import { getFips } from 'crypto'; const logFilePath = join(__dirname, 'read_batch_size.log'); @@ -36,33 +37,39 @@ describe('migration v2 - read batch size', () => { await delay(5); // give it a few seconds... cause we always do ¯\_(ツ)_/¯ }); - it('reduces the read batchSize in half if a batch exceeds maxReadBatchSizeBytes', async () => { - root = createRoot({ maxReadBatchSizeBytes: 15000 }); - await root.preboot(); - await root.setup(); - await root.start(); + if (getFips() === 0) { + it('reduces the read batchSize in half if a batch exceeds maxReadBatchSizeBytes', async () => { + root = createRoot({ maxReadBatchSizeBytes: 15000 }); + await root.preboot(); + await root.setup(); + await root.start(); - // Check for migration steps present in the logs - logs = await readLog(logFilePath); + // Check for migration steps present in the logs + logs = await readLog(logFilePath); - expect(logs).toMatch( - /Read a batch with a response content length of \d+ bytes which exceeds migrations\.maxReadBatchSizeBytes, retrying by reducing the batch size in half to 15/ - ); - expect(logs).toMatch('[.kibana] Migration completed'); - }); + expect(logs).toMatch( + /Read a batch with a response content length of \d+ bytes which exceeds migrations\.maxReadBatchSizeBytes, retrying by reducing the batch size in half to 15/ + ); + expect(logs).toMatch('[.kibana] Migration completed'); + }); - it('does not reduce the read batchSize in half if no batches exceeded maxReadBatchSizeBytes', async () => { - root = createRoot({ maxReadBatchSizeBytes: 50000 }); - await root.preboot(); - await root.setup(); - await root.start(); + it('does not reduce the read batchSize in half if no batches exceeded maxReadBatchSizeBytes', async () => { + root = createRoot({ maxReadBatchSizeBytes: 50000 }); + await root.preboot(); + await root.setup(); + await root.start(); - // Check for migration steps present in the logs - logs = await readLog(logFilePath); + // Check for migration steps present in the logs + logs = await readLog(logFilePath); - expect(logs).not.toMatch('retrying by reducing the batch size in half to'); - expect(logs).toMatch('[.kibana] Migration completed'); - }); + expect(logs).not.toMatch('retrying by reducing the batch size in half to'); + expect(logs).toMatch('[.kibana] Migration completed'); + }); + } else { + it('cannot run tests with dataArchives that have a basic licesne in FIPS mode', () => { + expect(getFips()).toBe(1); + }); + } }); function createRoot({ maxReadBatchSizeBytes }: { maxReadBatchSizeBytes?: number }) { diff --git a/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts b/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts index e76bbd8d2d65b..27af749593e70 100644 --- a/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts +++ b/src/core/server/integration_tests/saved_objects/serverless/migrations/smoke.test.ts @@ -13,28 +13,35 @@ import { TestServerlessKibanaUtils, createTestServerlessInstances, } from '@kbn/core-test-helpers-kbn-server'; +import { getFips } from 'crypto'; -describe('Basic smoke test', () => { +describe('Basic smoke test', function () { let serverlessES: TestServerlessESUtils; let serverlessKibana: TestServerlessKibanaUtils; let root: TestServerlessKibanaUtils['root']; - beforeEach(async () => { - const { startES, startKibana } = createTestServerlessInstances({ - adjustTimeout: jest.setTimeout, + if (getFips() === 0) { + beforeEach(async () => { + const { startES, startKibana } = createTestServerlessInstances({ + adjustTimeout: jest.setTimeout, + }); + serverlessES = await startES(); + serverlessKibana = await startKibana(); + root = serverlessKibana.root; }); - serverlessES = await startES(); - serverlessKibana = await startKibana(); - root = serverlessKibana.root; - }); - afterEach(async () => { - await serverlessES?.stop(); - await serverlessKibana?.stop(); - }); + afterEach(async () => { + await serverlessES?.stop(); + await serverlessKibana?.stop(); + }); - test('it can start Kibana running against serverless ES', async () => { - const { body } = await request.get(root, '/api/status').expect(200); - expect(body).toMatchObject({ status: { overall: { level: 'available' } } }); - }); + test('it can start Kibana running against serverless ES', async () => { + const { body } = await request.get(root, '/api/status').expect(200); + expect(body).toMatchObject({ status: { overall: { level: 'available' } } }); + }); + } else { + test('FIPS is enabled, serverless doesnt like the config overrides', () => { + expect(getFips()).toBe(1); + }); + } }); diff --git a/src/dev/build/lib/integration_tests/fs.test.ts b/src/dev/build/lib/integration_tests/fs.test.ts index 2b1adac411f65..4a24da0e0b5f6 100644 --- a/src/dev/build/lib/integration_tests/fs.test.ts +++ b/src/dev/build/lib/integration_tests/fs.test.ts @@ -13,6 +13,7 @@ import { chmodSync, statSync } from 'fs'; import del from 'del'; import { mkdirp, write, read, getChildPaths, copyAll, getFileHash, untar, gunzip } from '../fs'; +import { getFips } from 'crypto'; const TMP = resolve(__dirname, '../__tmp__'); const FIXTURES = resolve(__dirname, '../__fixtures__'); @@ -266,9 +267,12 @@ describe('getFileHash()', () => { '7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730' ); }); - it('resolves with the md5 hash of a file', async () => { - expect(await getFileHash(BAR_TXT_PATH, 'md5')).toBe('c157a79031e1c40f85931829bc5fc552'); - }); + + if (getFips() !== 1) { + it('resolves with the md5 hash of a file', async () => { + expect(await getFileHash(BAR_TXT_PATH, 'md5')).toBe('c157a79031e1c40f85931829bc5fc552'); + }); + } }); describe('untar()', () => { diff --git a/src/plugins/files/server/blob_storage_service/adapters/es/integration_tests/es.test.ts b/src/plugins/files/server/blob_storage_service/adapters/es/integration_tests/es.test.ts index afbae27ec367d..ab440e60629b5 100644 --- a/src/plugins/files/server/blob_storage_service/adapters/es/integration_tests/es.test.ts +++ b/src/plugins/files/server/blob_storage_service/adapters/es/integration_tests/es.test.ts @@ -27,10 +27,13 @@ describe('Elasticsearch blob storage', () => { beforeAll(async () => { ElasticsearchBlobStorageClient.configureConcurrentTransfers(Infinity); - const { startES, startKibana } = createTestServers({ adjustTimeout: jest.setTimeout }); + + const { startES, startKibana } = createTestServers({ + adjustTimeout: jest.setTimeout, + }); manageES = await startES(); manageKbn = await startKibana(); - esClient = manageKbn.coreStart.elasticsearch.client.asInternalUser; + esClient = manageKbn.coreStart.elasticsearch.createClient('es.test').asInternalUser; }); afterAll(async () => { diff --git a/src/plugins/files/server/file_client/create_es_file_client.ts b/src/plugins/files/server/file_client/create_es_file_client.ts index ddfcfc0833e80..3302e878c3631 100644 --- a/src/plugins/files/server/file_client/create_es_file_client.ts +++ b/src/plugins/files/server/file_client/create_es_file_client.ts @@ -8,6 +8,7 @@ */ import type { Logger, ElasticsearchClient } from '@kbn/core/server'; +import { getFips } from 'crypto'; import { ElasticsearchBlobStorageClient } from '../blob_storage_service'; import { FileClientImpl } from './file_client'; import type { FileClient } from './types'; @@ -66,12 +67,18 @@ export function createEsFileClient(arg: CreateEsFileClientArgs): FileClient { maxSizeBytes, indexIsAlias, } = arg; + + let hashes: Array<'sha1' | 'sha256' | 'sha512' | 'md5'> = ['sha1', 'sha256', 'sha512']; + if (getFips() !== 1) { + hashes = ['md5', ...hashes]; + } + return new FileClientImpl( { id: NO_FILE_KIND, http: {}, maxSizeBytes, - hashes: ['md5', 'sha1', 'sha256', 'sha512'], + hashes, }, new EsIndexFilesMetadataClient(metadataIndex, elasticsearchClient, logger, indexIsAlias), new ElasticsearchBlobStorageClient( diff --git a/src/plugins/files/server/file_client/integration_tests/es_file_client.test.ts b/src/plugins/files/server/file_client/integration_tests/es_file_client.test.ts index 5ae9967b84b1d..dfe51dcb7ff72 100644 --- a/src/plugins/files/server/file_client/integration_tests/es_file_client.test.ts +++ b/src/plugins/files/server/file_client/integration_tests/es_file_client.test.ts @@ -13,6 +13,7 @@ import { TestEnvironmentUtils, setupIntegrationEnvironment } from '../../test_ut import { createEsFileClient } from '../create_es_file_client'; import { FileClient } from '../types'; import { FileMetadata } from '../../../common'; +import { getFips } from 'crypto'; describe('ES-index-backed file client', () => { let esClient: TestEnvironmentUtils['esClient']; @@ -107,13 +108,21 @@ describe('ES-index-backed file client', () => { }); await file.uploadContent(Readable.from([Buffer.from('test')])); - expect(file.toJSON().hash).toStrictEqual({ - md5: '098f6bcd4621d373cade4e832627b4f6', + let expected: Record = { sha1: 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', sha512: 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff', - }); + }; + + if (getFips() !== 1) { + expected = { + md5: '098f6bcd4621d373cade4e832627b4f6', + ...expected, + }; + } + + expect(file.toJSON().hash).toStrictEqual(expected); await deleteFile({ id: file.id, hasContent: true }); }); diff --git a/src/plugins/files/server/file_client/stream_transforms/file_hash_transform/file_hash_transform.test.ts b/src/plugins/files/server/file_client/stream_transforms/file_hash_transform/file_hash_transform.test.ts index f0801cf763dd0..18f0cd6184e71 100644 --- a/src/plugins/files/server/file_client/stream_transforms/file_hash_transform/file_hash_transform.test.ts +++ b/src/plugins/files/server/file_client/stream_transforms/file_hash_transform/file_hash_transform.test.ts @@ -24,6 +24,8 @@ import { BlobStorageService } from '../../../blob_storage_service'; import { InternalFileShareService } from '../../../file_share_service'; import { InternalFileService } from '../../../file_service/internal_file_service'; +import { getFips } from 'crypto'; + describe('When using the FileHashTransform', () => { let file: IFile; let fileContent: Readable; @@ -75,23 +77,25 @@ describe('When using the FileHashTransform', () => { expect(() => fileHash.getFileHash()).toThrow('File hash generation not yet complete'); }); - it.each([ - ['md5', '098f6bcd4621d373cade4e832627b4f6'], - ['sha1', 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'], - ['sha256', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'], - [ - 'sha512', - 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff', - ], - ] as Array<[SupportedFileHashAlgorithm, string]>)( - 'should generate file hash using algorithm: %s', - async (algorithm, expectedHash) => { - const fileHash = createFileHashTransform(algorithm); - await file.uploadContent(fileContent, undefined, { - transforms: [fileHash], - }); + describe('algorithms', function () { + it.each([ + ...(getFips() !== 1 ? [['md5', '098f6bcd4621d373cade4e832627b4f6']] : []), + ['sha1', 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'], + ['sha256', '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'], + [ + 'sha512', + 'ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff', + ], + ] as Array<[SupportedFileHashAlgorithm, string]>)( + 'should generate file hash using algorithm: %s', + async (algorithm, expectedHash) => { + const fileHash = createFileHashTransform(algorithm); + await file.uploadContent(fileContent, undefined, { + transforms: [fileHash], + }); - expect(fileHash.getFileHash()).toEqual({ algorithm, value: expectedHash }); - } - ); + expect(fileHash.getFileHash()).toEqual({ algorithm, value: expectedHash }); + } + ); + }); }); diff --git a/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts b/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts index 1685228699fc3..abd6b1c5dd1c1 100644 --- a/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts +++ b/test/functional/apps/dashboard/group5/dashboard_panel_listing.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const dashboardAddPanel = getService('dashboardAddPanel'); - describe('dashboard panel listing', () => { + describe('dashboard panel listing', function () { + this.tags('skipFIPS'); before(async () => { await kibanaServer.savedObjects.cleanStandardList(); await kibanaServer.importExport.load( diff --git a/test/server_integration/http/ssl_with_p12/index.js b/test/server_integration/http/ssl_with_p12/index.js index b402ce548c6a1..21c09a8d7e63f 100644 --- a/test/server_integration/http/ssl_with_p12/index.js +++ b/test/server_integration/http/ssl_with_p12/index.js @@ -10,7 +10,8 @@ export default function ({ getService }) { const supertest = getService('supertest'); - describe('kibana server with ssl', () => { + describe('kibana server with ssl', function () { + this.tags('skipFIPS'); it('handles requests using ssl with a P12 keystore', async () => { await supertest.get('/').expect(302); }); diff --git a/test/server_integration/http/ssl_with_p12_intermediate/index.js b/test/server_integration/http/ssl_with_p12_intermediate/index.js index b01df762c7345..0fc4a2f793e20 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/index.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/index.js @@ -10,7 +10,8 @@ export default function ({ getService }) { const supertest = getService('supertest'); - describe('kibana server with ssl', () => { + describe('kibana server with ssl', function () { + this.tags('skipFIPS'); it('handles requests using ssl with a P12 keystore that uses an intermediate CA', async () => { await supertest.get('/').expect(302); }); diff --git a/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts index 3a4101bb9f152..5130762bb3b20 100644 --- a/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts +++ b/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts @@ -28,6 +28,7 @@ import { DEFAULT_MICROSOFT_GRAPH_API_SCOPE, DEFAULT_MICROSOFT_GRAPH_API_URL, } from '../../common'; +import { getFips } from 'crypto'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -251,19 +252,6 @@ describe('axios connections', () => { expect(res.status).toBe(200); }); - test('it works with pfx and passphrase in SSL overrides', async () => { - const { url, server } = await createServer({ useHttps: true, requestCert: true }); - testServer = server; - - const configurationUtilities = getACUfromConfig(); - const sslOverrides = { - pfx: KIBANA_P12, - passphrase: 'storepass', - }; - const res = await request({ axios, url, logger, configurationUtilities, sslOverrides }); - expect(res.status).toBe(200); - }); - test('it fails with cert and key but no ca in SSL overrides', async () => { const { url, server } = await createServer({ useHttps: true, requestCert: true }); testServer = server; @@ -278,18 +266,33 @@ describe('axios connections', () => { await expect(fn()).rejects.toThrow('certificate'); }); - test('it fails with pfx but no passphrase in SSL overrides', async () => { - const { url, server } = await createServer({ useHttps: true, requestCert: true }); - testServer = server; + if (getFips() !== 1) { + test('it works with pfx and passphrase in SSL overrides', async () => { + const { url, server } = await createServer({ useHttps: true, requestCert: true }); + testServer = server; + + const configurationUtilities = getACUfromConfig(); + const sslOverrides = { + pfx: KIBANA_P12, + passphrase: 'storepass', + }; + const res = await request({ axios, url, logger, configurationUtilities, sslOverrides }); + expect(res.status).toBe(200); + }); - const configurationUtilities = getACUfromConfig(); - const sslOverrides = { - pfx: KIBANA_P12, - }; - const fn = async () => - await request({ axios, url, logger, configurationUtilities, sslOverrides }); - await expect(fn()).rejects.toThrow('mac verify'); - }); + test('it fails with pfx but no passphrase in SSL overrides', async () => { + const { url, server } = await createServer({ useHttps: true, requestCert: true }); + testServer = server; + + const configurationUtilities = getACUfromConfig(); + const sslOverrides = { + pfx: KIBANA_P12, + }; + const fn = async () => + await request({ axios, url, logger, configurationUtilities, sslOverrides }); + await expect(fn()).rejects.toThrow('mac verify'); + }); + } test('it fails with a client-side certificate issued by an invalid ca', async () => { const { url, server } = await createServer({ useHttps: true, requestCert: true }); diff --git a/x-pack/plugins/fleet/.storybook/smoke.test.tsx b/x-pack/plugins/fleet/.storybook/smoke.test.tsx index a3984a42a5ab2..63c1199b75aa9 100644 --- a/x-pack/plugins/fleet/.storybook/smoke.test.tsx +++ b/x-pack/plugins/fleet/.storybook/smoke.test.tsx @@ -5,22 +5,30 @@ * 2.0. */ +import { getFips } from 'crypto'; + import { mount } from 'enzyme'; import { createElement } from 'react'; import { act } from 'react-dom/test-utils'; import initStoryshots from '@storybook/addon-storyshots'; -describe('Fleet Storybook Smoke', () => { - test('Init', async () => { - await initStoryshots({ - configPath: __dirname, - framework: 'react', - test: async ({ story }) => { - const renderer = mount(createElement(story.render)); - // wait until the element will perform all renders and resolve all promises (lazy loading, especially) - await act(() => new Promise((resolve) => setTimeout(resolve, 0))); - expect(renderer.html()).not.toContain('euiErrorBoundary'); - }, +describe('Fleet Storybook Smoke', function () { + if (getFips() !== 1) { + test('Init', async () => { + await initStoryshots({ + configPath: __dirname, + framework: 'react', + test: async ({ story }) => { + const renderer = mount(createElement(story.render)); + // wait until the element will perform all renders and resolve all promises (lazy loading, especially) + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + expect(renderer.html()).not.toContain('euiErrorBoundary'); + }, + }); + }); + } else { + test('fips is enabled', function () { + expect(getFips() === 1).toEqual(true); }); - }); + } }); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts index cff6deb6a24a3..3539fbc2c97a3 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts @@ -153,7 +153,6 @@ export async function hashSecret(secret: string) { return `${salt}:${derivedKey.toString('hex')}`; } - async function verifySecret(hash: string, secret: string) { const [salt, key] = hash.split(':'); const derivedKey = await pbkdf2Async(secret, salt, maxIteration, keyLength, 'sha512'); diff --git a/x-pack/test/functional/apps/index_lifecycle_management/read_only_view.ts b/x-pack/test/functional/apps/index_lifecycle_management/read_only_view.ts index 030074a97b4bd..b30ee9ecee763 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/read_only_view.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/read_only_view.ts @@ -15,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); describe('Read only view', function () { + this.tags('skipFIPS'); before(async () => { await security.testUser.setRoles(['read_ilm']); From 7575f42ebf6f17abe8a236cfbec3b527a6306405 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 06:07:43 +1100 Subject: [PATCH 31/42] [8.x] Authorized route migration for routes owned by security-solution (#198382) (#200782) # Backport This will backport the following commits from `main` to `8.x`: - [Authorized route migration for routes owned by security-solution (#198382)](https://github.com/elastic/kibana/pull/198382) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .../server/lib/dashboards/routes/get_dashboards_by_tags.ts | 6 ++++-- .../routes/privileges/read_privileges_route.ts | 6 ++++-- .../telemetry/telemetry_detection_rules_preview_route.ts | 6 ++++-- .../routes/users/suggest_user_profiles_route.ts | 6 ++++-- .../server/lib/exceptions/api/manage_exceptions/route.ts | 6 ++++-- .../security_solution/server/lib/tags/routes/create_tag.ts | 6 ++++-- .../server/lib/tags/routes/get_tags_by_name.ts | 6 ++++-- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts index dda4a6af5d221..28e823874b529 100644 --- a/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/dashboards/routes/get_dashboards_by_tags.ts @@ -21,8 +21,10 @@ export const getDashboardsByTagsRoute = (router: SecuritySolutionPluginRouter, l .post({ path: INTERNAL_DASHBOARDS_URL, access: 'internal', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( 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 314d2c273b04a..22c031d5d5eb5 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 @@ -22,8 +22,10 @@ export const readPrivilegesRoute = ( .get({ path: DETECTION_ENGINE_PRIVILEGES_URL, access: 'public', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts index 271e6e7d27749..8013b2af9742b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/telemetry/telemetry_detection_rules_preview_route.ts @@ -26,8 +26,10 @@ export const telemetryDetectionRulesPreviewRoute = ( .get({ path: SECURITY_TELEMETRY_URL, access: 'internal', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/users/suggest_user_profiles_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/users/suggest_user_profiles_route.ts index 1b1aeada05660..2b8f65af12ca5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/users/suggest_user_profiles_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/users/suggest_user_profiles_route.ts @@ -23,8 +23,10 @@ export const suggestUserProfilesRoute = ( .get({ path: DETECTION_ENGINE_ALERT_SUGGEST_USERS_URL, access: 'internal', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/exceptions/api/manage_exceptions/route.ts b/x-pack/plugins/security_solution/server/lib/exceptions/api/manage_exceptions/route.ts index 01a04a284b16a..5b2a3a70be1a2 100644 --- a/x-pack/plugins/security_solution/server/lib/exceptions/api/manage_exceptions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/exceptions/api/manage_exceptions/route.ts @@ -23,8 +23,10 @@ export const createSharedExceptionListRoute = (router: SecuritySolutionPluginRou .post({ path: SHARED_EXCEPTION_LIST_URL, access: 'public', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts index 1604b4374b984..8b76d6b380893 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/create_tag.ts @@ -21,8 +21,10 @@ export const createTagRoute = (router: SecuritySolutionPluginRouter, logger: Log .put({ path: INTERNAL_TAGS_URL, access: 'internal', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( diff --git a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts index 75ae24d0eacd5..dc5a9da70c71f 100644 --- a/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts +++ b/x-pack/plugins/security_solution/server/lib/tags/routes/get_tags_by_name.ts @@ -21,8 +21,10 @@ export const getTagsByNameRoute = (router: SecuritySolutionPluginRouter, logger: .get({ path: INTERNAL_TAGS_URL, access: 'internal', - options: { - tags: ['access:securitySolution'], + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, }, }) .addVersion( From 4a9f70d814d98487c13f7e5743bb8cd4433e6c22 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:07:21 +1100 Subject: [PATCH 32/42] [8.x] [Fleet] Filter integrations/packages list shown depending on the `policy_templates_behavior` field (#200605) (#200749) # Backport This will backport the following commits from `main` to `8.x`: - [[Fleet] Filter integrations/packages list shown depending on the `policy_templates_behavior` field (#200605)](https://github.com/elastic/kibana/pull/200605) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --------- Co-authored-by: Mario Rodriguez Molins --- x-pack/plugins/fleet/common/services/index.ts | 1 + .../common/services/policy_template.test.ts | 160 ++++++++++++++ .../fleet/common/services/policy_template.ts | 25 +++ .../plugins/fleet/common/types/models/epm.ts | 1 + .../fleet/common/types/models/package_spec.ts | 1 + .../home/hooks/use_available_packages.tsx | 50 +++-- .../fleet/public/search_provider.test.ts | 200 ++++++++++++++++++ .../plugins/fleet/public/search_provider.ts | 9 +- 8 files changed, 421 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 4443878617796..7061d6d3028d8 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -33,6 +33,7 @@ export { isIntegrationPolicyTemplate, getNormalizedInputs, getNormalizedDataStreams, + filterPolicyTemplatesTiles, } from './policy_template'; export { doesPackageHaveIntegrations } from './packages_with_integrations'; export type { diff --git a/x-pack/plugins/fleet/common/services/policy_template.test.ts b/x-pack/plugins/fleet/common/services/policy_template.test.ts index 87ce2121b8b59..4065d0c863c9e 100644 --- a/x-pack/plugins/fleet/common/services/policy_template.test.ts +++ b/x-pack/plugins/fleet/common/services/policy_template.test.ts @@ -10,6 +10,7 @@ import type { RegistryPolicyIntegrationTemplate, PackageInfo, RegistryVarType, + PackageListItem, } from '../types'; import { @@ -17,6 +18,7 @@ import { isIntegrationPolicyTemplate, getNormalizedInputs, getNormalizedDataStreams, + filterPolicyTemplatesTiles, } from './policy_template'; describe('isInputOnlyPolicyTemplate', () => { @@ -280,3 +282,161 @@ describe('getNormalizedDataStreams', () => { expect(result?.[0].streams?.[0]?.vars).toEqual([datasetVar]); }); }); + +describe('filterPolicyTemplatesTiles', () => { + const topPackagePolicy: PackageListItem = { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx', + download: 'nginx', + path: 'nginx', + }; + + const childPolicyTemplates: PackageListItem[] = [ + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-1', + download: 'nginx-template-1', + path: 'nginx-template-1', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-2', + download: 'nginx-template-2', + path: 'nginx-template-2', + }, + ]; + it('should return all tiles as undefined value', () => { + expect(filterPolicyTemplatesTiles(undefined, topPackagePolicy, childPolicyTemplates)).toEqual([ + { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx', + download: 'nginx', + path: 'nginx', + }, + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-1', + download: 'nginx-template-1', + path: 'nginx-template-1', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-2', + download: 'nginx-template-2', + path: 'nginx-template-2', + }, + ]); + }); + it('should return all tiles', () => { + expect(filterPolicyTemplatesTiles('all', topPackagePolicy, childPolicyTemplates)).toEqual([ + { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx', + download: 'nginx', + path: 'nginx', + }, + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-1', + download: 'nginx-template-1', + path: 'nginx-template-1', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-2', + download: 'nginx-template-2', + path: 'nginx-template-2', + }, + ]); + }); + it('should return just the combined policy tile', () => { + expect( + filterPolicyTemplatesTiles('combined_policy', topPackagePolicy, childPolicyTemplates) + ).toEqual([ + { + id: 'nginx', + integration: 'nginx', + title: 'Nginx', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx', + download: 'nginx', + path: 'nginx', + }, + ]); + }); + it('should return just the individual policies (tiles)', () => { + expect( + filterPolicyTemplatesTiles('individual_policies', topPackagePolicy, childPolicyTemplates) + ).toEqual([ + { + id: 'nginx-template1', + integration: 'nginx-template-1', + title: 'Nginx Template 1', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-1', + download: 'nginx-template-1', + path: 'nginx-template-1', + }, + { + id: 'nginx-template2', + integration: 'nginx-template-2', + title: 'Nginx Template 2', + name: 'nginx', + version: '0.0.1', + status: 'not_installed', + description: 'nginx-template-2', + download: 'nginx-template-2', + path: 'nginx-template-2', + }, + ]); + }); +}); diff --git a/x-pack/plugins/fleet/common/services/policy_template.ts b/x-pack/plugins/fleet/common/services/policy_template.ts index ed390e0c6b45d..efa65a880576a 100644 --- a/x-pack/plugins/fleet/common/services/policy_template.ts +++ b/x-pack/plugins/fleet/common/services/policy_template.ts @@ -39,6 +39,7 @@ export function packageHasNoPolicyTemplates(packageInfo: PackageInfo): boolean { ) ); } + export function isInputOnlyPolicyTemplate( policyTemplate: RegistryPolicyTemplate ): policyTemplate is RegistryPolicyInputOnlyTemplate { @@ -142,3 +143,27 @@ const createDefaultDatasetName = ( packageInfo: { name: string }, policyTemplate: { name: string } ): string => packageInfo.name + '.' + policyTemplate.name; + +export function filterPolicyTemplatesTiles( + templatesBehavior: string | undefined, + packagePolicy: T, + packagePolicyTemplates: T[] +): T[] { + switch (templatesBehavior || 'all') { + case 'combined_policy': + return [packagePolicy]; + case 'individual_policies': + return [ + ...(packagePolicyTemplates && packagePolicyTemplates.length > 1 + ? packagePolicyTemplates + : []), + ]; + default: + return [ + packagePolicy, + ...(packagePolicyTemplates && packagePolicyTemplates.length > 1 + ? packagePolicyTemplates + : []), + ]; + } +} diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 45d2908faa0b2..0588300f88dce 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -311,6 +311,7 @@ export type RegistrySearchResult = Pick< | 'icons' | 'internal' | 'data_streams' + | 'policy_templates_behavior' | 'policy_templates' | 'categories' >; diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts index 6d1d61e7dc61c..6968d8b2c1f01 100644 --- a/x-pack/plugins/fleet/common/types/models/package_spec.ts +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -24,6 +24,7 @@ export interface PackageSpecManifest { conditions?: PackageSpecConditions; icons?: PackageSpecIcon[]; screenshots?: PackageSpecScreenshot[]; + policy_templates_behavior?: 'all' | 'combined_policy' | 'individual_policies'; policy_templates?: RegistryPolicyTemplate[]; vars?: RegistryVarsEntry[]; owner: { github: string; type?: 'elastic' | 'partner' | 'community' }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx index 2f506b30b2626..c399a0241c22a 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx @@ -28,6 +28,7 @@ import { doesPackageHaveIntegrations, ExperimentalFeaturesService } from '../../ import { isInputOnlyPolicyTemplate, isIntegrationPolicyTemplate, + filterPolicyTemplatesTiles, } from '../../../../../../../../common/services'; import { @@ -83,30 +84,33 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { categories: getAllCategoriesFromIntegrations(pkg), }; - return [ - ...acc, + const integrationsPolicyTemplates = doesPackageHaveIntegrations(pkg) + ? policyTemplates.map((policyTemplate) => { + const { name, title, description, icons } = policyTemplate; + + const categories = + isIntegrationPolicyTemplate(policyTemplate) && policyTemplate.categories + ? policyTemplate.categories + : []; + const allCategories = [...topCategories, ...categories]; + return { + ...restOfPackage, + id: `${restOfPackage.id}-${name}`, + integration: name, + title, + description, + icons: icons || restOfPackage.icons, + categories: uniq(allCategories), + }; + }) + : []; + + const tiles = filterPolicyTemplatesTiles( + pkg.policy_templates_behavior, topPackage, - ...(doesPackageHaveIntegrations(pkg) - ? policyTemplates.map((policyTemplate) => { - const { name, title, description, icons } = policyTemplate; - - const categories = - isIntegrationPolicyTemplate(policyTemplate) && policyTemplate.categories - ? policyTemplate.categories - : []; - const allCategories = [...topCategories, ...categories]; - return { - ...restOfPackage, - id: `${restOfPackage.id}-${name}`, - integration: name, - title, - description, - icons: icons || restOfPackage.icons, - categories: uniq(allCategories), - }; - }) - : []), - ]; + integrationsPolicyTemplates + ); + return [...acc, ...tiles]; }, []); }; diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index 5f95eef60546c..4228cca993b46 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -112,6 +112,123 @@ const testResponse: GetPackagesResponse['items'] = [ }, ]; +const testResponseBehaviorField: GetPackagesResponse['items'] = [ + { + description: 'testWithPolicyTemplateBehaviorAll', + download: 'testWithPolicyTemplateBehaviorAll', + id: 'testWithPolicyTemplateBehaviorAll', + name: 'testWithPolicyTemplateBehaviorAll', + path: 'testWithPolicyTemplateBehaviorAll', + release: 'ga', + status: 'not_installed', + title: 'testWithPolicyTemplateBehaviorAll', + version: 'testWithPolicyTemplateBehaviorAll', + policy_templates_behavior: 'all', + policy_templates: [ + { + description: 'testPolicyTemplate1BehaviorAll', + name: 'testPolicyTemplate1BehaviorAll', + icons: [ + { + src: 'testPolicyTemplate1BehaviorAll', + path: 'testPolicyTemplate1BehaviorAll', + }, + ], + title: 'testPolicyTemplate1BehaviorAll', + type: 'testPolicyTemplate1BehaviorAll', + }, + { + description: 'testPolicyTemplate2BehaviorAll', + name: 'testPolicyTemplate2BehaviorAll', + icons: [ + { + src: 'testPolicyTemplate2BehaviorAll', + path: 'testPolicyTemplate2BehaviorAll', + }, + ], + title: 'testPolicyTemplate2BehaviorAll', + type: 'testPolicyTemplate2BehaviorAll', + }, + ], + }, + { + description: 'testWithPolicyTemplateBehaviorCombined', + download: 'testWithPolicyTemplateBehaviorCombined', + id: 'testWithPolicyTemplateBehaviorCombined', + name: 'testWithPolicyTemplateBehaviorCombined', + path: 'testWithPolicyTemplateBehaviorCombined', + release: 'ga', + status: 'not_installed', + title: 'testWithPolicyTemplateBehaviorCombined', + version: 'testWithPolicyTemplateBehaviorCombined', + policy_templates_behavior: 'combined_policy', + policy_templates: [ + { + description: 'testPolicyTemplate1BehaviorCombined', + name: 'testPolicyTemplate1BehaviorCombined', + icons: [ + { + src: 'testPolicyTemplate1BehaviorCombined', + path: 'testPolicyTemplate1BehaviorCombined', + }, + ], + title: 'testPolicyTemplate1BehaviorCombined', + type: 'testPolicyTemplate1BehaviorCombined', + }, + { + description: 'testPolicyTemplate2BehaviorCombined', + name: 'testPolicyTemplate2BehaviorCombined', + icons: [ + { + src: 'testPolicyTemplate2BehaviorCombined', + path: 'testPolicyTemplate2BehaviorCombined', + }, + ], + title: 'testPolicyTemplate2BehaviorCombined', + type: 'testPolicyTemplate2BehaviorCombined', + }, + ], + }, + { + description: 'testWithPolicyTemplateBehaviorIndividual', + download: 'testWithPolicyTemplateBehaviorIndividual', + id: 'testWithPolicyTemplateBehaviorIndividual', + name: 'testWithPolicyTemplateBehaviorIndividual', + path: 'testWithPolicyTemplateBehaviorIndividual', + release: 'ga', + status: 'not_installed', + title: 'testWithPolicyTemplateBehaviorIndividual', + version: 'testWithPolicyTemplateBehaviorIndividual', + policy_templates_behavior: 'individual_policies', + policy_templates: [ + { + description: 'testPolicyTemplate1BehaviorIndividual', + name: 'testPolicyTemplate1BehaviorIndividual', + icons: [ + { + src: 'testPolicyTemplate1BehaviorIndividual', + path: 'testPolicyTemplate1BehaviorIndividual', + }, + ], + title: 'testPolicyTemplate1BehaviorIndividual', + type: 'testPolicyTemplate1BehaviorIndividual', + }, + { + description: 'testPolicyTemplate2BehaviorIndividual', + name: 'testPolicyTemplate2BehaviorIndividual', + icons: [ + { + src: 'testPolicyTemplate2BehaviorIndividual', + path: 'testPolicyTemplate2BehaviorIndividual', + }, + ], + title: 'testPolicyTemplate2BehaviorIndividual', + type: 'testPolicyTemplate2BehaviorIndividual', + }, + ], + }, +]; + const getTestScheduler = () => { return new TestScheduler((actual, expected) => { return expect(actual).toEqual(expected); @@ -394,6 +511,89 @@ describe('Package search provider', () => { expect(sendGetPackages).toHaveBeenCalledTimes(1); }); + test('with integration tag, with policy_templates_behavior field', () => { + getTestScheduler().run(({ hot, expectObservable }) => { + mockSendGetPackages.mockReturnValue( + hot('--(a|)', { a: { data: { response: testResponseBehaviorField } } }) + ); + setupMock.getStartServices.mockReturnValue( + hot('--(a|)', { a: [coreMock.createStart()] }) as any + ); + const packageSearchProvider = createPackageSearchProvider(setupMock); + expectObservable( + packageSearchProvider.find( + { types: ['integration'] }, + { aborted$: NEVER, maxResults: 100, preference: '' } + ) + ).toBe('--(a|)', { + a: [ + { + id: 'testWithPolicyTemplateBehaviorAll', + score: 80, + title: 'testWithPolicyTemplateBehaviorAll', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorAll/overview', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate1BehaviorAll', + score: 80, + title: 'testPolicyTemplate1BehaviorAll', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorAll/overview?integration=testPolicyTemplate1BehaviorAll', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate2BehaviorAll', + score: 80, + title: 'testPolicyTemplate2BehaviorAll', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorAll/overview?integration=testPolicyTemplate2BehaviorAll', + prependBasePath: false, + }, + }, + { + id: 'testWithPolicyTemplateBehaviorCombined', + score: 80, + title: 'testWithPolicyTemplateBehaviorCombined', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorCombined/overview', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate1BehaviorIndividual', + score: 80, + title: 'testPolicyTemplate1BehaviorIndividual', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorIndividual/overview?integration=testPolicyTemplate1BehaviorIndividual', + prependBasePath: false, + }, + }, + { + id: 'testPolicyTemplate2BehaviorIndividual', + score: 80, + title: 'testPolicyTemplate2BehaviorIndividual', + type: 'integration', + url: { + path: 'undefined/detail/testWithPolicyTemplateBehaviorIndividual/overview?integration=testPolicyTemplate2BehaviorIndividual', + prependBasePath: false, + }, + }, + ], + }); + }); + + expect(sendGetPackages).toHaveBeenCalledTimes(1); + }); + test('with integration tag, with search term', () => { getTestScheduler().run(({ hot, expectObservable }) => { mockSendGetPackages.mockReturnValue( diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index ce1ddd10ac5a2..caeca49c105b5 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -16,6 +16,7 @@ import type { } from '@kbn/global-search-plugin/public'; import { INTEGRATIONS_PLUGIN_ID } from '../common'; +import { filterPolicyTemplatesTiles } from '../common/services'; import { sendGetPackages } from './hooks'; import type { GetPackagesResponse, PackageListItem } from './types'; @@ -74,10 +75,12 @@ export const toSearchResult = ( }) ); - return [ + const tiles = filterPolicyTemplatesTiles( + pkg.policy_templates_behavior, packageResult, - ...(policyTemplateResults && policyTemplateResults.length > 1 ? policyTemplateResults : []), - ]; + policyTemplateResults || [] + ); + return [...tiles]; }; export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResultProvider => { From c501d2f5891d90521036dbfc1b77b6e04a7f0eda Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:13:06 +1100 Subject: [PATCH 33/42] [8.x] [Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898) (#200807) # Backport This will backport the following commits from `main` to `8.x`: - [[Cases] [Security Solution] New cases subfeatures, add comments and reopen cases (#194898)](https://github.com/elastic/kibana/pull/194898) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> --- .../project_roles/security/roles.yml | 24 +- .../serverless_resources/security_roles.json | 14 +- .../features/product_features.ts | 2 +- .../features/src/cases/index.ts | 23 +- .../features/src/cases/types.ts | 1 - .../src/cases/v1_features/kibana_features.ts | 98 +++++ .../{ => v1_features}/kibana_sub_features.ts | 11 +- .../features/src/cases/v1_features/types.ts | 14 + .../{ => v2_features}/kibana_features.ts | 11 +- .../cases/v2_features/kibana_sub_features.ts | 177 +++++++++ .../features/src/constants.ts | 9 + .../features/src/product_features_keys.ts | 2 + .../__snapshots__/cases.test.ts.snap | 14 +- .../feature_privilege_builder/cases.test.ts | 13 +- .../feature_privilege_builder/cases.ts | 10 +- .../cases/common/constants/application.ts | 2 + .../plugins/cases/common/constants/index.ts | 2 + x-pack/plugins/cases/common/index.ts | 4 + x-pack/plugins/cases/common/ui/types.ts | 6 + .../utils/__snapshots__/api_tags.test.ts.snap | 12 +- x-pack/plugins/cases/common/utils/api_tags.ts | 3 +- .../cases/common/utils/capabilities.test.tsx | 7 + .../cases/common/utils/capabilities.ts | 7 + .../client/helpers/can_use_cases.test.ts | 62 +-- .../public/client/helpers/can_use_cases.ts | 14 +- .../client/helpers/capabilities.test.ts | 18 + .../public/client/helpers/capabilities.ts | 17 +- .../common/lib/kibana/__mocks__/index.ts | 2 +- .../public/common/lib/kibana/hooks.test.tsx | 2 +- .../cases/public/common/lib/kibana/hooks.ts | 12 +- .../common/lib/kibana/kibana_react.mock.tsx | 4 +- .../cases/public/common/mock/permissions.ts | 63 ++- .../status/use_should_disable_status.test.tsx | 88 +++++ .../status/use_should_disable_status.tsx | 39 ++ .../actions/status/use_status_action.test.tsx | 46 ++- .../actions/status/use_status_action.tsx | 22 +- .../components/add_comment/index.test.tsx | 28 +- .../public/components/add_comment/index.tsx | 4 +- .../components/all_cases/use_actions.test.tsx | 94 +++++ .../components/all_cases/use_actions.tsx | 20 +- .../all_cases/use_bulk_actions.test.tsx | 73 +++- .../components/all_cases/use_bulk_actions.tsx | 11 +- .../components/all_cases/utility_bar.tsx | 4 +- .../cases/public/components/app/index.tsx | 2 +- .../app/use_available_owners.test.ts | 12 +- .../components/app/use_available_owners.ts | 8 +- .../components/case_action_bar/index.tsx | 10 +- .../status_context_menu.test.tsx | 84 +++- .../case_action_bar/status_context_menu.tsx | 26 +- .../public/components/cases_context/index.tsx | 4 + .../public/components/files/add_file.test.tsx | 14 +- .../public/components/files/add_file.tsx | 2 +- .../components/recent_cases/index.test.tsx | 4 +- .../public/components/user_actions/index.tsx | 8 +- .../use_user_permissions.test.tsx | 259 ++++++++++++ .../user_actions/use_user_permissions.tsx | 38 ++ .../public/containers/use_get_cases.test.tsx | 4 +- x-pack/plugins/cases/public/mocks.ts | 2 + .../__snapshots__/audit_logger.test.ts.snap | 84 ++++ .../__snapshots__/authorization.test.ts.snap | 150 +++++++ .../server/authorization/audit_logger.ts | 7 +- .../authorization/authorization.test.ts | 76 ++++ .../server/authorization/authorization.ts | 42 +- .../cases/server/authorization/index.ts | 10 +- .../cases/server/authorization/types.ts | 3 +- .../server/client/cases/bulk_update.test.ts | 133 ++++++- .../cases/server/client/cases/bulk_update.ts | 23 +- .../server/connectors/cases/index.test.ts | 4 + .../server/connectors/cases/utils.test.ts | 1 + .../cases/server/connectors/cases/utils.ts | 3 +- .../cases/server/features/constants.ts | 18 + x-pack/plugins/cases/server/features/index.ts | 15 + .../server/{features.ts => features/v1.ts} | 48 ++- x-pack/plugins/cases/server/features/v2.ts | 195 +++++++++ x-pack/plugins/cases/server/plugin.ts | 8 +- .../common/feature_kibana_privileges.ts | 21 + .../__snapshots__/oss_features.test.ts.snap | 12 + .../feature_privilege_iterator.test.ts | 52 +++ .../feature_privilege_iterator.ts | 8 + .../plugins/features/server/feature_schema.ts | 2 + .../register_alerts_table_configuration.tsx | 2 +- .../header/add_to_case_action.test.tsx | 2 + .../observability/common/index.ts | 2 + .../pages/alerts/components/alert_actions.tsx | 2 +- .../pages/cases/components/cases.stories.tsx | 4 + .../observability/server/features/cases_v1.ts | 151 +++++++ .../observability/server/features/cases_v2.ts | 181 +++++++++ .../observability/server/plugin.ts | 113 +----- .../observability_shared/common/index.ts | 2 +- .../public/utils/cases_permissions.ts | 4 + .../roles/elasticsearch_role.test.ts | 4 +- .../security_solution/common/constants.ts | 2 +- .../common/test/ess_roles.json | 6 +- .../actions/take_action/index.tsx | 4 +- .../public/cases_test_utils.ts | 14 + .../use_add_to_existing_case.tsx | 2 +- .../use_add_to_new_case.tsx | 2 +- .../public/common/links/links.test.tsx | 20 +- .../alert_context_menu.test.tsx | 2 + .../use_add_to_case_actions.tsx | 12 +- .../public/management/cypress/tasks/common.ts | 2 +- .../public/overview/pages/data_quality.tsx | 4 +- .../security_solution/public/plugin.tsx | 2 +- .../components/modal/header/index.test.tsx | 2 +- .../components/modal/header/index.tsx | 2 +- .../endpoint_operations_analyst.ts | 2 +- .../without_response_actions_role.ts | 2 +- .../lib/product_features_service/mocks.ts | 5 + .../product_features_service.test.ts | 5 +- .../product_features_service.ts | 19 + .../components/add_to_existing_case.test.tsx | 6 +- .../cases/components/add_to_new_case.test.tsx | 6 +- .../cases/hooks/use_case_permission.test.tsx | 6 +- .../cases/hooks/use_case_permission.ts | 2 +- .../apis/cases/common/roles.ts | 78 ++++ .../apis/cases/common/users.ts | 24 ++ .../api_integration/apis/cases/privileges.ts | 70 ++++ .../apis/features/features/features.ts | 12 +- .../apis/security/privileges.ts | 30 ++ .../apis/security/privileges_basic.ts | 33 ++ .../security_solution/cases_privileges.ts | 4 +- .../common/lib/api/case.ts | 37 +- .../common/lib/authentication/roles.ts | 200 ++++------ .../common/lib/authentication/users.ts | 24 ++ .../security_solution/server/plugin.ts | 46 +++ .../trial/create_comment_sub_privilege.ts | 370 ++++++++++++++++++ .../tests/trial/delete_sub_privilege.ts | 3 +- .../security_and_spaces/tests/trial/index.ts | 1 + .../functional/services/ml/security_common.ts | 4 +- .../services/observability/users.ts | 2 +- .../apps/cases/common/roles.ts | 6 +- .../plugins/cases/public/application.tsx | 2 + .../observability_security.ts | 4 +- .../observability/pages/alerts/add_to_case.ts | 4 +- .../observability/pages/cases/case_details.ts | 2 +- .../tests/features/deprecated_features.ts | 3 + .../e2e/investigations/timelines/export.cy.ts | 3 +- .../cypress/tasks/privileges.ts | 4 + .../common/suites/create.ts | 2 + .../common/suites/get.ts | 2 + .../common/suites/get_all.ts | 2 + .../spaces_only/telemetry/telemetry.ts | 3 + x-pack/test/tsconfig.json | 2 +- .../lib/security/default_http_headers.ts | 1 + .../project_controller_security_roles.yml | 2 + 145 files changed, 3541 insertions(+), 516 deletions(-) create mode 100644 x-pack/packages/security-solution/features/src/cases/v1_features/kibana_features.ts rename x-pack/packages/security-solution/features/src/cases/{ => v1_features}/kibana_sub_features.ts (85%) create mode 100644 x-pack/packages/security-solution/features/src/cases/v1_features/types.ts rename x-pack/packages/security-solution/features/src/cases/{ => v2_features}/kibana_features.ts (84%) create mode 100644 x-pack/packages/security-solution/features/src/cases/v2_features/kibana_sub_features.ts create mode 100644 x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.test.tsx create mode 100644 x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.tsx create mode 100644 x-pack/plugins/cases/public/components/user_actions/use_user_permissions.test.tsx create mode 100644 x-pack/plugins/cases/public/components/user_actions/use_user_permissions.tsx create mode 100644 x-pack/plugins/cases/server/authorization/__snapshots__/authorization.test.ts.snap create mode 100644 x-pack/plugins/cases/server/features/constants.ts create mode 100644 x-pack/plugins/cases/server/features/index.ts rename x-pack/plugins/cases/server/{features.ts => features/v1.ts} (67%) create mode 100644 x-pack/plugins/cases/server/features/v2.ts create mode 100644 x-pack/plugins/observability_solution/observability/server/features/cases_v1.ts create mode 100644 x-pack/plugins/observability_solution/observability/server/features/cases_v2.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/trial/create_comment_sub_privilege.ts diff --git a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml index 5c8446123a4fb..07016d0f9fd8d 100644 --- a/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml +++ b/packages/kbn-es/src/serverless_resources/project_roles/security/roles.yml @@ -46,7 +46,7 @@ viewer: - feature_siem.read - feature_siem.read_alerts - feature_siem.endpoint_list_read - - feature_securitySolutionCases.read + - feature_securitySolutionCasesV2.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -126,7 +126,7 @@ editor: - feature_siem.process_operations_all - feature_siem.actions_log_management_all # Response actions history - feature_siem.file_operations_all - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -175,7 +175,7 @@ t1_analyst: - feature_siem.read - feature_siem.read_alerts - feature_siem.endpoint_list_read - - feature_securitySolutionCases.read + - feature_securitySolutionCasesV2.read - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -230,7 +230,7 @@ t2_analyst: - feature_siem.read - feature_siem.read_alerts - feature_siem.endpoint_list_read - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -300,7 +300,7 @@ t3_analyst: - feature_siem.actions_log_management_all # Response actions history - feature_siem.file_operations_all - feature_siem.scan_operations_all - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -362,7 +362,7 @@ threat_intelligence_analyst: - feature_siem.all - feature_siem.endpoint_list_read - feature_siem.blocklist_all - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -430,7 +430,7 @@ rule_author: - feature_siem.host_isolation_exceptions_read - feature_siem.blocklist_all # Elastic Defend Policy Management - feature_siem.actions_log_management_read - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.read @@ -502,7 +502,7 @@ soc_manager: - feature_siem.file_operations_all - feature_siem.execute_operations_all - feature_siem.scan_operations_all - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.all @@ -562,7 +562,7 @@ detections_admin: - feature_siem.all - feature_siem.read_alerts - feature_siem.crud_alerts - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.all @@ -621,7 +621,7 @@ platform_engineer: - feature_siem.host_isolation_exceptions_all - feature_siem.blocklist_all # Elastic Defend Policy Management - feature_siem.actions_log_management_read - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.all @@ -694,7 +694,7 @@ endpoint_operations_analyst: - feature_siem.file_operations_all - feature_siem.execute_operations_all - feature_siem.scan_operations_all - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.all @@ -769,7 +769,7 @@ endpoint_policy_manager: - feature_siem.event_filters_all - feature_siem.host_isolation_exceptions_all - feature_siem.blocklist_all # Elastic Defend Policy Management - - feature_securitySolutionCases.all + - feature_securitySolutionCasesV2.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.all diff --git a/packages/kbn-es/src/serverless_resources/security_roles.json b/packages/kbn-es/src/serverless_resources/security_roles.json index 75106ba041d60..424cb898a4f96 100644 --- a/packages/kbn-es/src/serverless_resources/security_roles.json +++ b/packages/kbn-es/src/serverless_resources/security_roles.json @@ -35,7 +35,7 @@ "siem": ["read", "read_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["read"], + "securitySolutionCasesV2": ["read"], "actions": ["read"], "builtInAlerts": ["read"] }, @@ -82,7 +82,7 @@ "siem": ["read", "read_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["read"], + "securitySolutionCasesV2": ["read"], "actions": ["read"], "builtInAlerts": ["read"] }, @@ -150,7 +150,7 @@ "actions_log_management_all", "file_operations_all" ], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], "actions": ["read"], @@ -210,7 +210,7 @@ "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "actions": ["read"], "builtInAlerts": ["all"] }, @@ -263,7 +263,7 @@ "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "actions": ["all"], "builtInAlerts": ["all"] }, @@ -311,7 +311,7 @@ "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "actions": ["read"], "builtInAlerts": ["all"], "dev_tools": ["all"] @@ -366,7 +366,7 @@ "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "actions": ["all"], "builtInAlerts": ["all"] }, diff --git a/x-pack/packages/security-solution/features/product_features.ts b/x-pack/packages/security-solution/features/product_features.ts index b2c524ff6de1d..67d61f21fae5e 100644 --- a/x-pack/packages/security-solution/features/product_features.ts +++ b/x-pack/packages/security-solution/features/product_features.ts @@ -6,6 +6,6 @@ */ export { getSecurityFeature } from './src/security'; -export { getCasesFeature } from './src/cases'; +export { getCasesFeature, getCasesV2Feature } from './src/cases'; export { getAssistantFeature } from './src/assistant'; export { getAttackDiscoveryFeature } from './src/attack_discovery'; diff --git a/x-pack/packages/security-solution/features/src/cases/index.ts b/x-pack/packages/security-solution/features/src/cases/index.ts index 1dcb33d9c3be3..17e5110538b37 100644 --- a/x-pack/packages/security-solution/features/src/cases/index.ts +++ b/x-pack/packages/security-solution/features/src/cases/index.ts @@ -6,10 +6,21 @@ */ import type { CasesSubFeatureId } from '../product_features_keys'; import type { ProductFeatureParams } from '../types'; -import { getCasesBaseKibanaFeature } from './kibana_features'; -import { getCasesBaseKibanaSubFeatureIds, getCasesSubFeaturesMap } from './kibana_sub_features'; +import { getCasesBaseKibanaFeature } from './v1_features/kibana_features'; +import { + getCasesBaseKibanaSubFeatureIds, + getCasesSubFeaturesMap, +} from './v1_features/kibana_sub_features'; import type { CasesFeatureParams } from './types'; +import { getCasesBaseKibanaFeatureV2 } from './v2_features/kibana_features'; +import { + getCasesBaseKibanaSubFeatureIdsV2, + getCasesSubFeaturesMapV2, +} from './v2_features/kibana_sub_features'; +/** + * @deprecated Use getCasesV2Feature instead + */ export const getCasesFeature = ( params: CasesFeatureParams ): ProductFeatureParams => ({ @@ -17,3 +28,11 @@ export const getCasesFeature = ( baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIds(), subFeaturesMap: getCasesSubFeaturesMap(params), }); + +export const getCasesV2Feature = ( + params: CasesFeatureParams +): ProductFeatureParams => ({ + baseKibanaFeature: getCasesBaseKibanaFeatureV2(params), + baseKibanaSubFeatureIds: getCasesBaseKibanaSubFeatureIdsV2(), + subFeaturesMap: getCasesSubFeaturesMapV2(params), +}); diff --git a/x-pack/packages/security-solution/features/src/cases/types.ts b/x-pack/packages/security-solution/features/src/cases/types.ts index a87a1d787d7c0..17fb10fdd64ee 100644 --- a/x-pack/packages/security-solution/features/src/cases/types.ts +++ b/x-pack/packages/security-solution/features/src/cases/types.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import type { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; import type { ProductFeatureCasesKey, CasesSubFeatureId } from '../product_features_keys'; import type { ProductFeatureKibanaConfig } from '../types'; diff --git a/x-pack/packages/security-solution/features/src/cases/v1_features/kibana_features.ts b/x-pack/packages/security-solution/features/src/cases/v1_features/kibana_features.ts new file mode 100644 index 0000000000000..db442d894363a --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/v1_features/kibana_features.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import type { BaseKibanaFeatureConfig } from '../../types'; +import { APP_ID, CASES_FEATURE_ID, CASES_FEATURE_ID_V2 } from '../../constants'; +import type { CasesFeatureParams } from '../types'; + +/** + * @deprecated Use getCasesBaseKibanaFeatureV2 instead + */ +export const getCasesBaseKibanaFeature = ({ + uiCapabilities, + apiTags, + savedObjects, +}: CasesFeatureParams): BaseKibanaFeatureConfig => { + return { + deprecated: { + notice: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCase.deprecationMessage', + { + defaultMessage: + 'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.', + values: { + currentId: CASES_FEATURE_ID, + casesFeatureIdV2: CASES_FEATURE_ID_V2, + }, + } + ), + }, + id: CASES_FEATURE_ID, + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitleDeprecated', + { + defaultMessage: 'Cases (Deprecated)', + } + ), + order: 1100, + category: DEFAULT_APP_CATEGORIES.security, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: [APP_ID], + privileges: { + all: { + api: [...apiTags.all, ...apiTags.createComment], + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + create: [APP_ID], + read: [APP_ID], + update: [APP_ID], + push: [APP_ID], + createComment: [APP_ID], + reopenCase: [APP_ID], + }, + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + ui: uiCapabilities.all, + replacedBy: { + default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['all'] }], + minimal: [ + { + feature: CASES_FEATURE_ID_V2, + privileges: ['minimal_all', 'create_comment', 'case_reopen'], + }, + ], + }, + }, + read: { + api: apiTags.read, + app: [CASES_FEATURE_ID, 'kibana'], + catalogue: [APP_ID], + cases: { + read: [APP_ID], + }, + savedObject: { + all: [], + read: [...savedObjects.files], + }, + ui: uiCapabilities.read, + replacedBy: { + default: [{ feature: CASES_FEATURE_ID_V2, privileges: ['read'] }], + minimal: [{ feature: CASES_FEATURE_ID_V2, privileges: ['minimal_read'] }], + }, + }, + }, + }; +}; diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/cases/v1_features/kibana_sub_features.ts similarity index 85% rename from x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts rename to x-pack/packages/security-solution/features/src/cases/v1_features/kibana_sub_features.ts index 914b23687956b..ade0dbab2bfea 100644 --- a/x-pack/packages/security-solution/features/src/cases/kibana_sub_features.ts +++ b/x-pack/packages/security-solution/features/src/cases/v1_features/kibana_sub_features.ts @@ -7,9 +7,9 @@ import { i18n } from '@kbn/i18n'; import type { SubFeatureConfig } from '@kbn/features-plugin/common'; -import { CasesSubFeatureId } from '../product_features_keys'; -import { APP_ID } from '../constants'; -import type { CasesFeatureParams } from './types'; +import { CasesSubFeatureId } from '../../product_features_keys'; +import { APP_ID, CASES_FEATURE_ID_V2 } from '../../constants'; +import type { CasesFeatureParams } from '../types'; /** * Sub-features that will always be available for Security Cases @@ -21,7 +21,8 @@ export const getCasesBaseKibanaSubFeatureIds = (): CasesSubFeatureId[] => [ ]; /** - * Defines all the Security Assistant subFeatures available. + * @deprecated Use getCasesSubFeaturesMapV2 instead + * @description - Defines all the Security Solution Cases available. * The order of the subFeatures is the order they will be displayed */ export const getCasesSubFeaturesMap = ({ @@ -55,6 +56,7 @@ export const getCasesSubFeaturesMap = ({ delete: [APP_ID], }, ui: uiCapabilities.delete, + replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_delete'] }], }, ], }, @@ -89,6 +91,7 @@ export const getCasesSubFeaturesMap = ({ settings: [APP_ID], }, ui: uiCapabilities.settings, + replacedBy: [{ feature: CASES_FEATURE_ID_V2, privileges: ['cases_settings'] }], }, ], }, diff --git a/x-pack/packages/security-solution/features/src/cases/v1_features/types.ts b/x-pack/packages/security-solution/features/src/cases/v1_features/types.ts new file mode 100644 index 0000000000000..f17f83ddecce8 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/v1_features/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ProductFeatureCasesKey, CasesSubFeatureId } from '../../product_features_keys'; +import type { ProductFeatureKibanaConfig } from '../../types'; + +export type DefaultCasesProductFeaturesConfig = Record< + ProductFeatureCasesKey, + ProductFeatureKibanaConfig +>; diff --git a/x-pack/packages/security-solution/features/src/cases/kibana_features.ts b/x-pack/packages/security-solution/features/src/cases/v2_features/kibana_features.ts similarity index 84% rename from x-pack/packages/security-solution/features/src/cases/kibana_features.ts rename to x-pack/packages/security-solution/features/src/cases/v2_features/kibana_features.ts index dd49a60328288..c0c025335d054 100644 --- a/x-pack/packages/security-solution/features/src/cases/kibana_features.ts +++ b/x-pack/packages/security-solution/features/src/cases/v2_features/kibana_features.ts @@ -9,17 +9,17 @@ import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; -import type { BaseKibanaFeatureConfig } from '../types'; -import { APP_ID, CASES_FEATURE_ID } from '../constants'; -import type { CasesFeatureParams } from './types'; +import type { BaseKibanaFeatureConfig } from '../../types'; +import { APP_ID, CASES_FEATURE_ID_V2, CASES_FEATURE_ID } from '../../constants'; +import type { CasesFeatureParams } from '../types'; -export const getCasesBaseKibanaFeature = ({ +export const getCasesBaseKibanaFeatureV2 = ({ uiCapabilities, apiTags, savedObjects, }: CasesFeatureParams): BaseKibanaFeatureConfig => { return { - id: CASES_FEATURE_ID, + id: CASES_FEATURE_ID_V2, name: i18n.translate( 'securitySolutionPackages.features.featureRegistry.linkSecuritySolutionCaseTitle', { @@ -41,6 +41,7 @@ export const getCasesBaseKibanaFeature = ({ create: [APP_ID], read: [APP_ID], update: [APP_ID], + push: [APP_ID], }, savedObject: { all: [...savedObjects.files], diff --git a/x-pack/packages/security-solution/features/src/cases/v2_features/kibana_sub_features.ts b/x-pack/packages/security-solution/features/src/cases/v2_features/kibana_sub_features.ts new file mode 100644 index 0000000000000..59aeb866039d4 --- /dev/null +++ b/x-pack/packages/security-solution/features/src/cases/v2_features/kibana_sub_features.ts @@ -0,0 +1,177 @@ +/* + * 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 type { SubFeatureConfig } from '@kbn/features-plugin/common'; +import { CasesSubFeatureId } from '../../product_features_keys'; +import { APP_ID } from '../../constants'; +import type { CasesFeatureParams } from '../types'; + +/** + * Sub-features that will always be available for Security Cases + * regardless of the product type. + */ +export const getCasesBaseKibanaSubFeatureIdsV2 = (): CasesSubFeatureId[] => [ + CasesSubFeatureId.deleteCases, + CasesSubFeatureId.casesSettings, + CasesSubFeatureId.createComment, + CasesSubFeatureId.reopenCase, +]; + +/** + * Defines all the Security Solution Cases subFeatures available. + * The order of the subFeatures is the order they will be displayed + */ +export const getCasesSubFeaturesMapV2 = ({ + uiCapabilities, + apiTags, + savedObjects, +}: CasesFeatureParams) => { + const deleteCasesSubFeature: SubFeatureConfig = { + name: i18n.translate('securitySolutionPackages.features.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.delete, + id: 'cases_delete', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.deleteSubFeatureDetails', + { + defaultMessage: 'Delete cases and comments', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + delete: [APP_ID], + }, + ui: uiCapabilities.delete, + }, + ], + }, + ], + }; + + const casesSettingsCasesSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureName', + { + defaultMessage: 'Case settings', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit case settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + settings: [APP_ID], + }, + ui: uiCapabilities.settings, + }, + ], + }, + ], + }; + + /* The below sub features were newly added in v2 (8.17) */ + + const casesAddCommentsCasesSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureName', + { + defaultMessage: 'Create comments & attachments', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.createComment, + id: 'create_comment', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.addCommentsSubFeatureDetails', + { + defaultMessage: 'Add comments to cases', + } + ), + includeIn: 'all', + savedObject: { + all: [...savedObjects.files], + read: [...savedObjects.files], + }, + cases: { + createComment: [APP_ID], + }, + ui: uiCapabilities.createComment, + }, + ], + }, + ], + }; + const casesreopenCaseSubFeature: SubFeatureConfig = { + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureName', + { + defaultMessage: 'Re-open', + } + ), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'case_reopen', + name: i18n.translate( + 'securitySolutionPackages.features.featureRegistry.reopenCaseSubFeatureDetails', + { + defaultMessage: 'Re-open closed cases', + } + ), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [APP_ID], + }, + ui: uiCapabilities.reopenCase, + }, + ], + }, + ], + }; + + return new Map([ + [CasesSubFeatureId.deleteCases, deleteCasesSubFeature], + [CasesSubFeatureId.casesSettings, casesSettingsCasesSubFeature], + /* The below sub features were newly added in v2 (8.17) */ + [CasesSubFeatureId.createComment, casesAddCommentsCasesSubFeature], + [CasesSubFeatureId.reopenCase, casesreopenCaseSubFeature], + ]); +}; diff --git a/x-pack/packages/security-solution/features/src/constants.ts b/x-pack/packages/security-solution/features/src/constants.ts index 5027a7c8d393b..c6acab28c4860 100644 --- a/x-pack/packages/security-solution/features/src/constants.ts +++ b/x-pack/packages/security-solution/features/src/constants.ts @@ -9,7 +9,16 @@ export const APP_ID = 'securitySolution' as const; export const SERVER_APP_ID = 'siem' as const; +/** + * @deprecated deprecated in 8.17. Use CASE_FEATURE_ID_V2 instead + */ export const CASES_FEATURE_ID = 'securitySolutionCases' as const; + +// New version created in 8.17 to adopt the roles migration changes +export const CASES_FEATURE_ID_V2 = 'securitySolutionCasesV2' as const; + +export const SECURITY_SOLUTION_CASES_APP_ID = 'securitySolutionCases' as const; + export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; diff --git a/x-pack/packages/security-solution/features/src/product_features_keys.ts b/x-pack/packages/security-solution/features/src/product_features_keys.ts index e72e669716c59..42a190b189234 100644 --- a/x-pack/packages/security-solution/features/src/product_features_keys.ts +++ b/x-pack/packages/security-solution/features/src/product_features_keys.ts @@ -148,6 +148,8 @@ export enum SecuritySubFeatureId { export enum CasesSubFeatureId { deleteCases = 'deleteCasesSubFeature', casesSettings = 'casesSettingsSubFeature', + createComment = 'createCommentSubFeature', + reopenCase = 'reopenCaseSubFeature', } /** Sub-features IDs for Security Assistant */ diff --git a/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap b/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap index 1874a17515e19..2997187697c40 100644 --- a/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap +++ b/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/__snapshots__/cases.test.ts.snap @@ -4,7 +4,6 @@ exports[`cases feature_privilege_builder within feature grants all privileges un Array [ "cases:observability/pushCase", "cases:observability/createCase", - "cases:observability/createComment", "cases:observability/getCase", "cases:observability/getComment", "cases:observability/getTags", @@ -17,12 +16,19 @@ Array [ "cases:observability/deleteComment", "cases:observability/createConfiguration", "cases:observability/updateConfiguration", + "cases:observability/createComment", + "cases:observability/reopenCase", ] `; exports[`cases feature_privilege_builder within feature grants create privileges under feature with id securitySolution 1`] = ` Array [ "cases:securitySolution/createCase", +] +`; + +exports[`cases feature_privilege_builder within feature grants createComment privileges under feature with id securitySolution 1`] = ` +Array [ "cases:securitySolution/createComment", ] `; @@ -51,6 +57,12 @@ Array [ ] `; +exports[`cases feature_privilege_builder within feature grants reopenCase privileges under feature with id observability 1`] = ` +Array [ + "cases:observability/reopenCase", +] +`; + exports[`cases feature_privilege_builder within feature grants settings privileges under feature with id observability 1`] = ` Array [ "cases:observability/createConfiguration", diff --git a/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts b/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts index ad0563ef7a827..eae3bbc942e34 100644 --- a/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts +++ b/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.test.ts @@ -48,6 +48,8 @@ describe(`cases`, () => { ['update', 'observability'], ['delete', 'securitySolution'], ['settings', 'observability'], + ['createComment', 'securitySolution'], + ['reopenCase', 'observability'], ])('grants %s privileges under feature with id %s', (operation, featureID) => { const actions = new Actions(); const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions); @@ -89,6 +91,8 @@ describe(`cases`, () => { delete: ['security'], read: ['obs'], settings: ['security'], + createComment: ['security'], + reopenCase: ['security'], }, savedObject: { all: [], @@ -112,7 +116,6 @@ describe(`cases`, () => { Array [ "cases:security/pushCase", "cases:security/createCase", - "cases:security/createComment", "cases:security/getCase", "cases:security/getComment", "cases:security/getTags", @@ -125,6 +128,8 @@ describe(`cases`, () => { "cases:security/deleteComment", "cases:security/createConfiguration", "cases:security/updateConfiguration", + "cases:security/createComment", + "cases:security/reopenCase", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", @@ -168,7 +173,6 @@ describe(`cases`, () => { Array [ "cases:security/pushCase", "cases:security/createCase", - "cases:security/createComment", "cases:security/getCase", "cases:security/getComment", "cases:security/getTags", @@ -181,9 +185,10 @@ describe(`cases`, () => { "cases:security/deleteComment", "cases:security/createConfiguration", "cases:security/updateConfiguration", + "cases:security/createComment", + "cases:security/reopenCase", "cases:other-security/pushCase", "cases:other-security/createCase", - "cases:other-security/createComment", "cases:other-security/getCase", "cases:other-security/getComment", "cases:other-security/getTags", @@ -196,6 +201,8 @@ describe(`cases`, () => { "cases:other-security/deleteComment", "cases:other-security/createConfiguration", "cases:other-security/updateConfiguration", + "cases:other-security/createComment", + "cases:other-security/reopenCase", "cases:obs/getCase", "cases:obs/getComment", "cases:obs/getTags", diff --git a/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts b/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts index 7672e1920fd4b..3cf293b935b36 100644 --- a/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts +++ b/x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts @@ -22,7 +22,7 @@ export type CasesSupportedOperations = (typeof allOperations)[number]; */ const pushOperations = ['pushCase'] as const; -const createOperations = ['createCase', 'createComment'] as const; +const createOperations = ['createCase'] as const; const readOperations = [ 'getCase', 'getComment', @@ -31,9 +31,12 @@ const readOperations = [ 'getUserActions', 'findConfigurations', ] as const; +// Update operations do not currently include the ability to re-open a case const updateOperations = ['updateCase', 'updateComment'] as const; const deleteOperations = ['deleteCase', 'deleteComment'] as const; const settingsOperations = ['createConfiguration', 'updateConfiguration'] as const; +const createCommentOperations = ['createComment'] as const; +const reopenOperations = ['reopenCase'] as const; const allOperations = [ ...pushOperations, ...createOperations, @@ -41,6 +44,8 @@ const allOperations = [ ...updateOperations, ...deleteOperations, ...settingsOperations, + ...createCommentOperations, + ...reopenOperations, ] as const; export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { @@ -56,7 +61,6 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { operations.map((operation) => this.actions.cases.get(owner, operation)) ); }; - return uniq([ ...getCasesPrivilege(allOperations, privilegeDefinition.cases?.all), ...getCasesPrivilege(pushOperations, privilegeDefinition.cases?.push), @@ -65,6 +69,8 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder { ...getCasesPrivilege(updateOperations, privilegeDefinition.cases?.update), ...getCasesPrivilege(deleteOperations, privilegeDefinition.cases?.delete), ...getCasesPrivilege(settingsOperations, privilegeDefinition.cases?.settings), + ...getCasesPrivilege(createCommentOperations, privilegeDefinition.cases?.createComment), + ...getCasesPrivilege(reopenOperations, privilegeDefinition.cases?.reopenCase), ]); } } diff --git a/x-pack/plugins/cases/common/constants/application.ts b/x-pack/plugins/cases/common/constants/application.ts index 4b43a17708ab6..01bbea157e7d2 100644 --- a/x-pack/plugins/cases/common/constants/application.ts +++ b/x-pack/plugins/cases/common/constants/application.ts @@ -12,7 +12,9 @@ import { CASE_VIEW_PAGE_TABS } from '../types'; */ export const APP_ID = 'cases' as const; +/** @deprecated Please use FEATURE_ID_V2 instead */ export const FEATURE_ID = 'generalCases' as const; +export const FEATURE_ID_V2 = 'generalCasesV2' as const; export const APP_OWNER = 'cases' as const; export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const; export const CASES_CREATE_PATH = '/create' as const; diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index aa3855807cea2..1fee73f8608c8 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -174,6 +174,8 @@ export const DELETE_CASES_CAPABILITY = 'delete_cases' as const; export const PUSH_CASES_CAPABILITY = 'push_cases' as const; export const CASES_SETTINGS_CAPABILITY = 'cases_settings' as const; export const CASES_CONNECTORS_CAPABILITY = 'cases_connectors' as const; +export const CASES_REOPEN_CAPABILITY = 'case_reopen' as const; +export const CREATE_COMMENT_CAPABILITY = 'create_comment' as const; /** * Cases API Tags diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index ead81710c451d..8e3b2644ee01a 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -18,6 +18,7 @@ export type { CasesBulkGetResponse, CasePostRequest, + CasePatchRequest, GetRelatedCasesByAlertResponse, UserActionFindResponse, } from './types/api'; @@ -38,6 +39,7 @@ export { CaseSeverity } from './types/domain'; export { APP_ID, FEATURE_ID, + FEATURE_ID_V2, CASES_URL, SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER, @@ -55,6 +57,8 @@ export { CASES_CONNECTORS_CAPABILITY, GET_CONNECTORS_CONFIGURE_API_TAG, CASES_SETTINGS_CAPABILITY, + CREATE_COMMENT_CAPABILITY, + CASES_REOPEN_CAPABILITY, } from './constants'; export type { AttachmentAttributes } from './types/domain'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 6d75b30dd119d..99c92e0dbb55b 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -11,6 +11,8 @@ import type { DELETE_CASES_CAPABILITY, READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, + CREATE_COMMENT_CAPABILITY, + CASES_REOPEN_CAPABILITY, } from '..'; import type { CASES_CONNECTORS_CAPABILITY, @@ -305,6 +307,8 @@ export interface CasesPermissions { push: boolean; connectors: boolean; settings: boolean; + reopenCase: boolean; + createComment: boolean; } export interface CasesCapabilities { @@ -315,4 +319,6 @@ export interface CasesCapabilities { [PUSH_CASES_CAPABILITY]: boolean; [CASES_CONNECTORS_CAPABILITY]: boolean; [CASES_SETTINGS_CAPABILITY]: boolean; + [CREATE_COMMENT_CAPABILITY]: boolean; + [CASES_REOPEN_CAPABILITY]: boolean; } diff --git a/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap b/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap index 9cca596cc84d8..10fdb6da9673a 100644 --- a/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap +++ b/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap @@ -6,9 +6,11 @@ Object { "casesSuggestUserProfiles", "bulkGetUserProfiles", "casesGetConnectorsConfigure", - "casesFilesCasesCreate", "casesFilesCasesRead", ], + "createComment": Array [ + "casesFilesCasesCreate", + ], "delete": Array [ "casesFilesCasesDelete", ], @@ -27,9 +29,11 @@ Object { "casesSuggestUserProfiles", "bulkGetUserProfiles", "casesGetConnectorsConfigure", - "observabilityFilesCasesCreate", "observabilityFilesCasesRead", ], + "createComment": Array [ + "observabilityFilesCasesCreate", + ], "delete": Array [ "observabilityFilesCasesDelete", ], @@ -48,9 +52,11 @@ Object { "casesSuggestUserProfiles", "bulkGetUserProfiles", "casesGetConnectorsConfigure", - "securitySolutionFilesCasesCreate", "securitySolutionFilesCasesRead", ], + "createComment": Array [ + "securitySolutionFilesCasesCreate", + ], "delete": Array [ "securitySolutionFilesCasesDelete", ], diff --git a/x-pack/plugins/cases/common/utils/api_tags.ts b/x-pack/plugins/cases/common/utils/api_tags.ts index 3fbad714e55f9..e4750540c5b5e 100644 --- a/x-pack/plugins/cases/common/utils/api_tags.ts +++ b/x-pack/plugins/cases/common/utils/api_tags.ts @@ -18,6 +18,7 @@ export interface CasesApiTags { all: readonly string[]; read: readonly string[]; delete: readonly string[]; + createComment: readonly string[]; } export const getApiTags = (owner: Owner): CasesApiTags => { @@ -30,7 +31,6 @@ export const getApiTags = (owner: Owner): CasesApiTags => { SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, GET_CONNECTORS_CONFIGURE_API_TAG, - create, read, ] as const, read: [ @@ -40,5 +40,6 @@ export const getApiTags = (owner: Owner): CasesApiTags => { read, ] as const, delete: [deleteTag] as const, + createComment: [create] as const, }; }; diff --git a/x-pack/plugins/cases/common/utils/capabilities.test.tsx b/x-pack/plugins/cases/common/utils/capabilities.test.tsx index 07b82ea0d0e8f..11f74af8e02d8 100644 --- a/x-pack/plugins/cases/common/utils/capabilities.test.tsx +++ b/x-pack/plugins/cases/common/utils/capabilities.test.tsx @@ -17,6 +17,10 @@ describe('createUICapabilities', () => { "update_cases", "push_cases", "cases_connectors", + "cases_settings", + ], + "createComment": Array [ + "create_comment", ], "delete": Array [ "delete_cases", @@ -25,6 +29,9 @@ describe('createUICapabilities', () => { "read_cases", "cases_connectors", ], + "reopenCase": Array [ + "case_reopen", + ], "settings": Array [ "cases_settings", ], diff --git a/x-pack/plugins/cases/common/utils/capabilities.ts b/x-pack/plugins/cases/common/utils/capabilities.ts index 6b33dd8c8dceb..6897dc6bae774 100644 --- a/x-pack/plugins/cases/common/utils/capabilities.ts +++ b/x-pack/plugins/cases/common/utils/capabilities.ts @@ -13,6 +13,8 @@ import { READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, CASES_SETTINGS_CAPABILITY, + CASES_REOPEN_CAPABILITY, + CREATE_COMMENT_CAPABILITY, } from '../constants'; export interface CasesUiCapabilities { @@ -20,6 +22,8 @@ export interface CasesUiCapabilities { read: readonly string[]; delete: readonly string[]; settings: readonly string[]; + reopenCase: readonly string[]; + createComment: readonly string[]; } /** * Return the UI capabilities for each type of operation. These strings must match the values defined in the UI @@ -32,8 +36,11 @@ export const createUICapabilities = (): CasesUiCapabilities => ({ UPDATE_CASES_CAPABILITY, PUSH_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY, + CASES_SETTINGS_CAPABILITY, ] as const, read: [READ_CASES_CAPABILITY, CASES_CONNECTORS_CAPABILITY] as const, delete: [DELETE_CASES_CAPABILITY] as const, settings: [CASES_SETTINGS_CAPABILITY] as const, + reopenCase: [CASES_REOPEN_CAPABILITY] as const, + createComment: [CREATE_COMMENT_CAPABILITY] as const, }); diff --git a/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts b/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts index 5b82919523f36..69eca9d064602 100644 --- a/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts +++ b/x-pack/plugins/cases/public/client/helpers/can_use_cases.test.ts @@ -20,67 +20,67 @@ import { canUseCases } from './can_use_cases'; type CasesCapabilities = Pick< ApplicationStart['capabilities'], - 'securitySolutionCases' | 'observabilityCases' | 'generalCases' + 'securitySolutionCasesV2' | 'observabilityCasesV2' | 'generalCasesV2' >; const hasAll: CasesCapabilities = { - securitySolutionCases: allCasesCapabilities(), - observabilityCases: allCasesCapabilities(), - generalCases: allCasesCapabilities(), + securitySolutionCasesV2: allCasesCapabilities(), + observabilityCasesV2: allCasesCapabilities(), + generalCasesV2: allCasesCapabilities(), }; const hasNone: CasesCapabilities = { - securitySolutionCases: noCasesCapabilities(), - observabilityCases: noCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: noCasesCapabilities(), + observabilityCasesV2: noCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasSecurity: CasesCapabilities = { - securitySolutionCases: allCasesCapabilities(), - observabilityCases: noCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: allCasesCapabilities(), + observabilityCasesV2: noCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasObservability: CasesCapabilities = { - securitySolutionCases: noCasesCapabilities(), - observabilityCases: allCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: noCasesCapabilities(), + observabilityCasesV2: allCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasObservabilityWriteTrue: CasesCapabilities = { - securitySolutionCases: noCasesCapabilities(), - observabilityCases: writeCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: noCasesCapabilities(), + observabilityCasesV2: writeCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasSecurityWriteTrue: CasesCapabilities = { - securitySolutionCases: writeCasesCapabilities(), - observabilityCases: noCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: writeCasesCapabilities(), + observabilityCasesV2: noCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasObservabilityReadTrue: CasesCapabilities = { - securitySolutionCases: noCasesCapabilities(), - observabilityCases: readCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: noCasesCapabilities(), + observabilityCasesV2: readCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasSecurityReadTrue: CasesCapabilities = { - securitySolutionCases: readCasesCapabilities(), - observabilityCases: noCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: readCasesCapabilities(), + observabilityCasesV2: noCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasSecurityWriteAndObservabilityRead: CasesCapabilities = { - securitySolutionCases: writeCasesCapabilities(), - observabilityCases: readCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: writeCasesCapabilities(), + observabilityCasesV2: readCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const hasSecurityConnectors: CasesCapabilities = { - securitySolutionCases: readCasesCapabilities(), - observabilityCases: noCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: readCasesCapabilities(), + observabilityCasesV2: noCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; describe('canUseCases', () => { diff --git a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts index 90b0d3b18908f..3e318132f8adf 100644 --- a/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts +++ b/x-pack/plugins/cases/public/client/helpers/can_use_cases.ts @@ -7,7 +7,7 @@ import type { ApplicationStart } from '@kbn/core/public'; import { - FEATURE_ID, + FEATURE_ID_V2, GENERAL_CASES_OWNER, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER, @@ -42,6 +42,8 @@ export const canUseCases = acc.push = acc.push || userCapabilitiesForOwner.push; acc.connectors = acc.connectors || userCapabilitiesForOwner.connectors; acc.settings = acc.settings || userCapabilitiesForOwner.settings; + acc.reopenCase = acc.reopenCase || userCapabilitiesForOwner.reopenCase; + acc.createComment = acc.createComment || userCapabilitiesForOwner.createComment; const allFromAcc = acc.create && @@ -50,7 +52,9 @@ export const canUseCases = acc.delete && acc.push && acc.connectors && - acc.settings; + acc.settings && + acc.reopenCase && + acc.createComment; acc.all = acc.all || userCapabilitiesForOwner.all || allFromAcc; @@ -65,6 +69,8 @@ export const canUseCases = push: false, connectors: false, settings: false, + reopenCase: false, + createComment: false, } ); @@ -75,8 +81,8 @@ export const canUseCases = const getFeatureID = (owner: CasesOwners) => { if (owner === GENERAL_CASES_OWNER) { - return FEATURE_ID; + return FEATURE_ID_V2; } - return `${owner}Cases`; + return `${owner}CasesV2`; }; diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts index ce374243b10b2..ec1b90eee0eb1 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.test.ts @@ -14,9 +14,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": false, + "createComment": false, "delete": false, "push": false, "read": false, + "reopenCase": false, "settings": false, "update": false, } @@ -29,9 +31,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": false, + "createComment": false, "delete": false, "push": false, "read": false, + "reopenCase": false, "settings": false, "update": false, } @@ -44,9 +48,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": true, + "createComment": false, "delete": false, "push": false, "read": false, + "reopenCase": false, "settings": false, "update": false, } @@ -68,9 +74,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": false, + "createComment": false, "delete": false, "push": false, "read": false, + "reopenCase": false, "settings": false, "update": false, } @@ -83,9 +91,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": false, + "createComment": false, "delete": false, "push": false, "read": false, + "reopenCase": false, "settings": false, "update": false, } @@ -107,9 +117,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": true, "create": false, + "createComment": false, "delete": true, "push": true, "read": true, + "reopenCase": false, "settings": false, "update": true, } @@ -132,9 +144,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": true, + "createComment": false, "delete": true, "push": true, "read": true, + "reopenCase": false, "settings": true, "update": true, } @@ -157,9 +171,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": true, "create": true, + "createComment": false, "delete": true, "push": true, "read": true, + "reopenCase": false, "settings": false, "update": true, } @@ -172,9 +188,11 @@ describe('getUICapabilities', () => { "all": false, "connectors": false, "create": false, + "createComment": false, "delete": false, "push": false, "read": false, + "reopenCase": false, "settings": true, "update": false, } diff --git a/x-pack/plugins/cases/public/client/helpers/capabilities.ts b/x-pack/plugins/cases/public/client/helpers/capabilities.ts index 9be5b5f05f646..634cb3188602d 100644 --- a/x-pack/plugins/cases/public/client/helpers/capabilities.ts +++ b/x-pack/plugins/cases/public/client/helpers/capabilities.ts @@ -14,6 +14,8 @@ import { PUSH_CASES_CAPABILITY, READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, + CASES_REOPEN_CAPABILITY, + CREATE_COMMENT_CAPABILITY, } from '../../../common/constants'; export const getUICapabilities = ( @@ -26,8 +28,19 @@ export const getUICapabilities = ( const push = !!featureCapabilities?.[PUSH_CASES_CAPABILITY]; const connectors = !!featureCapabilities?.[CASES_CONNECTORS_CAPABILITY]; const settings = !!featureCapabilities?.[CASES_SETTINGS_CAPABILITY]; + const reopenCase = !!featureCapabilities?.[CASES_REOPEN_CAPABILITY]; + const createComment = !!featureCapabilities?.[CREATE_COMMENT_CAPABILITY]; - const all = create && read && update && deletePriv && push && connectors && settings; + const all = + create && + read && + update && + deletePriv && + push && + connectors && + settings && + reopenCase && + createComment; return { all, @@ -38,5 +51,7 @@ export const getUICapabilities = ( push, connectors, settings, + reopenCase, + createComment, }; }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts index 7bf4e71e0717a..5e65dd0933e0e 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts @@ -48,7 +48,7 @@ export const useNavigation = jest.fn().mockReturnValue({ export const useApplicationCapabilities = jest.fn().mockReturnValue({ actions: { crud: true, read: true }, - generalCases: { crud: true, read: true }, + generalCasesV2: { crud: true, read: true }, visualize: { crud: true, read: true }, dashboard: { crud: true, read: true }, }); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx b/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx index 8d0beb130edc6..60b798d37822a 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.test.tsx @@ -23,7 +23,7 @@ describe('hooks', () => { expect(result.current).toEqual({ actions: { crud: true, read: true }, - generalCases: allCasesPermissions(), + generalCasesV2: allCasesPermissions(), visualize: { crud: true, read: true }, dashboard: { crud: true, read: true }, }); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index 3d72e5ca552b9..6a309111ceddb 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -15,7 +15,7 @@ import type { NavigateToAppOptions } from '@kbn/core/public'; import { getUICapabilities } from '../../../client/helpers/capabilities'; import { convertToCamelCase } from '../../../api/utils'; import { - FEATURE_ID, + FEATURE_ID_V2, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ, } from '../../../../common/constants'; @@ -166,7 +166,7 @@ interface Capabilities { } interface UseApplicationCapabilities { actions: Capabilities; - generalCases: CasesPermissions; + generalCasesV2: CasesPermissions; visualize: Capabilities; dashboard: Capabilities; } @@ -178,13 +178,13 @@ interface UseApplicationCapabilities { export const useApplicationCapabilities = (): UseApplicationCapabilities => { const capabilities = useKibana().services?.application?.capabilities; - const casesCapabilities = capabilities[FEATURE_ID]; + const casesCapabilities = capabilities[FEATURE_ID_V2]; const permissions = getUICapabilities(casesCapabilities); return useMemo( () => ({ actions: { crud: !!capabilities.actions?.save, read: !!capabilities.actions?.show }, - generalCases: { + generalCasesV2: { all: permissions.all, create: permissions.create, read: permissions.read, @@ -193,6 +193,8 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { push: permissions.push, connectors: permissions.connectors, settings: permissions.settings, + reopenCase: permissions.reopenCase, + createComment: permissions.createComment, }, visualize: { crud: !!capabilities.visualize?.save, read: !!capabilities.visualize?.show }, dashboard: { @@ -215,6 +217,8 @@ export const useApplicationCapabilities = (): UseApplicationCapabilities => { permissions.push, permissions.connectors, permissions.settings, + permissions.reopenCase, + permissions.createComment, ] ); }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx index 0223e4648ac93..48ef98c8dffa8 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.tsx @@ -83,7 +83,7 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta services.application.capabilities = { ...services.application.capabilities, actions: { save: true, show: true }, - generalCases: { + generalCasesV2: { create_cases: true, read_cases: true, update_cases: true, @@ -91,6 +91,8 @@ export const createStartServicesMock = ({ license }: StartServiceArgs = {}): Sta push_cases: true, cases_connectors: true, cases_settings: true, + case_reopen: true, + create_comment: true, }, visualize: { save: true, show: true }, dashboard: { show: true, createNew: true }, diff --git a/x-pack/plugins/cases/public/common/mock/permissions.ts b/x-pack/plugins/cases/public/common/mock/permissions.ts index fce274cd7f338..9e08120a8c275 100644 --- a/x-pack/plugins/cases/public/common/mock/permissions.ts +++ b/x-pack/plugins/cases/public/common/mock/permissions.ts @@ -17,6 +17,8 @@ export const noCasesPermissions = () => push: false, connectors: false, settings: false, + createComment: false, + reopenCase: false, }); export const readCasesPermissions = () => @@ -28,16 +30,52 @@ export const readCasesPermissions = () => push: false, connectors: true, settings: false, + createComment: false, + reopenCase: false, }); export const noCreateCasesPermissions = () => buildCasesPermissions({ create: false }); -export const noUpdateCasesPermissions = () => buildCasesPermissions({ update: false }); +export const noCreateCommentCasesPermissions = () => + buildCasesPermissions({ createComment: false }); +export const noUpdateCasesPermissions = () => + buildCasesPermissions({ update: false, reopenCase: false }); export const noPushCasesPermissions = () => buildCasesPermissions({ push: false }); export const noDeleteCasesPermissions = () => buildCasesPermissions({ delete: false }); +export const noReopenCasesPermissions = () => buildCasesPermissions({ reopenCase: false }); export const writeCasesPermissions = () => buildCasesPermissions({ read: false }); +export const onlyCreateCommentPermissions = () => + buildCasesPermissions({ + read: false, + create: false, + update: false, + delete: true, + push: false, + createComment: true, + reopenCase: false, + }); export const onlyDeleteCasesPermission = () => - buildCasesPermissions({ read: false, create: false, update: false, delete: true, push: false }); + buildCasesPermissions({ + read: false, + create: false, + update: false, + delete: true, + push: false, + createComment: false, + reopenCase: false, + }); +// In practice, a real life user should never have this configuration, but testing for thoroughness +export const onlyReopenCasesPermission = () => + buildCasesPermissions({ + read: false, + create: false, + update: false, + delete: false, + push: false, + createComment: false, + reopenCase: true, + }); export const noConnectorsCasePermission = () => buildCasesPermissions({ connectors: false }); export const noCasesSettingsPermission = () => buildCasesPermissions({ settings: false }); +export const disabledReopenCasePermission = () => buildCasesPermissions({ reopenCase: false }); export const buildCasesPermissions = (overrides: Partial> = {}) => { const create = overrides.create ?? true; @@ -47,7 +85,18 @@ export const buildCasesPermissions = (overrides: Partial push_cases: false, cases_connectors: false, cases_settings: false, + create_comment: false, + case_reopen: false, }); export const readCasesCapabilities = () => buildCasesCapabilities({ @@ -79,6 +132,8 @@ export const readCasesCapabilities = () => delete_cases: false, push_cases: false, cases_settings: false, + create_comment: false, + case_reopen: false, }); export const writeCasesCapabilities = () => { return buildCasesCapabilities({ @@ -95,5 +150,7 @@ export const buildCasesCapabilities = (overrides?: Partial) = push_cases: overrides?.push_cases ?? true, cases_connectors: overrides?.cases_connectors ?? true, cases_settings: overrides?.cases_settings ?? true, + create_comment: overrides?.create_comment ?? true, + case_reopen: overrides?.case_reopen ?? true, }; }; diff --git a/x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.test.tsx b/x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.test.tsx new file mode 100644 index 0000000000000..37957c9fe1f8e --- /dev/null +++ b/x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.test.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 { renderHook } from '@testing-library/react-hooks'; +import { CaseStatuses } from '../../../../common/types/domain'; +import { useUserPermissions } from '../../user_actions/use_user_permissions'; +import { useShouldDisableStatus } from './use_should_disable_status'; + +jest.mock('../../user_actions/use_user_permissions'); +const mockUseUserPermissions = useUserPermissions as jest.Mock; + +describe('useShouldDisableStatus', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should disable status when user has no permissions', () => { + mockUseUserPermissions.mockReturnValue({ + canUpdate: false, + canReopenCase: false, + }); + + const { result } = renderHook(() => useShouldDisableStatus()); + + const cases = [{ status: CaseStatuses.open }]; + expect(result.current(cases)).toBe(true); + }); + + it('should allow status change when user has all permissions', () => { + mockUseUserPermissions.mockReturnValue({ + canUpdate: true, + canReopenCase: true, + }); + + const { result } = renderHook(() => useShouldDisableStatus()); + + const cases = [{ status: CaseStatuses.open }]; + expect(result.current(cases)).toBe(false); + }); + + it('should only allow reopening when user can only reopen cases', () => { + mockUseUserPermissions.mockReturnValue({ + canUpdate: false, + canReopenCase: true, + }); + + const { result } = renderHook(() => useShouldDisableStatus()); + + const cases = [{ status: CaseStatuses.closed }, { status: CaseStatuses.open }]; + + expect(result.current(cases)).toBe(false); + + const closedCases = [{ status: CaseStatuses.closed }]; + expect(result.current(closedCases)).toBe(false); + }); + + it('should prevent reopening closed cases when user cannot reopen', () => { + mockUseUserPermissions.mockReturnValue({ + canUpdate: true, + canReopenCase: false, + }); + + const { result } = renderHook(() => useShouldDisableStatus()); + + const closedCases = [{ status: CaseStatuses.closed }]; + expect(result.current(closedCases)).toBe(true); + + const openCases = [{ status: CaseStatuses.open }]; + expect(result.current(openCases)).toBe(false); + }); + + it('should handle multiple selected cases correctly', () => { + mockUseUserPermissions.mockReturnValue({ + canUpdate: true, + canReopenCase: false, + }); + + const { result } = renderHook(() => useShouldDisableStatus()); + + const mixedCases = [{ status: CaseStatuses.open }, { status: CaseStatuses.closed }]; + + expect(result.current(mixedCases)).toBe(true); + }); +}); diff --git a/x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.tsx b/x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.tsx new file mode 100644 index 0000000000000..e329a3c8787b7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/actions/status/use_should_disable_status.tsx @@ -0,0 +1,39 @@ +/* + * 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 { useCallback } from 'react'; +import type { CasesUI } from '../../../../common'; +import { CaseStatuses } from '../../../../common/types/domain'; + +import { useUserPermissions } from '../../user_actions/use_user_permissions'; + +export const useShouldDisableStatus = () => { + const { canUpdate, canReopenCase } = useUserPermissions(); + + const shouldDisableStatusFn = useCallback( + (selectedCases: Array>) => { + // Read Only + Disabled => Cannot do anything + const missingAllUpdatePermissions = !canUpdate && !canReopenCase; + if (missingAllUpdatePermissions) return true; + + // All + Enabled reopen => can change status at any point in any way + if (canUpdate && canReopenCase) return false; + + const selectedCasesContainsClosed = selectedCases.some( + (theCase) => theCase.status === CaseStatuses.closed + ); + + if (selectedCasesContainsClosed) { + return !canReopenCase; + } else { + return !canUpdate; + } + }, + [canReopenCase, canUpdate] + ); + + return shouldDisableStatusFn; +}; diff --git a/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx b/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx index bb4aef3379aa3..5ad7f9803dd67 100644 --- a/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx +++ b/x-pack/plugins/cases/public/components/actions/status/use_status_action.test.tsx @@ -13,7 +13,11 @@ import { useStatusAction } from './use_status_action'; import * as api from '../../../containers/api'; import { basicCase } from '../../../containers/mock'; import { CaseStatuses } from '../../../../common/types/domain'; +import { useUserPermissions } from '../../user_actions/use_user_permissions'; +import { useShouldDisableStatus } from './use_should_disable_status'; +jest.mock('../../user_actions/use_user_permissions'); +jest.mock('./use_should_disable_status'); jest.mock('../../../containers/api'); describe('useStatusAction', () => { @@ -24,6 +28,12 @@ describe('useStatusAction', () => { beforeEach(() => { appMockRender = createAppMockRenderer(); jest.clearAllMocks(); + (useShouldDisableStatus as jest.Mock).mockReturnValue(() => false); + + (useUserPermissions as jest.Mock).mockReturnValue({ + canUpdate: true, + canReopenCase: true, + }); }); it('renders an action', async () => { @@ -43,7 +53,7 @@ describe('useStatusAction', () => { Array [ Object { "data-test-subj": "cases-bulk-action-status-open", - "disabled": true, + "disabled": false, "icon": "empty", "key": "cases-bulk-action-status-open", "name": "Open", @@ -172,6 +182,8 @@ describe('useStatusAction', () => { ]; it.each(disabledTests)('disables the status button correctly: %s', async (status, index) => { + (useShouldDisableStatus as jest.Mock).mockReturnValue(() => true); + const { result } = renderHook( () => useStatusAction({ onAction, onActionSuccess, isDisabled: false }), { @@ -197,4 +209,36 @@ describe('useStatusAction', () => { expect(actions[index].disabled).toBe(true); } ); + + it('respects user permissions when everything is false', () => { + (useUserPermissions as jest.Mock).mockReturnValue({ + canUpdate: false, + canReopenCase: false, + }); + + const { result } = renderHook( + () => useStatusAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + expect(result.current.canUpdateStatus).toBe(false); + }); + + it('respects user permissions when only reopen is true', () => { + (useUserPermissions as jest.Mock).mockReturnValue({ + canUpdate: false, + canReopenCase: true, + }); + + const { result } = renderHook( + () => useStatusAction({ onAction, onActionSuccess, isDisabled: false }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + expect(result.current.canUpdateStatus).toBe(true); + }); }); diff --git a/x-pack/plugins/cases/public/components/actions/status/use_status_action.tsx b/x-pack/plugins/cases/public/components/actions/status/use_status_action.tsx index eb00800961085..abbc0535656d3 100644 --- a/x-pack/plugins/cases/public/components/actions/status/use_status_action.tsx +++ b/x-pack/plugins/cases/public/components/actions/status/use_status_action.tsx @@ -14,7 +14,8 @@ import { CaseStatuses } from '../../../../common/types/domain'; import * as i18n from './translations'; import type { UseActionProps } from '../types'; import { statuses } from '../../status'; -import { useCasesContext } from '../../cases_context/use_cases_context'; +import { useUserPermissions } from '../../user_actions/use_user_permissions'; +import { useShouldDisableStatus } from './use_should_disable_status'; const getStatusToasterMessage = (status: CaseStatuses, cases: CasesUI): string => { const totalCases = cases.length; @@ -35,9 +36,6 @@ interface UseStatusActionProps extends UseActionProps { selectedStatus?: CaseStatuses; } -const shouldDisableStatus = (cases: CasesUI, status: CaseStatuses) => - cases.every((theCase) => theCase.status === status); - export const useStatusAction = ({ onAction, onActionSuccess, @@ -45,10 +43,7 @@ export const useStatusAction = ({ selectedStatus, }: UseStatusActionProps) => { const { mutate: updateCases } = useUpdateCases(); - const { permissions } = useCasesContext(); - const canUpdateStatus = permissions.update; - const isActionDisabled = isDisabled || !canUpdateStatus; - + const { canUpdate, canReopenCase } = useUserPermissions(); const handleUpdateCaseStatus = useCallback( (selectedCases: CasesUI, status: CaseStatuses) => { onAction(); @@ -69,6 +64,8 @@ export const useStatusAction = ({ [onAction, updateCases, onActionSuccess] ); + const shouldDisableStatus = useShouldDisableStatus(); + const getStatusIcon = (status: CaseStatuses): string => selectedStatus && selectedStatus === status ? 'check' : 'empty'; @@ -78,7 +75,7 @@ export const useStatusAction = ({ name: statuses[CaseStatuses.open].label, icon: getStatusIcon(CaseStatuses.open), onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses.open), - disabled: isActionDisabled || shouldDisableStatus(selectedCases, CaseStatuses.open), + disabled: isDisabled || shouldDisableStatus(selectedCases), 'data-test-subj': 'cases-bulk-action-status-open', key: 'cases-bulk-action-status-open', }, @@ -86,8 +83,7 @@ export const useStatusAction = ({ name: statuses[CaseStatuses['in-progress']].label, icon: getStatusIcon(CaseStatuses['in-progress']), onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses['in-progress']), - disabled: - isActionDisabled || shouldDisableStatus(selectedCases, CaseStatuses['in-progress']), + disabled: isDisabled || shouldDisableStatus(selectedCases), 'data-test-subj': 'cases-bulk-action-status-in-progress', key: 'cases-bulk-action-status-in-progress', }, @@ -95,14 +91,14 @@ export const useStatusAction = ({ name: statuses[CaseStatuses.closed].label, icon: getStatusIcon(CaseStatuses.closed), onClick: () => handleUpdateCaseStatus(selectedCases, CaseStatuses.closed), - disabled: isActionDisabled || shouldDisableStatus(selectedCases, CaseStatuses.closed), + disabled: isDisabled || shouldDisableStatus(selectedCases), 'data-test-subj': 'cases-bulk-action-status-closed', key: 'cases-bulk-status-action', }, ]; }; - return { getActions, canUpdateStatus }; + return { getActions, canUpdateStatus: canUpdate || canReopenCase }; }; export type UseStatusAction = ReturnType; diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index 5664151aa6df0..60fcb320ddfd0 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -10,7 +10,12 @@ import { waitFor, act, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { noop } from 'lodash/fp'; -import { noCreateCasesPermissions, TestProviders, createAppMockRenderer } from '../../common/mock'; +import { + onlyCreateCommentPermissions, + noCreateCommentCasesPermissions, + TestProviders, + createAppMockRenderer, +} from '../../common/mock'; import { AttachmentType } from '../../../common/types/domain'; import { SECURITY_SOLUTION_OWNER, MAX_COMMENT_LENGTH } from '../../../common/constants'; @@ -93,19 +98,36 @@ describe('AddComment ', () => { expect(screen.getByTestId('submit-comment')).toHaveAttribute('disabled'); }); - it('should hide the component when the user does not have create permissions', () => { + it('should hide the component when the user does not have createComment permissions', () => { createAttachmentsMock.mockImplementation(() => ({ ...defaultResponse, isLoading: true, })); appMockRender.render( - + ); expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('add-comment-form-wrapper')).not.toBeInTheDocument(); + }); + + it('should show the component when the user does not have create permissions, but has createComment permissions', () => { + createAttachmentsMock.mockImplementation(() => ({ + ...defaultResponse, + isLoading: true, + })); + + appMockRender.render( + + + + ); + + expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('add-comment-form-wrapper')).toBeInTheDocument(); }); it('should post comment on submit click', async () => { diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index c84f799b1c899..11d3b89eb13d2 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -191,8 +191,8 @@ export const AddComment = React.memo( size="xl" /> )} - {permissions.create && ( - + {permissions.createComment && ( + { expect(res.getByTestId(`case-action-popover-button-${basicCase.id}`)).toBeDisabled(); }); }); + + it('shows actions when user only has reopenCase permission and only when case is closed', async () => { + appMockRender = createAppMockRenderer({ + permissions: { + all: false, + read: true, + create: false, + update: false, + delete: false, + reopenCase: true, + push: false, + connectors: true, + settings: false, + createComment: false, + }, + }); + + const { result } = renderHook(() => useActions({ disableActions: false }), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current.actions).not.toBe(null); + const caseWithClosedStatus = { ...basicCase, status: CaseStatuses.closed }; + const comp = result.current.actions!.render(caseWithClosedStatus) as React.ReactElement; + const res = appMockRender.render(comp); + + await user.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`)); + await waitForEuiPopoverOpen(); + + expect(res.queryByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument(); + expect(res.queryByTestId(`case-action-severity-panel-${basicCase.id}`)).toBeFalsy(); + expect(res.queryByTestId('cases-bulk-action-delete')).toBeFalsy(); + expect(res.getByTestId('cases-action-copy-id')).toBeInTheDocument(); + expect(res.queryByTestId(`actions-separator-${basicCase.id}`)).toBeFalsy(); + }); + + it('shows actions with combination of reopenCase and other permissions', async () => { + appMockRender = createAppMockRenderer({ + permissions: { + all: false, + read: true, + create: false, + update: false, + delete: true, + reopenCase: true, + push: false, + connectors: true, + settings: false, + createComment: false, + }, + }); + + const { result } = renderHook(() => useActions({ disableActions: false }), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current.actions).not.toBe(null); + const caseWithClosedStatus = { ...basicCase, status: CaseStatuses.closed }; + + const comp = result.current.actions!.render(caseWithClosedStatus) as React.ReactElement; + const res = appMockRender.render(comp); + + await user.click(res.getByTestId(`case-action-popover-button-${basicCase.id}`)); + await waitForEuiPopoverOpen(); + + expect(res.queryByTestId(`case-action-status-panel-${basicCase.id}`)).toBeInTheDocument(); + expect(res.queryByTestId(`case-action-severity-panel-${basicCase.id}`)).toBeFalsy(); + expect(res.getByTestId('cases-bulk-action-delete')).toBeInTheDocument(); + expect(res.getByTestId('cases-action-copy-id')).toBeInTheDocument(); + }); + + it('shows no actions with everything false but read', async () => { + appMockRender = createAppMockRenderer({ + permissions: { + all: false, + read: true, + create: false, + update: false, + delete: false, + reopenCase: false, + push: false, + connectors: true, + settings: false, + createComment: false, + }, + }); + + const { result } = renderHook(() => useActions({ disableActions: false }), { + wrapper: appMockRender.AppWrapper, + }); + + expect(result.current.actions).toBe(null); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx b/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx index 4c43201b1eab4..e34f64a2a6283 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_actions.tsx @@ -28,6 +28,7 @@ import { EditTagsFlyout } from '../actions/tags/edit_tags_flyout'; import { useAssigneesAction } from '../actions/assignees/use_assignees_action'; import { EditAssigneesFlyout } from '../actions/assignees/edit_assignees_flyout'; import { useCopyIDAction } from '../actions/copy_id/use_copy_id_action'; +import { useShouldDisableStatus } from '../actions/status/use_should_disable_status'; const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean }> = ({ theCase, @@ -38,6 +39,12 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean const closePopover = useCallback(() => setIsPopoverOpen(false), []); const refreshCases = useRefreshCases(); + const shouldDisable = useShouldDisableStatus(); + + const shouldDisableStatus = useMemo(() => { + return shouldDisable([theCase]); + }, [theCase, shouldDisable]); + const deleteAction = useDeleteAction({ isDisabled: false, onAction: closePopover, @@ -83,7 +90,7 @@ const ActionColumnComponent: React.FC<{ theCase: CaseUI; disableActions: boolean { id: 0, items: mainPanelItems, title: i18n.ACTIONS }, ]; - if (canUpdate) { + if (!shouldDisableStatus) { mainPanelItems.push({ name: ( { const { permissions } = useCasesContext(); - const shouldShowActions = permissions.update || permissions.delete; + const shouldShowActions = permissions.update || permissions.delete || permissions.reopenCase; return { actions: shouldShowActions diff --git a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx index fcf3da36fba96..1838ee3b14f59 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.test.tsx @@ -17,10 +17,12 @@ import { createAppMockRenderer, noDeleteCasesPermissions, onlyDeleteCasesPermission, + noReopenCasesPermissions, + onlyReopenCasesPermission, } from '../../common/mock'; import { useBulkActions } from './use_bulk_actions'; import * as api from '../../containers/api'; -import { basicCase } from '../../containers/mock'; +import { basicCase, basicCaseClosed } from '../../containers/mock'; jest.mock('../../containers/api'); jest.mock('../../containers/user_profiles/api'); @@ -117,7 +119,7 @@ describe('useBulkActions', () => { "items": Array [ Object { "data-test-subj": "cases-bulk-action-status-open", - "disabled": true, + "disabled": false, "icon": "empty", "key": "cases-bulk-action-status-open", "name": "Open", @@ -523,5 +525,72 @@ describe('useBulkActions', () => { expect(res.queryByTestId('bulk-actions-separator')).toBeFalsy(); }); }); + + it('shows the correct actions with no reopen permissions', async () => { + appMockRender = createAppMockRenderer({ permissions: noReopenCasesPermissions() }); + const { result, waitFor: waitForHook } = renderHook( + () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCaseClosed] }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const modals = result.current.modals; + const panels = result.current.panels; + + const res = appMockRender.render( + <> + + {modals} + + ); + + await waitForHook(() => { + expect(res.queryByTestId('case-bulk-action-status')).toBeInTheDocument(); + res.queryByTestId('case-bulk-action-status')?.click(); + }); + + await waitForHook(() => { + expect(res.queryByTestId('cases-bulk-action-status-open')).toBeDisabled(); + expect(res.queryByTestId('cases-bulk-action-status-in-progress')).toBeDisabled(); + expect(res.queryByTestId('cases-bulk-action-status-closed')).toBeDisabled(); + }); + }); + + it('shows the correct actions with reopen permissions', async () => { + appMockRender = createAppMockRenderer({ permissions: onlyReopenCasesPermission() }); + const { result } = renderHook( + () => useBulkActions({ onAction, onActionSuccess, selectedCases: [basicCaseClosed] }), + { + wrapper: appMockRender.AppWrapper, + } + ); + + const { modals, flyouts, panels } = result.current; + const renderResult = appMockRender.render( + <> + + {modals} + {flyouts} + + ); + + await waitFor(() => { + expect(renderResult.queryByTestId('case-bulk-action-status')).toBeInTheDocument(); + expect(renderResult.queryByTestId('case-bulk-action-severity')).toBeInTheDocument(); + expect(renderResult.queryByTestId('bulk-actions-separator')).not.toBeInTheDocument(); + expect(renderResult.queryByTestId('case-bulk-action-delete')).not.toBeInTheDocument(); + }); + + userEvent.click(renderResult.getByTestId('case-bulk-action-status')); + + await waitFor(() => { + expect(renderResult.queryByTestId('cases-bulk-action-status-open')).not.toBeDisabled(); + expect( + renderResult.queryByTestId('cases-bulk-action-status-in-progress') + ).not.toBeDisabled(); + expect(renderResult.queryByTestId('cases-bulk-action-status-closed')).not.toBeDisabled(); + }); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx index 009dfbf99f262..98828b00369f5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/use_bulk_actions.tsx @@ -76,9 +76,6 @@ export const useBulkActions = ({ const panels = useMemo((): EuiContextMenuPanelDescriptor[] => { const mainPanelItems: EuiContextMenuPanelItemDescriptor[] = []; - const panelsToBuild: EuiContextMenuPanelDescriptor[] = [ - { id: 0, items: mainPanelItems, title: i18n.ACTIONS }, - ]; if (canUpdate) { mainPanelItems.push({ @@ -119,7 +116,13 @@ export const useBulkActions = ({ if (canDelete) { mainPanelItems.push(deleteAction.getAction(selectedCases)); } - + const panelsToBuild: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + items: [...mainPanelItems], // Create a new array instead of using reference + title: i18n.ACTIONS, + }, + ]; if (canUpdate) { panelsToBuild.push({ id: 1, diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index 6808735a41184..389de5068ed51 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -94,7 +94,9 @@ export const CasesTableUtilityBar: FunctionComponent = React.memo( * Granular permission check for each action is performed * in the useBulkActions hook. */ - const showBulkActions = (permissions.update || permissions.delete) && selectedCases.length > 0; + const showBulkActions = + (permissions.update || permissions.delete || permissions.reopenCase) && + selectedCases.length > 0; const visibleCases = pagination?.pageSize && totalCases > pagination.pageSize ? pagination.pageSize : totalCases; diff --git a/x-pack/plugins/cases/public/components/app/index.tsx b/x-pack/plugins/cases/public/components/app/index.tsx index cc6c572275721..eaa334470ab0f 100644 --- a/x-pack/plugins/cases/public/components/app/index.tsx +++ b/x-pack/plugins/cases/public/components/app/index.tsx @@ -39,7 +39,7 @@ const CasesAppComponent: React.FC = ({ getFilesClient, owner: [APP_OWNER], useFetchAlertData: () => [false, {}], - permissions: userCapabilities.generalCases, + permissions: userCapabilities.generalCasesV2, basePath: '/', features: { alerts: { enabled: true, sync: false } }, })} diff --git a/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts b/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts index a26647704785f..4cd015de0c92e 100644 --- a/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts +++ b/x-pack/plugins/cases/public/components/app/use_available_owners.test.ts @@ -21,15 +21,15 @@ jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.MockedFunction; const hasAll = { - securitySolutionCases: allCasesCapabilities(), - observabilityCases: allCasesCapabilities(), - generalCases: allCasesCapabilities(), + securitySolutionCasesV2: allCasesCapabilities(), + observabilityCasesV2: allCasesCapabilities(), + generalCasesV2: allCasesCapabilities(), }; const secAllObsReadGenNone = { - securitySolutionCases: allCasesCapabilities(), - observabilityCases: readCasesCapabilities(), - generalCases: noCasesCapabilities(), + securitySolutionCasesV2: allCasesCapabilities(), + observabilityCasesV2: readCasesCapabilities(), + generalCasesV2: noCasesCapabilities(), }; const unrelatedFeatures = { diff --git a/x-pack/plugins/cases/public/components/app/use_available_owners.ts b/x-pack/plugins/cases/public/components/app/use_available_owners.ts index c829b9c590d01..4220ff8cdecd4 100644 --- a/x-pack/plugins/cases/public/components/app/use_available_owners.ts +++ b/x-pack/plugins/cases/public/components/app/use_available_owners.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { APP_ID, FEATURE_ID } from '../../../common/constants'; +import { APP_ID, FEATURE_ID_V2 } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; import type { CasesPermissions } from '../../containers/types'; import { allCasePermissions } from '../../utils/permissions'; @@ -25,7 +25,7 @@ export const useAvailableCasesOwners = ( return Object.entries(kibanaCapabilities).reduce( (availableOwners: string[], [featureId, kibanaCapability]) => { - if (!featureId.endsWith('Cases')) { + if (!featureId.endsWith('CasesV2')) { return availableOwners; } for (const cap of capabilities) { @@ -42,9 +42,9 @@ export const useAvailableCasesOwners = ( }; const getOwnerFromFeatureID = (featureID: string) => { - if (featureID === FEATURE_ID) { + if (featureID === FEATURE_ID_V2) { return APP_ID; } - return featureID.replace('Cases', ''); + return featureID.replace('CasesV2', ''); }; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index d6c17febb6348..7fd13396086c7 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiButtonEmpty, useEuiTheme } from '@elastic/eui'; import type { CaseStatuses } from '../../../common/types/domain'; @@ -23,6 +23,7 @@ import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_pa import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesFeatures } from '../../common/use_cases_features'; import { useGetCaseConnectors } from '../../containers/use_get_case_connectors'; +import { useShouldDisableStatus } from '../actions/status/use_should_disable_status'; export interface CaseActionBarProps { caseData: CaseUI; @@ -67,6 +68,11 @@ const CaseActionBarComponent: React.FC = ({ [caseData.settings, onUpdateField] ); + const shouldDisableStatusFn = useShouldDisableStatus(); + const isStatusMenuDisabled = useMemo(() => { + return shouldDisableStatusFn([caseData]); + }, [caseData, shouldDisableStatusFn]); + return ( = ({ diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx index 95d36bb058d79..e4497b14ff75e 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx @@ -10,17 +10,24 @@ import { mount } from 'enzyme'; import { CaseStatuses } from '../../../common/types/domain'; import { StatusContextMenu } from './status_context_menu'; +import { TestProviders } from '../../common/mock'; +import { useShouldDisableStatus } from '../actions/status/use_should_disable_status'; -describe('SyncAlertsSwitch', () => { +jest.mock('../actions/status/use_should_disable_status'); + +describe('StatusContextMenu', () => { const onStatusChanged = jest.fn(); beforeEach(() => { jest.clearAllMocks(); + (useShouldDisableStatus as jest.Mock).mockReturnValue(() => false); }); it('renders', async () => { const wrapper = mount( - + + + ); expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeTruthy(); @@ -28,11 +35,13 @@ describe('SyncAlertsSwitch', () => { it('renders a simple status badge when disabled', async () => { const wrapper = mount( - + + + ); expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeFalsy(); @@ -41,7 +50,9 @@ describe('SyncAlertsSwitch', () => { it('renders the current status correctly', async () => { const wrapper = mount( - + + + ); expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toBe( @@ -51,7 +62,9 @@ describe('SyncAlertsSwitch', () => { it('changes the status', async () => { const wrapper = mount( - + + + ); wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click'); @@ -62,14 +75,61 @@ describe('SyncAlertsSwitch', () => { expect(onStatusChanged).toHaveBeenCalledWith('in-progress'); }); - it('does not call onStatusChanged if selection is same as current status', async () => { + it('does not render the button at all if the status cannot change', async () => { + (useShouldDisableStatus as jest.Mock).mockReturnValue(() => true); const wrapper = mount( - + + + ); wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click'); - wrapper.find(`[data-test-subj="case-view-status-dropdown-open"] button`).simulate('click'); + expect(wrapper.find(`[data-test-subj="case-view-status-dropdown-open"] button`)).toHaveLength( + 0 + ); expect(onStatusChanged).not.toHaveBeenCalled(); }); + + it('updates menu items when shouldDisableStatus changes', async () => { + const mockShouldDisableStatus = jest.fn().mockReturnValue(false); + (useShouldDisableStatus as jest.Mock).mockReturnValue(mockShouldDisableStatus); + + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click'); + + expect(mockShouldDisableStatus).toHaveBeenCalledWith([{ status: CaseStatuses.open }]); + }); + + it('handles all statuses being disabled', async () => { + (useShouldDisableStatus as jest.Mock).mockReturnValue(() => true); + + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click'); + expect(wrapper.find('EuiContextMenuItem').prop('onClick')).toBeUndefined(); + }); + + it('correctly evaluates each status option', async () => { + (useShouldDisableStatus as jest.Mock).mockReturnValue(false); + + const wrapper = mount( + + + + ); + + expect( + wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).exists() + ).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index 422cf1aa44b80..b1c65fc796b46 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -12,6 +12,7 @@ import type { CaseStatuses } from '../../../common/types/domain'; import { caseStatuses } from '../../../common/types/domain'; import { StatusPopoverButton } from '../status'; import { CHANGE_STATUS } from '../all_cases/translations'; +import { useShouldDisableStatus } from '../actions/status/use_should_disable_status'; interface Props { currentStatus: CaseStatuses; @@ -27,6 +28,7 @@ const StatusContextMenuComponent: React.FC = ({ onStatusChanged, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const shouldDisableStatus = useShouldDisableStatus(); const togglePopover = useCallback( () => setIsPopoverOpen((prevPopoverStatus) => !prevPopoverStatus), [] @@ -57,17 +59,19 @@ const StatusContextMenuComponent: React.FC = ({ const panelItems = useMemo( () => - caseStatuses.map((status: CaseStatuses) => ( - onContextMenuItemClick(status)} - > - - - )), - [currentStatus, onContextMenuItemClick] + caseStatuses + .filter((_: CaseStatuses) => !shouldDisableStatus([{ status: currentStatus }])) + .map((status: CaseStatuses) => ( + onContextMenuItemClick(status)} + > + + + )), + [currentStatus, onContextMenuItemClick, shouldDisableStatus] ); if (disabled) { diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx index 85c267f5d05d7..77aee6551ac03 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -98,6 +98,8 @@ export const CasesProvider: FC< read: permissions.read, settings: permissions.settings, update: permissions.update, + reopenCase: permissions.reopenCase, + createComment: permissions.createComment, }, basePath, /** @@ -127,6 +129,8 @@ export const CasesProvider: FC< permissions.read, permissions.settings, permissions.update, + permissions.reopenCase, + permissions.createComment, ] ); diff --git a/x-pack/plugins/cases/public/components/files/add_file.test.tsx b/x-pack/plugins/cases/public/components/files/add_file.test.tsx index 69aa9e87a34e7..9a27b8780db2d 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.test.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.test.tsx @@ -107,19 +107,9 @@ describe('AddFile', () => { expect(await screen.findByTestId('cases-files-add')).toBeInTheDocument(); }); - it('AddFile is not rendered if user has no create permission', async () => { + it('AddFile is not rendered if user has no createComment permission', async () => { appMockRender = createAppMockRenderer({ - permissions: buildCasesPermissions({ create: false }), - }); - - appMockRender.render(); - - expect(screen.queryByTestId('cases-files-add')).not.toBeInTheDocument(); - }); - - it('AddFile is not rendered if user has no update permission', async () => { - appMockRender = createAppMockRenderer({ - permissions: buildCasesPermissions({ update: false }), + permissions: buildCasesPermissions({ createComment: false }), }); appMockRender.render(); diff --git a/x-pack/plugins/cases/public/components/files/add_file.tsx b/x-pack/plugins/cases/public/components/files/add_file.tsx index 7b91879834a78..ab83b75920d59 100644 --- a/x-pack/plugins/cases/public/components/files/add_file.tsx +++ b/x-pack/plugins/cases/public/components/files/add_file.tsx @@ -107,7 +107,7 @@ const AddFileComponent: React.FC = ({ caseId }) => { [caseId, createAttachments, owner, refreshAttachmentsTable, showDangerToast, showSuccessToast] ); - return permissions.create && permissions.update ? ( + return permissions.createComment ? ( { it('sets all available solutions correctly', () => { appMockRender = createAppMockRenderer({ owner: [] }); /** - * We set securitySolutionCases capability to not have + * We set securitySolutionCasesV2 capability to not have * any access to cases. This tests that we get the owners * that have at least read access. */ appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, - securitySolutionCases: noCasesCapabilities(), + securitySolutionCasesV2: noCasesCapabilities(), }; appMockRender.render(); diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx index a17dee7423fe3..793405276cdb4 100644 --- a/x-pack/plugins/cases/public/components/user_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx @@ -16,7 +16,6 @@ import { getManualAlertIdsWithNoRuleId } from './helpers'; import type { UserActionTreeProps } from './types'; import { useUserActionsHandler } from './use_user_actions_handler'; import { NEW_COMMENT_ID } from './constants'; -import { useCasesContext } from '../cases_context/use_cases_context'; import { UserToolTip } from '../user_profiles/user_tooltip'; import { Username } from '../user_profiles/username'; import { HoverableAvatar } from '../user_profiles/hoverable_avatar'; @@ -25,6 +24,7 @@ import { useUserActionsPagination } from './use_user_actions_pagination'; import { useLastPageUserActions } from './use_user_actions_last_page'; import { ShowMoreButton } from './show_more_button'; import { useLastPage } from './use_last_page'; +import { useUserPermissions } from './use_user_permissions'; const getIconsCss = (hasNextPage: boolean | undefined, euiTheme: EuiThemeComputed<{}>): string => { const customSize = hasNextPage @@ -108,10 +108,10 @@ export const UserActions = React.memo((props: UserActionTreeProps) => { const [loadingAlertData, manualAlertsData] = useFetchAlertData(alertIdsWithoutRuleInfo); - const { permissions } = useCasesContext(); + const { getCanAddUserComments } = useUserPermissions(); // add-comment markdown is not visible in History filter - const showCommentEditor = permissions.create && userActivityQueryParams.type !== 'action'; + const shouldShowCommentEditor = getCanAddUserComments(userActivityQueryParams); const { commentRefs, @@ -136,7 +136,7 @@ export const UserActions = React.memo((props: UserActionTreeProps) => { [caseId, handleUpdate, handleManageMarkdownEditId, statusActionButton, commentRefs] ); - const bottomActions = showCommentEditor + const bottomActions = shouldShowCommentEditor ? [ { username: ( diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_permissions.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_permissions.test.tsx new file mode 100644 index 0000000000000..e7c712b0df590 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_permissions.test.tsx @@ -0,0 +1,259 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import { useUserPermissions } from './use_user_permissions'; +import type { UserActivityParams } from '../user_actions_activity_bar/types'; + +jest.mock('../cases_context/use_cases_context'); +const mockUseCasesContext = useCasesContext as jest.Mock; + +describe('useUserPermissions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('canUpdate permission', () => { + it('should return true when user has update permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: true, + reopenCase: false, + createComment: false, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + expect(result.current.canUpdate).toBe(true); + }); + + it('should return false when user lacks update permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: false, + reopenCase: true, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + expect(result.current.canUpdate).toBe(false); + }); + }); + + describe('canReopenCase permission', () => { + it('should return true when user has reopenCase permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: false, + reopenCase: true, + createComment: false, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + expect(result.current.canReopenCase).toBe(true); + }); + + it('should return false when user lacks reopenCase permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: true, + reopenCase: false, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + expect(result.current.canReopenCase).toBe(false); + }); + }); + + describe('getCanAddUserComments permission', () => { + it('should return false when activity type is "action" regardless of createComment permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: false, + reopenCase: false, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + const userActivityParams: UserActivityParams = { + page: 1, + perPage: 10, + sortOrder: 'asc', + type: 'action', + }; + + expect(result.current.getCanAddUserComments(userActivityParams)).toBe(false); + }); + + it('should return true when type is not "action" and user has createComment permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: false, + reopenCase: false, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + const userActivityParams: UserActivityParams = { + page: 1, + perPage: 10, + sortOrder: 'asc', + type: 'user', + }; + + expect(result.current.getCanAddUserComments(userActivityParams)).toBe(true); + }); + + it('should return false when type is not "action" but user lacks createComment permission', () => { + mockUseCasesContext.mockReturnValue({ + permissions: { + update: true, + reopenCase: true, + createComment: false, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }, + }); + + const { result } = renderHook(() => useUserPermissions()); + const userActivityParams: UserActivityParams = { + page: 1, + perPage: 10, + sortOrder: 'asc', + type: 'user', + }; + + expect(result.current.getCanAddUserComments(userActivityParams)).toBe(false); + }); + }); + + it('should maintain stable references to memoized values when permissions do not change', () => { + const permissions = { + update: true, + reopenCase: true, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }; + + mockUseCasesContext.mockReturnValue({ permissions }); + + const { result, rerender } = renderHook(() => useUserPermissions()); + + const initialCanUpdate = result.current.canUpdate; + const initialCanReopenCase = result.current.canReopenCase; + const initialGetCanAddUserComments = result.current.getCanAddUserComments; + + rerender(); + + expect(result.current.canUpdate).toBe(initialCanUpdate); + expect(result.current.canReopenCase).toBe(initialCanReopenCase); + expect(result.current.getCanAddUserComments).toBe(initialGetCanAddUserComments); + }); + + it('should update memoized values when permissions change', () => { + const initialPermissions = { + update: true, + reopenCase: true, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }; + + mockUseCasesContext.mockReturnValue({ permissions: initialPermissions }); + + const { result, rerender } = renderHook(() => useUserPermissions()); + + const initialCanUpdate = result.current.canUpdate; + const initialCanReopenCase = result.current.canReopenCase; + const initialGetCanAddUserComments = result.current.getCanAddUserComments; + + const newPermissions = { + update: false, + reopenCase: false, + createComment: true, + all: false, + read: true, + create: false, + delete: false, + push: false, + connectors: true, + settings: false, + }; + + mockUseCasesContext.mockReturnValue({ permissions: newPermissions }); + rerender(); + + expect(result.current.canUpdate).not.toBe(initialCanUpdate); + expect(result.current.canReopenCase).not.toBe(initialCanReopenCase); + expect(result.current.getCanAddUserComments).toBe(initialGetCanAddUserComments); + }); +}); diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_permissions.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_permissions.tsx new file mode 100644 index 0000000000000..f0a79a6e285a5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_actions/use_user_permissions.tsx @@ -0,0 +1,38 @@ +/* + * 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 { useCallback } from 'react'; +import { useCasesContext } from '../cases_context/use_cases_context'; +import type { UserActivityParams } from '../user_actions_activity_bar/types'; + +export const useUserPermissions = () => { + const { permissions } = useCasesContext(); + + /** + * Determines if a user has the capability to update the case. Reopening a case is not part of this capability. + */ + + const canUpdate = permissions.update; + + /** + * Determines if a user has the capability to change the case from closed => open or closed => in progress + */ + + const canReopenCase = permissions.reopenCase; + + /** + * Determines if a user has the capability to add comments and attachments + */ + const getCanAddUserComments = useCallback( + (userActivityQueryParams: UserActivityParams) => { + if (userActivityQueryParams.type === 'action') return false; + return permissions.createComment; + }, + [permissions.createComment] + ); + + return { getCanAddUserComments, canReopenCase, canUpdate }; +}; diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index 53900a6920f20..92d7abde2f9d2 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -69,7 +69,7 @@ describe('useGetCases', () => { appMockRender.coreStart.application.capabilities = { ...appMockRender.coreStart.application.capabilities, - observabilityCases: { + observabilityCasesV2: { create_cases: true, read_cases: true, update_cases: true, @@ -78,7 +78,7 @@ describe('useGetCases', () => { delete_cases: true, cases_settings: true, }, - securitySolutionCases: { + securitySolutionCasesV2: { create_cases: true, read_cases: true, update_cases: true, diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts index e267c108a9b39..3de6a96979065 100644 --- a/x-pack/plugins/cases/public/mocks.ts +++ b/x-pack/plugins/cases/public/mocks.ts @@ -50,6 +50,8 @@ const helpersMock: jest.Mocked = { push: false, connectors: false, settings: false, + createComment: false, + reopenCase: false, }), getRuleIdFromEvent: jest.fn(), groupAlertsByRule: jest.fn(), diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap index ebb9501ff8960..b8129f9111b9c 100644 --- a/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/audit_logger.test.ts.snap @@ -2520,6 +2520,90 @@ Object { } `; +exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" with an error and entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_reopen", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to update cases [id=1] as owner \\"awesome\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" with an error but no entity 1`] = ` +Object { + "error": Object { + "code": "Error", + "message": "an error", + }, + "event": Object { + "action": "case_reopen", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "change", + ], + }, + "message": "Failed attempt to update a case as any owners", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" without an error but with an entity 1`] = ` +Object { + "event": Object { + "action": "case_reopen", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "5", + "type": "cases", + }, + }, + "message": "User is updating cases [id=5] as owner \\"super\\"", +} +`; + +exports[`audit_logger log function event structure creates the correct audit event for operation: "reopenCase" without an error or entity 1`] = ` +Object { + "event": Object { + "action": "case_reopen", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "change", + ], + }, + "message": "User is updating a case as any owners", +} +`; + exports[`audit_logger log function event structure creates the correct audit event for operation: "resolveCase" with an error and entity 1`] = ` Object { "error": Object { diff --git a/x-pack/plugins/cases/server/authorization/__snapshots__/authorization.test.ts.snap b/x-pack/plugins/cases/server/authorization/__snapshots__/authorization.test.ts.snap new file mode 100644 index 0000000000000..23575aaad0ddd --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/__snapshots__/authorization.test.ts.snap @@ -0,0 +1,150 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`authorization ensureAuthorized with operation arrays handles multiple operations successfully when authorized 1`] = ` +Array [ + Array [ + Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User is creating cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], +] +`; + +exports[`authorization ensureAuthorized with operation arrays logs each operation separately 1`] = ` +Array [ + Array [ + Object { + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "unknown", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User is creating cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "success", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "User has accessed cases [id=1] as owner \\"a\\"", + }, + ], +] +`; + +exports[`authorization ensureAuthorized with operation arrays throws on first unauthorized operation in array 1`] = ` +Array [ + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to create, access case with owners: \\"a\\"", + }, + "event": Object { + "action": "case_create", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "creation", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to create cases [id=1] as owner \\"a\\"", + }, + ], + Array [ + Object { + "error": Object { + "code": "Error", + "message": "Unauthorized to create, access case with owners: \\"a\\"", + }, + "event": Object { + "action": "case_get", + "category": Array [ + "database", + ], + "outcome": "failure", + "type": Array [ + "access", + ], + }, + "kibana": Object { + "saved_object": Object { + "id": "1", + "type": "cases", + }, + }, + "message": "Failed attempt to access cases [id=1] as owner \\"a\\"", + }, + ], +] +`; diff --git a/x-pack/plugins/cases/server/authorization/audit_logger.ts b/x-pack/plugins/cases/server/authorization/audit_logger.ts index 338af379bbcc7..2de847586228a 100644 --- a/x-pack/plugins/cases/server/authorization/audit_logger.ts +++ b/x-pack/plugins/cases/server/authorization/audit_logger.ts @@ -82,15 +82,18 @@ export class AuthorizationAuditLogger { operation, }: { owners: string[]; - operation: OperationDetails; + operation: OperationDetails | OperationDetails[]; }) { const ownerMsg = owners.length <= 0 ? 'of any owner' : `with owners: "${owners.join(', ')}"`; + const operations = Array.isArray(operation) ? operation : [operation]; + const operationVerbs = [...new Set(operations.map((op) => op.verbs.present))].join(', '); + const operationDocTypes = [...new Set(operations.map((op) => op.docType))].join(', '); /** * This will take the form: * `Unauthorized to create case with owners: "securitySolution, observability"` * `Unauthorized to access cases of any owner` */ - return `Unauthorized to ${operation.verbs.present} ${operation.docType} ${ownerMsg}`; + return `Unauthorized to ${operationVerbs} ${operationDocTypes} ${ownerMsg}`; } /** diff --git a/x-pack/plugins/cases/server/authorization/authorization.test.ts b/x-pack/plugins/cases/server/authorization/authorization.test.ts index 6385bc03813a0..9ba13ed51dcb3 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.test.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.test.ts @@ -1459,4 +1459,80 @@ describe('authorization', () => { }); }); }); + + describe('ensureAuthorized with operation arrays', () => { + let auth: Authorization; + let securityStart: ReturnType; + let featuresStart: jest.Mocked; + let spacesStart: jest.Mocked; + + beforeEach(async () => { + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: true })) + ); + + featuresStart = featuresPluginMock.createStart(); + featuresStart.getKibanaFeatures.mockReturnValue([ + { id: '1', cases: ['a'] }, + ] as unknown as KibanaFeature[]); + + spacesStart = createSpacesDisabledFeaturesMock(); + + auth = await Authorization.create({ + request, + securityAuth: securityStart.authz, + spaces: spacesStart, + features: featuresStart, + auditLogger: new AuthorizationAuditLogger(mockLogger), + logger: loggingSystemMock.createLogger(), + }); + }); + + it('handles multiple operations successfully when authorized', async () => { + await expect( + auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'a' }], + operation: [Operations.createCase, Operations.getCase], + }) + ).resolves.not.toThrow(); + + expect(mockLogger.log.mock.calls).toMatchSnapshot(); + }); + + it('throws on first unauthorized operation in array', async () => { + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: false })) + ); + + await expect( + auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'a' }], + operation: [Operations.createCase, Operations.getCase], + }) + ).rejects.toThrow('Unauthorized to create, access case with owners: "a"'); + + expect(mockLogger.log.mock.calls).toMatchSnapshot(); + }); + + it('logs each operation separately', async () => { + await auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'a' }], + operation: [Operations.createCase, Operations.getCase], + }); + + expect(mockLogger.log).toHaveBeenCalledTimes(2); + expect(mockLogger.log.mock.calls).toMatchSnapshot(); + }); + + it('handles empty operation array', async () => { + await expect( + auth.ensureAuthorized({ + entities: [{ id: '1', owner: 'a' }], + operation: [], + }) + ).resolves.not.toThrow(); + }); + }); }); diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index ed255a5df18aa..f760e4498d06e 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -108,18 +108,17 @@ export class Authorization { operation, }: { entities: OwnerEntity[]; - operation: OperationDetails; + operation: OperationDetails | OperationDetails[]; }) { + const uniqueOwners = Array.from(new Set(entities.map((entity) => entity.owner))); + const operations = Array.isArray(operation) ? operation : [operation]; try { - const uniqueOwners = Array.from(new Set(entities.map((entity) => entity.owner))); - - await this._ensureAuthorized(uniqueOwners, operation); + await this._ensureAuthorized(uniqueOwners, operations); } catch (error) { - this.logSavedObjects({ entities, operation, error }); + this.logSavedObjects({ entities, operation: operations, error }); throw error; } - - this.logSavedObjects({ entities, operation }); + this.logSavedObjects({ entities, operation: operations }); } /** @@ -177,11 +176,15 @@ export class Authorization { error, }: { entities: OwnerEntity[]; - operation: OperationDetails; + operation: OperationDetails | OperationDetails[]; error?: Error; }) { + const operations = Array.isArray(operation) ? operation : [operation]; + for (const entity of entities) { - this.auditLogger.log({ operation, error, entity }); + for (const op of operations) { + this.auditLogger.log({ operation: op, error, entity }); + } } } @@ -197,15 +200,13 @@ export class Authorization { } } - private async _ensureAuthorized(owners: string[], operation: OperationDetails) { + private async _ensureAuthorized(owners: string[], operations: OperationDetails[]) { const { securityAuth } = this; const areAllOwnersAvailable = owners.every((owner) => this.featureCaseOwners.has(owner)); - if (securityAuth && this.shouldCheckAuthorization()) { - const requiredPrivileges: string[] = owners.map((owner) => - securityAuth.actions.cases.get(owner, operation.name) + const requiredPrivileges: string[] = operations.flatMap((operation) => + owners.map((owner) => securityAuth.actions.cases.get(owner, operation.name)) ); - const checkPrivileges = securityAuth.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested } = await checkPrivileges({ kibana: requiredPrivileges, @@ -219,14 +220,20 @@ export class Authorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); + throw Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ owners, operation: operations }) + ); } if (!hasAllRequested) { - throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); + throw Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ owners, operation: operations }) + ); } } else if (!areAllOwnersAvailable) { - throw Boom.forbidden(AuthorizationAuditLogger.createFailureMessage({ owners, operation })); + throw Boom.forbidden( + AuthorizationAuditLogger.createFailureMessage({ owners, operation: operations }) + ); } // else security is disabled so let the operation proceed @@ -288,7 +295,6 @@ export class Authorization { const { hasAllRequested, username, privileges } = await checkPrivileges({ kibana: [...requiredPrivileges.keys()], }); - return { hasAllRequested, username, diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index 12653aa6079e6..40b6c5d7101c5 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -59,7 +59,7 @@ const EVENT_TYPES: Record> = { }; /** - * These values need to match the respective values in this file: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + * These values need to match the respective values in this file: x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts * These are shared between find, get, get all, and delete/delete all * There currently isn't a use case for a user to delete one comment but not all or differentiating between get, get all, * and find operations from a privilege stand point. @@ -182,6 +182,14 @@ const CaseOperations = { docType: 'cases', savedObjectType: CASE_SAVED_OBJECT, }, + [WriteOperations.ReopenCase]: { + ecsType: EVENT_TYPES.change, + name: WriteOperations.ReopenCase as const, + action: 'case_reopen', + verbs: updateVerbs, + docType: 'case', + savedObjectType: CASE_SAVED_OBJECT, + }, }; const ConfigurationOperations = { diff --git a/x-pack/plugins/cases/server/authorization/types.ts b/x-pack/plugins/cases/server/authorization/types.ts index f97c6fc597457..1031e2db0ec77 100644 --- a/x-pack/plugins/cases/server/authorization/types.ts +++ b/x-pack/plugins/cases/server/authorization/types.ts @@ -63,6 +63,7 @@ export enum WriteOperations { UpdateComment = 'updateComment', CreateConfiguration = 'createConfiguration', UpdateConfiguration = 'updateConfiguration', + ReopenCase = 'reopenCase', } /** @@ -75,7 +76,7 @@ export interface OperationDetails { ecsType: ArrayElement; /** * The name of the operation to authorize against for the privilege check. - * These values need to match one of the operation strings defined here: x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + * These values need to match one of the operation strings defined here: x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts * * To avoid the authorization strings getting too large, new operations should generally fit within one of the * CasesSupportedOperations. In the situation where a new one is needed we'll have to add it to the security plugin. diff --git a/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts b/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts index 0109e6eda8808..755084d624b9f 100644 --- a/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/bulk_update.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CustomFieldTypes } from '../../../common/types/domain'; +import { CustomFieldTypes, CaseStatuses } from '../../../common/types/domain'; import { MAX_CATEGORY_LENGTH, MAX_DESCRIPTION_LENGTH, @@ -19,6 +19,7 @@ import { } from '../../../common/constants'; import { mockCases } from '../../mocks'; import { createCasesClientMock, createCasesClientMockArgs } from '../mocks'; +import { Operations } from '../../authorization'; import { bulkUpdate } from './bulk_update'; describe('update', () => { @@ -1628,5 +1629,135 @@ describe('update', () => { ); }); }); + + describe('Authorization', () => { + const clientArgs = createCasesClientMockArgs(); + + beforeEach(() => { + jest.clearAllMocks(); + clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: mockCases }); + clientArgs.services.caseService.getAllCaseComments.mockResolvedValue({ + saved_objects: [], + total: 0, + per_page: 10, + page: 1, + }); + clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue( + new Map() + ); + }); + + it('checks authorization for updateCase operation', async () => { + clientArgs.services.caseService.patchCases.mockResolvedValue({ + saved_objects: [{ ...mockCases[0] }], + }); + + await bulkUpdate( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + title: 'Updated title', + }, + ], + }, + clientArgs, + casesClientMock + ); + + expect(clientArgs.authorization.ensureAuthorized).toHaveBeenCalledWith({ + entities: [{ id: mockCases[0].id, owner: mockCases[0].attributes.owner }], + operation: [Operations.updateCase], + }); + }); + + it('checks authorization for both reopenCase and updateCase operations when reopening a case', async () => { + // Mock a closed case + const closedCase = { + ...mockCases[0], + attributes: { + ...mockCases[0].attributes, + status: CaseStatuses.closed, + }, + }; + clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] }); + + clientArgs.services.caseService.patchCases.mockResolvedValue({ + saved_objects: [{ ...closedCase }], + }); + + await bulkUpdate( + { + cases: [ + { + id: closedCase.id, + version: closedCase.version ?? '', + status: CaseStatuses.open, + }, + ], + }, + clientArgs, + casesClientMock + ); + + expect(clientArgs.authorization.ensureAuthorized).not.toThrow(); + }); + + it('throws when user is not authorized to update case', async () => { + const error = new Error('Unauthorized'); + clientArgs.authorization.ensureAuthorized.mockRejectedValue(error); + + await expect( + bulkUpdate( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + title: 'Updated title', + }, + ], + }, + clientArgs, + casesClientMock + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized"` + ); + }); + + it('throws when user is not authorized to reopen case', async () => { + const closedCase = { + ...mockCases[0], + attributes: { + ...mockCases[0].attributes, + status: CaseStatuses.closed, + }, + }; + clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: [closedCase] }); + + const error = new Error('Unauthorized to reopen case'); + clientArgs.authorization.ensureAuthorized.mockRejectedValueOnce(error); // Reject reopenCase + + await expect( + bulkUpdate( + { + cases: [ + { + id: closedCase.id, + version: closedCase.version ?? '', + status: CaseStatuses.open, + }, + ], + }, + clientArgs, + casesClientMock + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Unauthorized to reopen case"` + ); + }); + }); }); }); diff --git a/x-pack/plugins/cases/server/client/cases/bulk_update.ts b/x-pack/plugins/cases/server/client/cases/bulk_update.ts index b9984ac53b05e..9a90168b858de 100644 --- a/x-pack/plugins/cases/server/client/cases/bulk_update.ts +++ b/x-pack/plugins/cases/server/client/cases/bulk_update.ts @@ -272,9 +272,11 @@ function partitionPatchRequest( conflictedCases: CasePatchRequest[]; // This will be a deduped array of case IDs with their corresponding owner casesToAuthorize: OwnerEntity[]; + reopenedCases: CasePatchRequest[]; } { const nonExistingCases: CasePatchRequest[] = []; const conflictedCases: CasePatchRequest[] = []; + const reopenedCases: CasePatchRequest[] = []; const casesToAuthorize: Map = new Map(); for (const reqCase of patchReqCases) { @@ -286,6 +288,13 @@ function partitionPatchRequest( conflictedCases.push(reqCase); // let's try to authorize the conflicted case even though we'll fail after afterwards just in case casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); + } else if ( + reqCase.status != null && + foundCase.attributes.status !== reqCase.status && + foundCase.attributes.status === CaseStatuses.closed + ) { + // Track cases that are closed and a user is attempting to reopen + reopenedCases.push(reqCase); } else { casesToAuthorize.set(foundCase.id, { id: foundCase.id, owner: foundCase.attributes.owner }); } @@ -294,6 +303,7 @@ function partitionPatchRequest( return { nonExistingCases, conflictedCases, + reopenedCases, casesToAuthorize: Array.from(casesToAuthorize.values()), }; } @@ -344,14 +354,17 @@ export const bulkUpdate = async ( return acc; }, new Map()); - const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest( - casesMap, - query.cases - ); + const { nonExistingCases, conflictedCases, casesToAuthorize, reopenedCases } = + partitionPatchRequest(casesMap, query.cases); + + const operationsToAuthorize = + reopenedCases.length > 0 + ? [Operations.reopenCase, Operations.updateCase] + : [Operations.updateCase]; await authorization.ensureAuthorized({ entities: casesToAuthorize, - operation: Operations.updateCase, + operation: operationsToAuthorize, }); if (nonExistingCases.length > 0) { diff --git a/x-pack/plugins/cases/server/connectors/cases/index.test.ts b/x-pack/plugins/cases/server/connectors/cases/index.test.ts index 5c7b29ef4e704..7b6d244d165b3 100644 --- a/x-pack/plugins/cases/server/connectors/cases/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/index.test.ts @@ -36,6 +36,7 @@ describe('getCasesConnectorType', () => { 'cases:my-owner/updateComment', 'cases:my-owner/deleteComment', 'cases:my-owner/findConfigurations', + 'cases:my-owner/reopenCase', ]); }); @@ -356,6 +357,7 @@ describe('getCasesConnectorType', () => { 'cases:securitySolution/updateComment', 'cases:securitySolution/deleteComment', 'cases:securitySolution/findConfigurations', + 'cases:securitySolution/reopenCase', ]); }); @@ -376,6 +378,7 @@ describe('getCasesConnectorType', () => { 'cases:observability/updateComment', 'cases:observability/deleteComment', 'cases:observability/findConfigurations', + 'cases:observability/reopenCase', ]); }); @@ -396,6 +399,7 @@ describe('getCasesConnectorType', () => { 'cases:securitySolution/updateComment', 'cases:securitySolution/deleteComment', 'cases:securitySolution/findConfigurations', + 'cases:securitySolution/reopenCase', ]); }); }); diff --git a/x-pack/plugins/cases/server/connectors/cases/utils.test.ts b/x-pack/plugins/cases/server/connectors/cases/utils.test.ts index 976a7eadb5aec..55ffb5c7170bd 100644 --- a/x-pack/plugins/cases/server/connectors/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/connectors/cases/utils.test.ts @@ -507,6 +507,7 @@ describe('utils', () => { 'cases:my-owner/updateComment', 'cases:my-owner/deleteComment', 'cases:my-owner/findConfigurations', + 'cases:my-owner/reopenCase', ]); }); }); diff --git a/x-pack/plugins/cases/server/connectors/cases/utils.ts b/x-pack/plugins/cases/server/connectors/cases/utils.ts index a2513027c9cb3..b9cd2982553e3 100644 --- a/x-pack/plugins/cases/server/connectors/cases/utils.ts +++ b/x-pack/plugins/cases/server/connectors/cases/utils.ts @@ -109,7 +109,7 @@ export const buildCustomFieldsForRequest = ( export const constructRequiredKibanaPrivileges = (owner: string): string[] => { /** * Kibana features privileges are defined in - * x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/cases.ts + * x-pack/packages/security/authorization_core/src/privileges/feature_privilege_builder/cases.ts */ return [ `cases:${owner}/createCase`, @@ -120,5 +120,6 @@ export const constructRequiredKibanaPrivileges = (owner: string): string[] => { `cases:${owner}/updateComment`, `cases:${owner}/deleteComment`, `cases:${owner}/findConfigurations`, + `cases:${owner}/reopenCase`, ]; }; diff --git a/x-pack/plugins/cases/server/features/constants.ts b/x-pack/plugins/cases/server/features/constants.ts new file mode 100644 index 0000000000000..fb0a0f4554dee --- /dev/null +++ b/x-pack/plugins/cases/server/features/constants.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Unique sub privilege ids for cases. + * @description When upgrading (creating new versions), the sub-privileges + * do not need to be versioned as they are appended to the top level privilege id which is the only id + * that will need to be versioned + */ + +export const CASES_DELETE_SUB_PRIVILEGE_ID = 'cases_delete'; +export const CASES_SETTINGS_SUB_PRIVILEGE_ID = 'cases_settings'; +export const CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID = 'create_comment'; +export const CASES_REOPEN_SUB_PRIVILEGE_ID = 'case_reopen'; diff --git a/x-pack/plugins/cases/server/features/index.ts b/x-pack/plugins/cases/server/features/index.ts new file mode 100644 index 0000000000000..afa3dfab9b311 --- /dev/null +++ b/x-pack/plugins/cases/server/features/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { getV1 } from './v1'; +import { getV2 } from './v2'; + +export const getCasesKibanaFeatures = (): { + v1: KibanaFeatureConfig; + v2: KibanaFeatureConfig; +} => ({ v1: getV1(), v2: getV2() }); diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features/v1.ts similarity index 67% rename from x-pack/plugins/cases/server/features.ts rename to x-pack/plugins/cases/server/features/v1.ts index f8f162b2ae3dc..25a43434f3723 100644 --- a/x-pack/plugins/cases/server/features.ts +++ b/x-pack/plugins/cases/server/features/v1.ts @@ -12,8 +12,9 @@ import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/s import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; -import { APP_ID, FEATURE_ID } from '../common/constants'; -import { createUICapabilities, getApiTags } from '../common'; +import { APP_ID, FEATURE_ID, FEATURE_ID_V2 } from '../../common/constants'; +import { createUICapabilities, getApiTags } from '../../common'; +import { CASES_DELETE_SUB_PRIVILEGE_ID, CASES_SETTINGS_SUB_PRIVILEGE_ID } from './constants'; /** * The order of appearance in the feature privilege page @@ -23,14 +24,24 @@ import { createUICapabilities, getApiTags } from '../common'; const FEATURE_ORDER = 3100; -export const getCasesKibanaFeature = (): KibanaFeatureConfig => { +export const getV1 = (): KibanaFeatureConfig => { const capabilities = createUICapabilities(); const apiTags = getApiTags(APP_ID); return { + deprecated: { + notice: i18n.translate('xpack.cases.features.casesFeature.deprecationMessage', { + defaultMessage: + 'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.', + values: { + currentId: FEATURE_ID, + casesFeatureIdV2: FEATURE_ID_V2, + }, + }), + }, id: FEATURE_ID, - name: i18n.translate('xpack.cases.features.casesFeatureName', { - defaultMessage: 'Cases', + name: i18n.translate('xpack.cases.features.casesFeatureNameDeprecated', { + defaultMessage: 'Cases (Deprecated)', }), category: DEFAULT_APP_CATEGORIES.management, scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], @@ -42,12 +53,14 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { cases: [APP_ID], privileges: { all: { - api: apiTags.all, + api: [...apiTags.all, ...apiTags.createComment], cases: { create: [APP_ID], read: [APP_ID], update: [APP_ID], push: [APP_ID], + createComment: [APP_ID], + reopenCase: [APP_ID], }, management: { insightsAndAlerting: [APP_ID], @@ -57,6 +70,15 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { read: [...filesSavedObjectTypes], }, ui: capabilities.all, + replacedBy: { + default: [{ feature: FEATURE_ID_V2, privileges: ['all'] }], + minimal: [ + { + feature: FEATURE_ID_V2, + privileges: ['minimal_all', 'create_comment', 'case_reopen'], + }, + ], + }, }, read: { api: apiTags.read, @@ -71,6 +93,10 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { read: [...filesSavedObjectTypes], }, ui: capabilities.read, + replacedBy: { + default: [{ feature: FEATURE_ID_V2, privileges: ['read'] }], + minimal: [{ feature: FEATURE_ID_V2, privileges: ['minimal_read'] }], + }, }, }, subFeatures: [ @@ -84,7 +110,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { privileges: [ { api: apiTags.delete, - id: 'cases_delete', + id: CASES_DELETE_SUB_PRIVILEGE_ID, name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', { defaultMessage: 'Delete cases and comments', }), @@ -97,6 +123,9 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { delete: [APP_ID], }, ui: capabilities.delete, + replacedBy: [ + { feature: FEATURE_ID_V2, privileges: [CASES_DELETE_SUB_PRIVILEGE_ID] }, + ], }, ], }, @@ -111,7 +140,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { groupType: 'independent', privileges: [ { - id: 'cases_settings', + id: CASES_SETTINGS_SUB_PRIVILEGE_ID, name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureDetails', { defaultMessage: 'Edit case settings', }), @@ -124,6 +153,9 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { settings: [APP_ID], }, ui: capabilities.settings, + replacedBy: [ + { feature: FEATURE_ID_V2, privileges: [CASES_SETTINGS_SUB_PRIVILEGE_ID] }, + ], }, ], }, diff --git a/x-pack/plugins/cases/server/features/v2.ts b/x-pack/plugins/cases/server/features/v2.ts new file mode 100644 index 0000000000000..fca97303f02ab --- /dev/null +++ b/x-pack/plugins/cases/server/features/v2.ts @@ -0,0 +1,195 @@ +/* + * 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 type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; + +import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { APP_ID, FEATURE_ID_V2 } from '../../common/constants'; +import { createUICapabilities, getApiTags } from '../../common'; +import { + CASES_DELETE_SUB_PRIVILEGE_ID, + CASES_SETTINGS_SUB_PRIVILEGE_ID, + CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID, + CASES_REOPEN_SUB_PRIVILEGE_ID, +} from './constants'; + +/** + * The order of appearance in the feature privilege page + * under the management section. Cases should be under + * the Actions and Connectors feature + */ + +const FEATURE_ORDER = 3100; + +export const getV2 = (): KibanaFeatureConfig => { + const capabilities = createUICapabilities(); + const apiTags = getApiTags(APP_ID); + + return { + id: FEATURE_ID_V2, + name: i18n.translate('xpack.cases.features.casesFeatureName', { + defaultMessage: 'Cases', + }), + category: DEFAULT_APP_CATEGORIES.management, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [], + order: FEATURE_ORDER, + management: { + insightsAndAlerting: [APP_ID], + }, + cases: [APP_ID], + privileges: { + all: { + api: apiTags.all, + cases: { + create: [APP_ID], + read: [APP_ID], + update: [APP_ID], + push: [APP_ID], + }, + management: { + insightsAndAlerting: [APP_ID], + }, + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + ui: capabilities.all, + }, + read: { + api: apiTags.read, + cases: { + read: [APP_ID], + }, + management: { + insightsAndAlerting: [APP_ID], + }, + savedObject: { + all: [], + read: [...filesSavedObjectTypes], + }, + ui: capabilities.read, + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.cases.features.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.delete, + id: CASES_DELETE_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', { + defaultMessage: 'Delete cases and comments', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + delete: [APP_ID], + }, + ui: capabilities.delete, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureName', { + defaultMessage: 'Case settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: CASES_SETTINGS_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.casesSettingsSubFeatureDetails', { + defaultMessage: 'Edit case settings', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [APP_ID], + }, + ui: capabilities.settings, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.addCommentsSubFeatureName', { + defaultMessage: 'Create comments & attachments', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: apiTags.createComment, + id: CASES_CREATE_COMMENT_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.addCommentsSubFeatureDetails', { + defaultMessage: 'Add comments to cases', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + createComment: [APP_ID], + }, + ui: capabilities.createComment, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.cases.features.reopenCaseSubFeatureName', { + defaultMessage: 'Re-open', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: CASES_REOPEN_SUB_PRIVILEGE_ID, + name: i18n.translate('xpack.cases.features.reopenCaseSubFeatureDetails', { + defaultMessage: 'Re-open closed cases', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [APP_ID], + }, + ui: capabilities.reopenCase, + }, + ], + }, + ], + }, + ], + }; +}; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index b40089ff75050..dfd4c013f0d58 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -30,7 +30,7 @@ import type { CasesServerStartDependencies, } from './types'; import { CasesClientFactory } from './client/factory'; -import { getCasesKibanaFeature } from './features'; +import { getCasesKibanaFeatures } from './features'; import { registerRoutes } from './routes/api/register_routes'; import { getExternalRoutes } from './routes/api/get_external_routes'; import { createCasesTelemetry, scheduleCasesTelemetryTask } from './telemetry'; @@ -92,7 +92,11 @@ export class CasePlugin this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory; if (this.caseConfig.stack.enabled) { - plugins.features.registerKibanaFeature(getCasesKibanaFeature()); + // V1 is deprecated, but has to be maintained for the time being + // https://github.com/elastic/kibana/pull/186800#issue-2369812818 + const casesFeatures = getCasesKibanaFeatures(); + plugins.features.registerKibanaFeature(casesFeatures.v1); + plugins.features.registerKibanaFeature(casesFeatures.v2); } registerSavedObjects({ diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 188fade8dd2cb..1939d0b5e4e49 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -188,6 +188,7 @@ export interface FeatureKibanaPrivileges { read?: readonly string[]; /** * List of case owners which users should have update access to when granted this privilege. + * This privilege does NOT provide access to re-opening a case. Please see `reopenCase` for said functionality. * @example * ```ts * { @@ -216,6 +217,26 @@ export interface FeatureKibanaPrivileges { * ``` */ settings?: readonly string[]; + /** + * List of case owners whose users should have createComment access when granted this privilege. + * @example + * ```ts + * { + * createComment: ['securitySolution'] + * } + * ``` + */ + createComment?: readonly string[]; + /** + * List of case owners whose users should have reopenCase access when granted this privilege. + * @example + * ```ts + * { + * reopenCase: ['securitySolution'] + * } + * ``` + */ + reopenCase?: readonly string[]; }; /** diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index c91244e2f1d9d..b8df9e9c2117b 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -557,9 +557,11 @@ Array [ "cases": Object { "all": Array [], "create": Array [], + "createComment": Array [], "delete": Array [], "push": Array [], "read": Array [], + "reopenCase": Array [], "settings": Array [], "update": Array [], }, @@ -716,9 +718,11 @@ Array [ "cases": Object { "all": Array [], "create": Array [], + "createComment": Array [], "delete": Array [], "push": Array [], "read": Array [], + "reopenCase": Array [], "settings": Array [], "update": Array [], }, @@ -1050,9 +1054,11 @@ Array [ "cases": Object { "all": Array [], "create": Array [], + "createComment": Array [], "delete": Array [], "push": Array [], "read": Array [], + "reopenCase": Array [], "settings": Array [], "update": Array [], }, @@ -1190,9 +1196,11 @@ Array [ "cases": Object { "all": Array [], "create": Array [], + "createComment": Array [], "delete": Array [], "push": Array [], "read": Array [], + "reopenCase": Array [], "settings": Array [], "update": Array [], }, @@ -1349,9 +1357,11 @@ Array [ "cases": Object { "all": Array [], "create": Array [], + "createComment": Array [], "delete": Array [], "push": Array [], "read": Array [], + "reopenCase": Array [], "settings": Array [], "update": Array [], }, @@ -1683,9 +1693,11 @@ Array [ "cases": Object { "all": Array [], "create": Array [], + "createComment": Array [], "delete": Array [], "push": Array [], "read": Array [], + "reopenCase": Array [], "settings": Array [], "update": Array [], }, diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts index 58a39c85bf9e9..c7d501bb17cf8 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -78,6 +78,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -148,6 +150,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -217,6 +221,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -288,6 +294,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -329,6 +337,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -391,6 +401,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-sub-type'], }, @@ -438,6 +450,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -506,6 +520,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -568,6 +584,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-sub-type'], }, @@ -615,6 +633,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -683,6 +703,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -746,6 +768,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-sub-type'], }, ui: ['ui-sub-type'], }, @@ -796,6 +820,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type', 'cases-delete-sub-type'], push: ['cases-push-type', 'cases-push-sub-type'], settings: ['cases-settings-type', 'cases-settings-sub-type'], + createComment: ['cases-create-comment-type', 'cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-type', 'cases-reopen-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -832,6 +858,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -875,6 +903,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -980,6 +1010,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -1015,6 +1047,8 @@ describe('featurePrivilegeIterator', () => { delete: [], push: [], settings: [], + createComment: [], + reopenCase: [], }, ui: ['ui-action'], }, @@ -1056,6 +1090,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -1119,6 +1155,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1169,6 +1207,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type', 'cases-delete-sub-type'], push: ['cases-push-type', 'cases-push-sub-type'], settings: ['cases-settings-type', 'cases-settings-sub-type'], + createComment: ['cases-create-comment-type', 'cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-type', 'cases-reopen-sub-type'], }, ui: ['ui-action', 'ui-sub-type'], }, @@ -1362,6 +1402,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1412,6 +1454,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1448,6 +1492,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-sub-type'], push: ['cases-push-sub-type'], settings: ['cases-settings-sub-type'], + createComment: ['cases-create-comment-sub-type'], + reopenCase: ['cases-reopen-sub-type'], }, ui: ['ui-sub-type'], }, @@ -1489,6 +1535,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -1580,6 +1628,8 @@ describe('featurePrivilegeIterator', () => { delete: ['cases-delete-type'], push: ['cases-push-type'], settings: ['cases-settings-type'], + createComment: ['cases-create-comment-type'], + reopenCase: ['cases-reopen-type'], }, ui: ['ui-action'], }, @@ -1615,6 +1665,8 @@ describe('featurePrivilegeIterator', () => { delete: [], push: [], settings: [], + createComment: [], + reopenCase: [], }, ui: ['ui-action'], }, diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts index 0d1dc8e3ab788..a9d7336ea0a22 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts @@ -151,6 +151,14 @@ function mergeWithSubFeatures( mergedConfig.cases?.settings ?? [], subFeaturePrivilege.cases?.settings ?? [] ), + createComment: mergeArrays( + mergedConfig.cases?.createComment ?? [], + subFeaturePrivilege.cases?.createComment ?? [] + ), + reopenCase: mergeArrays( + mergedConfig.cases?.reopenCase ?? [], + subFeaturePrivilege.cases?.reopenCase ?? [] + ), }; } return mergedConfig; diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 581fdc1037e2a..ce444c41e477d 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -83,6 +83,8 @@ const casesSchemaObject = schema.maybe( delete: schema.maybe(casesSchema), push: schema.maybe(casesSchema), settings: schema.maybe(casesSchema), + createComment: schema.maybe(casesSchema), + reopenCase: schema.maybe(casesSchema), }) ); diff --git a/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx b/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx index 25ffef0456e42..9154a2c77bf4a 100644 --- a/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx +++ b/x-pack/plugins/ml/public/alerting/anomaly_detection_alerts_table/register_alerts_table_configuration.tsx @@ -24,7 +24,7 @@ import { ALERT_STATUS, } from '@kbn/rule-data-utils'; import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; -import { APP_ID as CASE_APP_ID, FEATURE_ID as CASE_GENERAL_ID } from '@kbn/cases-plugin/common'; +import { APP_ID as CASE_APP_ID, FEATURE_ID_V2 as CASE_GENERAL_ID } from '@kbn/cases-plugin/common'; import { MANAGEMENT_APP_ID } from '@kbn/deeplinks-management/constants'; import { getAlertFlyout } from './use_alerts_flyout'; import { diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx index 1d42716bf405d..011fb93553ac4 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/header/add_to_case_action.test.tsx @@ -120,6 +120,8 @@ describe('AddToCaseAction', function () { push: false, connectors: false, settings: false, + createComment: false, + reopenCase: false, }, }) ); diff --git a/x-pack/plugins/observability_solution/observability/common/index.ts b/x-pack/plugins/observability_solution/observability/common/index.ts index 3dc44c5ac02aa..6ffad0b2ed436 100644 --- a/x-pack/plugins/observability_solution/observability/common/index.ts +++ b/x-pack/plugins/observability_solution/observability/common/index.ts @@ -63,7 +63,9 @@ export { getProbabilityFromProgressiveLoadingQuality, } from './progressive_loading'; +/** @deprecated deprecated in 8.17. Please use casesFeatureIdV2 instead */ export const casesFeatureId = 'observabilityCases'; +export const casesFeatureIdV2 = 'observabilityCasesV2'; export const sloFeatureId = 'slo'; // The ID of the observability app. Should more appropriately be called // 'observability' but it's used in telemetry by applicationUsage so we don't diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx index 071b75ab89632..cf0c4aa3c8b60 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/components/alert_actions.tsx @@ -159,7 +159,7 @@ export function AlertActions({ ); const actionsMenuItems = [ - ...(userCasesPermissions.create && userCasesPermissions.read + ...(userCasesPermissions.createComment && userCasesPermissions.read ? [ ({ + deprecated: { + // TODO: Add docLinks to link to documentation about the deprecation + notice: i18n.translate( + 'xpack.observability.featureRegistry.linkObservabilityTitle.deprecationMessage', + { + defaultMessage: + 'The {currentId} permissions are deprecated, please see {casesFeatureIdV2}.', + values: { + currentId: casesFeatureId, + casesFeatureIdV2, + }, + } + ), + }, + id: casesFeatureId, + name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitleDeprecated', { + defaultMessage: 'Cases (Deprecated)', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: [observabilityFeatureId], + privileges: { + all: { + api: [...casesApiTags.all, ...casesApiTags.createComment], + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + create: [observabilityFeatureId], + read: [observabilityFeatureId], + update: [observabilityFeatureId], + push: [observabilityFeatureId], + createComment: [observabilityFeatureId], + reopenCase: [observabilityFeatureId], + }, + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + ui: casesCapabilities.all, + replacedBy: { + default: [{ feature: casesFeatureIdV2, privileges: ['all'] }], + minimal: [ + { + feature: casesFeatureIdV2, + privileges: ['minimal_all', 'create_comment', 'case_reopen'], + }, + ], + }, + }, + read: { + api: casesApiTags.read, + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + read: [observabilityFeatureId], + }, + savedObject: { + all: [], + read: [...filesSavedObjectTypes], + }, + ui: casesCapabilities.read, + replacedBy: { + default: [{ feature: casesFeatureIdV2, privileges: ['read'] }], + minimal: [{ feature: casesFeatureIdV2, privileges: ['minimal_read'] }], + }, + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: casesApiTags.delete, + id: 'cases_delete', + name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureDetails', { + defaultMessage: 'Delete cases and comments', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + delete: [observabilityFeatureId], + }, + ui: casesCapabilities.delete, + replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_delete'] }], + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.casesSettingsSubFeatureName', { + defaultMessage: 'Case settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'xpack.observability.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit case settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [observabilityFeatureId], + }, + ui: casesCapabilities.settings, + replacedBy: [{ feature: casesFeatureIdV2, privileges: ['cases_settings'] }], + }, + ], + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/observability_solution/observability/server/features/cases_v2.ts b/x-pack/plugins/observability_solution/observability/server/features/cases_v2.ts new file mode 100644 index 0000000000000..52b501a62bb2e --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/server/features/cases_v2.ts @@ -0,0 +1,181 @@ +/* + * 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; +import { i18n } from '@kbn/i18n'; +import { KibanaFeatureConfig, KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { CasesUiCapabilities, CasesApiTags } from '@kbn/cases-plugin/common'; +import { casesFeatureIdV2, casesFeatureId, observabilityFeatureId } from '../../common'; + +export const getCasesFeatureV2 = ( + casesCapabilities: CasesUiCapabilities, + casesApiTags: CasesApiTags +): KibanaFeatureConfig => ({ + id: casesFeatureIdV2, + name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { + defaultMessage: 'Cases', + }), + order: 1100, + category: DEFAULT_APP_CATEGORIES.observability, + scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: [observabilityFeatureId], + privileges: { + all: { + api: casesApiTags.all, + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + create: [observabilityFeatureId], + read: [observabilityFeatureId], + update: [observabilityFeatureId], + push: [observabilityFeatureId], + }, + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + ui: casesCapabilities.all, + }, + read: { + api: casesApiTags.read, + app: [casesFeatureId, 'kibana'], + catalogue: [observabilityFeatureId], + cases: { + read: [observabilityFeatureId], + }, + savedObject: { + all: [], + read: [...filesSavedObjectTypes], + }, + ui: casesCapabilities.read, + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', { + defaultMessage: 'Delete', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: casesApiTags.delete, + id: 'cases_delete', + name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureDetails', { + defaultMessage: 'Delete cases and comments', + }), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + delete: [observabilityFeatureId], + }, + ui: casesCapabilities.delete, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.casesSettingsSubFeatureName', { + defaultMessage: 'Case settings', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cases_settings', + name: i18n.translate( + 'xpack.observability.featureRegistry.casesSettingsSubFeatureDetails', + { + defaultMessage: 'Edit case settings', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + settings: [observabilityFeatureId], + }, + ui: casesCapabilities.settings, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.addCommentsSubFeatureName', { + defaultMessage: 'Create comments & attachments', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + api: casesApiTags.createComment, + id: 'create_comment', + name: i18n.translate( + 'xpack.observability.featureRegistry.addCommentsSubFeatureDetails', + { + defaultMessage: 'Add comments to cases', + } + ), + includeIn: 'all', + savedObject: { + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], + }, + cases: { + createComment: [observabilityFeatureId], + }, + ui: casesCapabilities.createComment, + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.observability.featureRegistry.reopenCaseSubFeatureName', { + defaultMessage: 'Re-open', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'case_reopen', + name: i18n.translate( + 'xpack.observability.featureRegistry.reopenCaseSubFeatureDetails', + { + defaultMessage: 'Re-open closed cases', + } + ), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + cases: { + reopenCase: [observabilityFeatureId], + }, + ui: casesCapabilities.reopenCase, + }, + ], + }, + ], + }, + ], +}); diff --git a/x-pack/plugins/observability_solution/observability/server/plugin.ts b/x-pack/plugins/observability_solution/observability/server/plugin.ts index 7f9a37a5a26c4..b98fe316c712e 100644 --- a/x-pack/plugins/observability_solution/observability/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/server/plugin.ts @@ -21,7 +21,6 @@ import { } from '@kbn/core/server'; import { LogsExplorerLocatorParams, LOGS_EXPLORER_LOCATOR_ID } from '@kbn/deeplinks-observability'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; -import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server'; import { i18n } from '@kbn/i18n'; import { @@ -41,7 +40,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { ObservabilityConfig } from '.'; -import { casesFeatureId, observabilityFeatureId } from '../common'; +import { observabilityFeatureId } from '../common'; import { kubernetesGuideConfig, kubernetesGuideId, @@ -58,6 +57,8 @@ import { registerRoutes } from './routes/register_routes'; import { threshold } from './saved_objects/threshold'; import { AlertDetailsContextualInsightsService } from './services'; import { uiSettings } from './ui_settings'; +import { getCasesFeature } from './features/cases_v1'; +import { getCasesFeatureV2 } from './features/cases_v2'; export type ObservabilityPluginSetup = ReturnType; @@ -110,112 +111,8 @@ export class ObservabilityPlugin implements Plugin { const alertDetailsContextualInsightsService = new AlertDetailsContextualInsightsService(); - plugins.features.registerKibanaFeature({ - id: casesFeatureId, - name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', { - defaultMessage: 'Cases', - }), - order: 1100, - category: DEFAULT_APP_CATEGORIES.observability, - scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], - app: [casesFeatureId, 'kibana'], - catalogue: [observabilityFeatureId], - cases: [observabilityFeatureId], - privileges: { - all: { - api: casesApiTags.all, - app: [casesFeatureId, 'kibana'], - catalogue: [observabilityFeatureId], - cases: { - create: [observabilityFeatureId], - read: [observabilityFeatureId], - update: [observabilityFeatureId], - push: [observabilityFeatureId], - }, - savedObject: { - all: [...filesSavedObjectTypes], - read: [...filesSavedObjectTypes], - }, - ui: casesCapabilities.all, - }, - read: { - api: casesApiTags.read, - app: [casesFeatureId, 'kibana'], - catalogue: [observabilityFeatureId], - cases: { - read: [observabilityFeatureId], - }, - savedObject: { - all: [], - read: [...filesSavedObjectTypes], - }, - ui: casesCapabilities.read, - }, - }, - subFeatures: [ - { - name: i18n.translate('xpack.observability.featureRegistry.deleteSubFeatureName', { - defaultMessage: 'Delete', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - api: casesApiTags.delete, - id: 'cases_delete', - name: i18n.translate( - 'xpack.observability.featureRegistry.deleteSubFeatureDetails', - { - defaultMessage: 'Delete cases and comments', - } - ), - includeIn: 'all', - savedObject: { - all: [...filesSavedObjectTypes], - read: [...filesSavedObjectTypes], - }, - cases: { - delete: [observabilityFeatureId], - }, - ui: casesCapabilities.delete, - }, - ], - }, - ], - }, - { - name: i18n.translate('xpack.observability.featureRegistry.casesSettingsSubFeatureName', { - defaultMessage: 'Case settings', - }), - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'cases_settings', - name: i18n.translate( - 'xpack.observability.featureRegistry.casesSettingsSubFeatureDetails', - { - defaultMessage: 'Edit case settings', - } - ), - includeIn: 'all', - savedObject: { - all: [...filesSavedObjectTypes], - read: [...filesSavedObjectTypes], - }, - cases: { - settings: [observabilityFeatureId], - }, - ui: casesCapabilities.settings, - }, - ], - }, - ], - }, - ], - }); + plugins.features.registerKibanaFeature(getCasesFeature(casesCapabilities, casesApiTags)); + plugins.features.registerKibanaFeature(getCasesFeatureV2(casesCapabilities, casesApiTags)); let annotationsApiPromise: Promise | undefined; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index b4b7731d166b7..f483bcc5dc269 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -8,7 +8,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; export const observabilityFeatureId = 'observability'; export const observabilityAppId = 'observability-overview'; -export const casesFeatureId = 'observabilityCases'; +export const casesFeatureId = 'observabilityCasesV2'; export const sloFeatureId = 'slo'; // SLO alerts table in slo detail page diff --git a/x-pack/plugins/observability_solution/observability_shared/public/utils/cases_permissions.ts b/x-pack/plugins/observability_solution/observability_shared/public/utils/cases_permissions.ts index 0ceea46ad0d38..0b3699e49b40c 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/utils/cases_permissions.ts +++ b/x-pack/plugins/observability_solution/observability_shared/public/utils/cases_permissions.ts @@ -14,6 +14,8 @@ export const noCasesPermissions = () => ({ push: false, connectors: false, settings: false, + createComment: false, + reopenCase: false, }); export const allCasesPermissions = () => ({ @@ -25,4 +27,6 @@ export const allCasesPermissions = () => ({ push: true, connectors: true, settings: true, + createComment: true, + reopenCase: true, }); diff --git a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts index 6e3f6751d11dc..49cb34ccdc09e 100644 --- a/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts +++ b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.test.ts @@ -94,7 +94,7 @@ const roles = [ applications: [ { application: 'kibana-.kibana', - privileges: ['feature_securitySolutionCases.a;;'], + privileges: ['feature_securitySolutionCasesV2.a;;'], resources: ['*'], }, ], @@ -184,7 +184,7 @@ const roles = [ applications: [ { application: 'kibana-.kibana', - privileges: ['feature_securitySolutionCases.a;;'], + privileges: ['feature_securitySolutionCasesV2.a;;'], resources: ['space:default'], }, ], diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 137afe7ba9112..b366a0e555357 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -21,7 +21,7 @@ export const APP_ID = 'securitySolution' as const; export const APP_UI_ID = 'securitySolutionUI' as const; export const ASSISTANT_FEATURE_ID = 'securitySolutionAssistant' as const; export const ATTACK_DISCOVERY_FEATURE_ID = 'securitySolutionAttackDiscovery' as const; -export const CASES_FEATURE_ID = 'securitySolutionCases' as const; +export const CASES_FEATURE_ID = 'securitySolutionCasesV2' as const; export const SERVER_APP_ID = 'siem' as const; export const APP_NAME = 'Security' as const; export const APP_ICON = 'securityAnalyticsApp' as const; diff --git a/x-pack/plugins/security_solution/common/test/ess_roles.json b/x-pack/plugins/security_solution/common/test/ess_roles.json index 94bd3d57a6d7b..361d5d4321756 100644 --- a/x-pack/plugins/security_solution/common/test/ess_roles.json +++ b/x-pack/plugins/security_solution/common/test/ess_roles.json @@ -30,7 +30,7 @@ "siem": ["read", "read_alerts"], "securitySolutionAssistant": ["none"], "securitySolutionAttackDiscovery": ["none"], - "securitySolutionCases": ["read"], + "securitySolutionCasesV2": ["read"], "actions": ["read"], "builtInAlerts": ["read"] }, @@ -79,7 +79,7 @@ "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "actions": ["read"], "builtInAlerts": ["all"] }, @@ -128,7 +128,7 @@ "siem": ["all", "read_alerts", "crud_alerts"], "securitySolutionAssistant": ["all"], "securitySolutionAttackDiscovery": ["all"], - "securitySolutionCases": ["all"], + "securitySolutionCasesV2": ["all"], "builtInAlerts": ["all"] }, "spaces": ["*"], diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actions/take_action/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actions/take_action/index.tsx index 9701114915507..af2150b4010d9 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actions/take_action/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/results/attack_discovery_panel/actions/take_action/index.tsx @@ -33,8 +33,8 @@ const TakeActionComponent: React.FC = ({ attackDiscovery, replacements }) const { cases } = useKibana().services; const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const canUserCreateAndReadCases = useCallback( - () => userCasesPermissions.create && userCasesPermissions.read, - [userCasesPermissions.create, userCasesPermissions.read] + () => userCasesPermissions.createComment && userCasesPermissions.read, + [userCasesPermissions.createComment, userCasesPermissions.read] ); const { disabled: addToCaseDisabled, onAddToNewCase } = useAddToNewCase({ canUserCreateAndReadCases, diff --git a/x-pack/plugins/security_solution/public/cases_test_utils.ts b/x-pack/plugins/security_solution/public/cases_test_utils.ts index dc70dcab33eaa..f3c356507bcfe 100644 --- a/x-pack/plugins/security_solution/public/cases_test_utils.ts +++ b/x-pack/plugins/security_solution/public/cases_test_utils.ts @@ -15,6 +15,8 @@ export const noCasesCapabilities = (): CasesCapabilities => ({ push_cases: false, cases_connectors: false, cases_settings: false, + case_reopen: false, + create_comment: false, }); export const readCasesCapabilities = (): CasesCapabilities => ({ @@ -25,6 +27,8 @@ export const readCasesCapabilities = (): CasesCapabilities => ({ push_cases: false, cases_connectors: true, cases_settings: false, + case_reopen: false, + create_comment: false, }); export const allCasesCapabilities = (): CasesCapabilities => ({ @@ -35,6 +39,8 @@ export const allCasesCapabilities = (): CasesCapabilities => ({ push_cases: true, cases_connectors: true, cases_settings: true, + case_reopen: true, + create_comment: true, }); export const noCasesPermissions = (): CasesPermissions => ({ @@ -46,6 +52,8 @@ export const noCasesPermissions = (): CasesPermissions => ({ push: false, connectors: false, settings: false, + reopenCase: false, + createComment: false, }); export const readCasesPermissions = (): CasesPermissions => ({ @@ -57,6 +65,8 @@ export const readCasesPermissions = (): CasesPermissions => ({ push: false, connectors: true, settings: false, + reopenCase: false, + createComment: false, }); export const writeCasesPermissions = (): CasesPermissions => ({ @@ -68,6 +78,8 @@ export const writeCasesPermissions = (): CasesPermissions => ({ push: true, connectors: true, settings: true, + reopenCase: true, + createComment: true, }); export const allCasesPermissions = (): CasesPermissions => ({ @@ -79,4 +91,6 @@ export const allCasesPermissions = (): CasesPermissions => ({ push: true, connectors: true, settings: true, + reopenCase: true, + createComment: true, }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx index aa11ced2603a9..c07bbd651316a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_existing_case.tsx @@ -59,7 +59,7 @@ export const useAddToExistingCase = ({ disabled: lensAttributes == null || timeRange == null || - !userCasesPermissions.create || + !userCasesPermissions.createComment || !userCasesPermissions.read, }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx index c2ac628000fa7..7803e27b2453f 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_add_to_new_case.tsx @@ -60,7 +60,7 @@ export const useAddToNewCase = ({ disabled: lensAttributes == null || timeRange == null || - !userCasesPermissions.create || + !userCasesPermissions.createComment || !userCasesPermissions.read, }; }; diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.tsx b/x-pack/plugins/security_solution/public/common/links/links.test.tsx index c0f8c8cc48da4..c5f05afde9c62 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.tsx +++ b/x-pack/plugins/security_solution/public/common/links/links.test.tsx @@ -432,9 +432,9 @@ describe('Security links', () => { describe('hasCapabilities', () => { const siemShow = 'siem.show'; - const createCases = 'securitySolutionCases.create_cases'; - const readCases = 'securitySolutionCases.read_cases'; - const pushCases = 'securitySolutionCases.push_cases'; + const createCases = 'securitySolutionCasesV2.create_cases'; + const readCases = 'securitySolutionCasesV2.read_cases'; + const pushCases = 'securitySolutionCasesV2.push_cases'; it('returns false when capabilities is an empty array', () => { expect(hasCapabilities(createCapabilities(), [])).toBeFalsy(); @@ -461,7 +461,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: true }, - securitySolutionCases: { create_cases: false }, + securitySolutionCasesV2: { create_cases: false }, }), [siemShow, createCases] ) @@ -473,7 +473,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: false }, - securitySolutionCases: { create_cases: true }, + securitySolutionCasesV2: { create_cases: true }, }), [siemShow, createCases] ) @@ -485,7 +485,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: true }, - securitySolutionCases: { create_cases: false }, + securitySolutionCasesV2: { create_cases: false }, }), [readCases, createCases] ) @@ -497,7 +497,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: true }, - securitySolutionCases: { read_cases: true, create_cases: true }, + securitySolutionCasesV2: { read_cases: true, create_cases: true }, }), [[readCases, createCases]] ) @@ -509,7 +509,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: false }, - securitySolutionCases: { read_cases: false, create_cases: true }, + securitySolutionCasesV2: { read_cases: false, create_cases: true }, }), [siemShow, [readCases, createCases]] ) @@ -521,7 +521,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: true }, - securitySolutionCases: { read_cases: false, create_cases: true }, + securitySolutionCasesV2: { read_cases: false, create_cases: true }, }), [siemShow, [readCases, createCases]] ) @@ -533,7 +533,7 @@ describe('Security links', () => { hasCapabilities( createCapabilities({ siem: { show: true }, - securitySolutionCases: { read_cases: false, create_cases: true, push_cases: false }, + securitySolutionCasesV2: { read_cases: false, create_cases: true, push_cases: false }, }), [ [siemShow, pushCases], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index bdef9cd84c8f6..fa14fc317a78a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -88,6 +88,8 @@ jest.mock('../../../../common/lib/kibana', () => { update: true, delete: true, push: true, + createComment: true, + reopenCase: true, }), getRuleIdFromEvent: jest.fn(), }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx index 60a19f005c53e..8ddcd34f092f0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx @@ -142,7 +142,7 @@ export const useAddToCaseActions = ({ const addToCaseActionItems: AlertTableContextMenuItem[] = useMemo(() => { if ( (isActiveTimelines || isInDetections) && - userCasesPermissions.create && + userCasesPermissions.createComment && userCasesPermissions.read && isAlert ) { @@ -169,14 +169,14 @@ export const useAddToCaseActions = ({ } return []; }, [ + isActiveTimelines, + isInDetections, + userCasesPermissions.createComment, + userCasesPermissions.read, + isAlert, ariaLabel, handleAddToExistingCaseClick, handleAddToNewCaseClick, - userCasesPermissions.create, - userCasesPermissions.read, - isInDetections, - isActiveTimelines, - isAlert, ]); return { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts index 64fd3279d18cb..b5c524255509f 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/common.ts @@ -15,7 +15,7 @@ export const API_AUTH = Object.freeze({ export const COMMON_API_HEADERS = Object.freeze({ 'kbn-xsrf': 'cypress', 'x-elastic-internal-origin': 'security-solution', - 'Elastic-Api-Version': '2023-10-31', + 'elastic-api-version': '2023-10-31', }); export const waitForPageToBeLoaded = () => { diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx index e785e58435432..fce22635f3f64 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx @@ -95,8 +95,8 @@ const DataQualityComponent: React.FC = () => { const userCasesPermissions = cases.helpers.canUseCases([APP_ID]); const canUserCreateAndReadCases = useCallback( - () => userCasesPermissions.create && userCasesPermissions.read, - [userCasesPermissions.create, userCasesPermissions.read] + () => userCasesPermissions.createComment && userCasesPermissions.read, + [userCasesPermissions.createComment, userCasesPermissions.read] ); const createCaseFlyout = cases.hooks.useCasesAddToNewCaseFlyout({ diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b20e645d71c2c..b74d0cffdc88d 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -346,7 +346,7 @@ export class Plugin implements IPlugin ({ status: AppStatus.inaccessible, visibleIn: [], diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx index 228c6bc70584c..1816fb47a102a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.test.tsx @@ -87,7 +87,7 @@ describe('TimelineModalHeader', () => { cases: { helpers: { canUseCases: jest.fn().mockReturnValue({ - create: true, + createComment: true, read: true, }), }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx index e30e0c2cf2a10..350ff1ca65cd6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/modal/header/index.tsx @@ -161,7 +161,7 @@ export const TimelineModalHeader = React.memo( isDisabled={isInspectDisabled} /> - {userCasesPermissions.create && userCasesPermissions.read ? ( + {userCasesPermissions.createComment && userCasesPermissions.read ? ( <> diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts index a1f3585ffcdc7..85cadf5aa65d4 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/endpoint_operations_analyst.ts @@ -55,7 +55,7 @@ export const getEndpointOperationsAnalyst: () => Omit = () => { fleet: ['all'], fleetv2: ['all'], osquery: ['all'], - securitySolutionCases: ['all'], + securitySolutionCasesV2: ['all'], builtinAlerts: ['all'], siem: [ 'all', diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts index 4ed5f91df77dd..d57ca059de994 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/roles_users/without_response_actions_role.ts @@ -37,7 +37,7 @@ export const getNoResponseActionsRole: () => Omit = () => ({ advancedSettings: ['all'], dev_tools: ['all'], fleet: ['all'], - generalCases: ['all'], + generalCasesV2: ['all'], indexPatterns: ['all'], osquery: ['all'], savedObjectsManagement: ['all'], diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts index c2275ebbcee5f..29df069020561 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/mocks.ts @@ -26,6 +26,11 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ baseKibanaSubFeatureIds: [], subFeaturesMap: new Map(), })), + getCasesV2Feature: jest.fn(() => ({ + baseKibanaFeature: {}, + baseKibanaSubFeatureIds: [], + subFeaturesMap: new Map(), + })), getAssistantFeature: jest.fn(() => ({ baseKibanaFeature: {}, baseKibanaSubFeatureIds: [], diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts index 8d274a30ca3c9..768228f319b24 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.test.ts @@ -44,6 +44,7 @@ jest.mock('@kbn/security-solution-features/product_features', () => ({ getAttackDiscoveryFeature: () => mockGetFeature(), getAssistantFeature: () => mockGetFeature(), getCasesFeature: () => mockGetFeature(), + getCasesV2Feature: () => mockGetFeature(), getSecurityFeature: () => mockGetFeature(), })); @@ -56,8 +57,8 @@ describe('ProductFeaturesService', () => { const experimentalFeatures = {} as ExperimentalFeatures; new ProductFeaturesService(loggerMock.create(), experimentalFeatures); - expect(mockGetFeature).toHaveBeenCalledTimes(4); - expect(MockedProductFeatures).toHaveBeenCalledTimes(4); + expect(mockGetFeature).toHaveBeenCalledTimes(5); + expect(MockedProductFeatures).toHaveBeenCalledTimes(5); }); it('should init all ProductFeatures when initialized', () => { diff --git a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts index 86928ff905545..2901734527a93 100644 --- a/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts +++ b/x-pack/plugins/security_solution/server/lib/product_features_service/product_features_service.ts @@ -20,6 +20,7 @@ import { getAttackDiscoveryFeature, getCasesFeature, getSecurityFeature, + getCasesV2Feature, } from '@kbn/security-solution-features/product_features'; import type { RecursiveReadonly } from '@kbn/utility-types'; import type { ExperimentalFeatures } from '../../../common'; @@ -35,6 +36,7 @@ export const API_ACTION_PREFIX = `${APP_ID}-`; export class ProductFeaturesService { private securityProductFeatures: ProductFeatures; private casesProductFeatures: ProductFeatures; + private casesProductV2Features: ProductFeatures; private securityAssistantProductFeatures: ProductFeatures; private attackDiscoveryProductFeatures: ProductFeatures; private productFeatures?: Set; @@ -59,6 +61,7 @@ export class ProductFeaturesService { apiTags: casesApiTags, savedObjects: { files: filesSavedObjectTypes }, }); + this.casesProductFeatures = new ProductFeatures( this.logger, casesFeature.subFeaturesMap, @@ -66,6 +69,19 @@ export class ProductFeaturesService { casesFeature.baseKibanaSubFeatureIds ); + const casesV2Feature = getCasesV2Feature({ + uiCapabilities: casesUiCapabilities, + apiTags: casesApiTags, + savedObjects: { files: filesSavedObjectTypes }, + }); + + this.casesProductV2Features = new ProductFeatures( + this.logger, + casesV2Feature.subFeaturesMap, + casesV2Feature.baseKibanaFeature, + casesV2Feature.baseKibanaSubFeatureIds + ); + const assistantFeature = getAssistantFeature(this.experimentalFeatures); this.securityAssistantProductFeatures = new ProductFeatures( this.logger, @@ -86,6 +102,7 @@ export class ProductFeaturesService { public init(featuresSetup: FeaturesPluginSetup) { this.securityProductFeatures.init(featuresSetup); this.casesProductFeatures.init(featuresSetup); + this.casesProductV2Features.init(featuresSetup); this.securityAssistantProductFeatures.init(featuresSetup); this.attackDiscoveryProductFeatures.init(featuresSetup); } @@ -96,6 +113,7 @@ export class ProductFeaturesService { const casesProductFeaturesConfig = configurator.cases(); this.casesProductFeatures.setConfig(casesProductFeaturesConfig); + this.casesProductV2Features.setConfig(casesProductFeaturesConfig); const securityAssistantProductFeaturesConfig = configurator.securityAssistant(); this.securityAssistantProductFeatures.setConfig(securityAssistantProductFeaturesConfig); @@ -124,6 +142,7 @@ export class ProductFeaturesService { return ( this.securityProductFeatures.isActionRegistered(action) || this.casesProductFeatures.isActionRegistered(action) || + this.casesProductV2Features.isActionRegistered(action) || this.securityAssistantProductFeatures.isActionRegistered(action) || this.attackDiscoveryProductFeatures.isActionRegistered(action) ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_existing_case.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_existing_case.test.tsx index 7cf41aac902a6..d498565dd3908 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_existing_case.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_existing_case.test.tsx @@ -26,7 +26,7 @@ describe('AddToExistingCase', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: true, + createComment: true, update: true, }), }, @@ -51,7 +51,7 @@ describe('AddToExistingCase', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: true, + createComment: true, update: true, }), }, @@ -85,7 +85,7 @@ describe('AddToExistingCase', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: false, + createComment: false, update: false, }), }, diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_new_case.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_new_case.test.tsx index 3baedf85b5b7e..a92a08d10c571 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_new_case.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/components/add_to_new_case.test.tsx @@ -26,7 +26,7 @@ describe('AddToNewCase', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: true, + createComment: true, update: true, }), }, @@ -51,7 +51,7 @@ describe('AddToNewCase', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: true, + createComment: true, update: true, }), }, @@ -86,7 +86,7 @@ describe('AddToNewCase', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: false, + createComment: false, update: false, }), }, diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx index a43efebe98391..8e2f5d3d96a25 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.test.tsx @@ -36,7 +36,7 @@ describe('useCasePermission', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: true, + createComment: true, update: true, }), }, @@ -60,7 +60,7 @@ describe('useCasePermission', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: false, + createComment: false, update: true, }), }, @@ -84,7 +84,7 @@ describe('useCasePermission', () => { helpers: { ...casesServiceMock.helpers, canUseCases: () => ({ - create: true, + createComment: true, update: true, }), }, diff --git a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.ts b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.ts index f1a1079c23af1..89e35b8074811 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/cases/hooks/use_case_permission.ts @@ -24,7 +24,7 @@ export const useCaseDisabled = (indicatorName: string): boolean => { // disable the item if there is no indicator name or if the user doesn't have the right permission // in the case's attachment, the indicator name is the link to open the flyout const invalidIndicatorName: boolean = indicatorName === EMPTY_VALUE; - const hasPermission: boolean = permissions.create && permissions.update; + const hasPermission: boolean = permissions.createComment && permissions.update; return invalidIndicatorName || !hasPermission; }; diff --git a/x-pack/test/api_integration/apis/cases/common/roles.ts b/x-pack/test/api_integration/apis/cases/common/roles.ts index 5c3e7025900fd..21ad6943ba0df 100644 --- a/x-pack/test/api_integration/apis/cases/common/roles.ts +++ b/x-pack/test/api_integration/apis/cases/common/roles.ts @@ -111,6 +111,31 @@ export const secAll: Role = { }, }; +export const secCasesV2All: Role = { + name: 'sec_cases_v2_all_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionCasesV2: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + export const secAllSpace1: Role = { name: 'sec_all_role_space1_api_int', privileges: { @@ -384,6 +409,31 @@ export const casesAll: Role = { }, }; +export const casesV2All: Role = { + name: 'cases_v2_all_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + generalCasesV2: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + export const casesRead: Role = { name: 'cases_read_role_api_int', privileges: { @@ -508,6 +558,31 @@ export const obsCasesAll: Role = { }, }; +export const obsCasesV2All: Role = { + name: 'obs_cases_v2_all_role_api_int', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + observabilityCasesV2: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + }, + ], + }, +}; + export const obsCasesRead: Role = { name: 'obs_cases_read_role_api_int', privileges: { @@ -537,6 +612,7 @@ export const roles = [ secAllCasesOnlyReadDelete, secAllCasesNoDelete, secAll, + secCasesV2All, secAllSpace1, secAllCasesRead, secAllCasesNone, @@ -548,10 +624,12 @@ export const roles = [ casesOnlyReadDelete, casesNoDelete, casesAll, + casesV2All, casesRead, obsCasesOnlyDelete, obsCasesOnlyReadDelete, obsCasesNoDelete, obsCasesAll, + obsCasesV2All, obsCasesRead, ]; diff --git a/x-pack/test/api_integration/apis/cases/common/users.ts b/x-pack/test/api_integration/apis/cases/common/users.ts index 6cf938dcb0740..a64b9767498fb 100644 --- a/x-pack/test/api_integration/apis/cases/common/users.ts +++ b/x-pack/test/api_integration/apis/cases/common/users.ts @@ -8,16 +8,19 @@ import { User } from '../../../../cases_api_integration/common/lib/authentication/types'; import { casesAll, + casesV2All, casesNoDelete, casesOnlyDelete, casesOnlyReadDelete, casesRead, obsCasesAll, + obsCasesV2All, obsCasesNoDelete, obsCasesOnlyDelete, obsCasesOnlyReadDelete, obsCasesRead, secAll, + secCasesV2All, secAllCasesNoDelete, secAllCasesNone, secAllCasesOnlyDelete, @@ -58,6 +61,12 @@ export const secAllUser: User = { roles: [secAll.name], }; +export const secCasesV2AllUser: User = { + username: 'sec_cases_v2_all_user_api_int', + password: 'password', + roles: [secCasesV2All.name], +}; + export const secAllSpace1User: User = { username: 'sec_all_space1_user_api_int', password: 'password', @@ -128,6 +137,12 @@ export const casesAllUser: User = { roles: [casesAll.name], }; +export const casesV2AllUser: User = { + username: 'cases_v2_all_user_api_int', + password: 'password', + roles: [casesV2All.name], +}; + export const casesReadUser: User = { username: 'cases_read_user_api_int', password: 'password', @@ -162,6 +177,12 @@ export const obsCasesAllUser: User = { roles: [obsCasesAll.name], }; +export const obsCasesV2AllUser: User = { + username: 'obs_cases_v2_all_user_api_int', + password: 'password', + roles: [obsCasesV2All.name], +}; + export const obsCasesReadUser: User = { username: 'obs_cases_read_user_api_int', password: 'password', @@ -189,6 +210,7 @@ export const users = [ secAllCasesOnlyReadDeleteUser, secAllCasesNoDeleteUser, secAllUser, + secCasesV2AllUser, secAllSpace1User, secAllCasesReadUser, secAllCasesNoneUser, @@ -200,11 +222,13 @@ export const users = [ casesOnlyReadDeleteUser, casesNoDeleteUser, casesAllUser, + casesV2AllUser, casesReadUser, obsCasesOnlyDeleteUser, obsCasesOnlyReadDeleteUser, obsCasesNoDeleteUser, obsCasesAllUser, + obsCasesV2AllUser, obsCasesReadUser, obsSecCasesAllUser, obsSecCasesReadUser, diff --git a/x-pack/test/api_integration/apis/cases/privileges.ts b/x-pack/test/api_integration/apis/cases/privileges.ts index 96a8970adeeee..53a1767f5c1a7 100644 --- a/x-pack/test/api_integration/apis/cases/privileges.ts +++ b/x-pack/test/api_integration/apis/cases/privileges.ts @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { APP_ID as CASES_APP_ID } from '@kbn/cases-plugin/common/constants'; +import { AttachmentType } from '@kbn/cases-plugin/common'; +import { CaseStatuses, UserCommentAttachmentPayload } from '@kbn/cases-plugin/common/types/domain'; import { APP_ID as SECURITY_SOLUTION_APP_ID } from '@kbn/security-solution-plugin/common/constants'; import { observabilityFeatureId as OBSERVABILITY_APP_ID } from '@kbn/observability-plugin/common'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -16,12 +18,16 @@ import { deleteAllCaseItems, deleteCases, getCase, + createComment, + updateCaseStatus, } from '../../../cases_api_integration/common/lib/api'; import { casesAllUser, + casesV2AllUser, casesNoDeleteUser, casesOnlyDeleteUser, obsCasesAllUser, + obsCasesV2AllUser, obsCasesNoDeleteUser, obsCasesOnlyDeleteUser, secAllCasesNoDeleteUser, @@ -29,6 +35,7 @@ import { secAllCasesOnlyDeleteUser, secAllCasesReadUser, secAllUser, + secCasesV2AllUser, secReadCasesAllUser, secReadCasesNoneUser, secReadCasesReadUser, @@ -48,10 +55,13 @@ export default ({ getService }: FtrProviderContext): void => { for (const { user, owner } of [ { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, { user: secReadCasesAllUser, owner: SECURITY_SOLUTION_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, + { user: casesV2AllUser, owner: CASES_APP_ID }, { user: casesNoDeleteUser, owner: CASES_APP_ID }, { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can create a case`, async () => { @@ -68,8 +78,10 @@ export default ({ getService }: FtrProviderContext): void => { { user: secReadCasesReadUser, owner: SECURITY_SOLUTION_APP_ID }, { user: secReadUser, owner: SECURITY_SOLUTION_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, + { user: casesV2AllUser, owner: CASES_APP_ID }, { user: casesNoDeleteUser, owner: CASES_APP_ID }, { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can get a case`, async () => { @@ -125,10 +137,13 @@ export default ({ getService }: FtrProviderContext): void => { for (const { user, owner } of [ { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, { user: secAllCasesOnlyDeleteUser, owner: SECURITY_SOLUTION_APP_ID }, { user: casesAllUser, owner: CASES_APP_ID }, + { user: casesV2AllUser, owner: CASES_APP_ID }, { user: casesOnlyDeleteUser, owner: CASES_APP_ID }, { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, { user: obsCasesOnlyDeleteUser, owner: OBSERVABILITY_APP_ID }, ]) { it(`User ${user.username} with role(s) ${user.roles.join()} can delete a case`, async () => { @@ -160,5 +175,60 @@ export default ({ getService }: FtrProviderContext): void => { }); }); } + + for (const { user, owner } of [ + { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, + { user: casesAllUser, owner: CASES_APP_ID }, + { user: casesV2AllUser, owner: CASES_APP_ID }, + ]) { + it(`User ${user.username} with role(s) ${user.roles.join()} can reopen a case`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'closed' as CaseStatuses, + version: '2', + expectedHttpCode: 200, + auth: { user, space: null }, + }); + + await updateCaseStatus({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + status: 'open' as CaseStatuses, + version: '3', + expectedHttpCode: 200, + auth: { user, space: null }, + }); + }); + } + + for (const { user, owner } of [ + { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: secCasesV2AllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + { user: obsCasesV2AllUser, owner: OBSERVABILITY_APP_ID }, + { user: casesAllUser, owner: CASES_APP_ID }, + { user: casesV2AllUser, owner: CASES_APP_ID }, + ]) { + it(`User ${user.username} with role(s) ${user.roles.join()} can add comments`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest({ owner })); + const comment: UserCommentAttachmentPayload = { + comment: 'test', + owner, + type: AttachmentType.user, + }; + await createComment({ + params: comment, + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + expectedHttpCode: 200, + auth: { user, space: null }, + }); + }); + } }); }; diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 547fd12a54203..4ded1782c9086 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -111,7 +111,7 @@ export default function ({ getService }: FtrProviderContext) { 'guidedOnboardingFeature', 'monitoring', 'observabilityAIAssistant', - 'observabilityCases', + 'observabilityCasesV2', 'savedObjectsManagement', 'savedQueryManagement', 'savedObjectsTagging', @@ -119,7 +119,7 @@ export default function ({ getService }: FtrProviderContext) { 'apm', 'stackAlerts', 'canvas', - 'generalCases', + 'generalCasesV2', 'infrastructure', 'inventory', 'logs', @@ -133,7 +133,7 @@ export default function ({ getService }: FtrProviderContext) { 'slo', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', - 'securitySolutionCases', + 'securitySolutionCasesV2', 'fleet', 'fleetv2', ].sort() @@ -161,7 +161,7 @@ export default function ({ getService }: FtrProviderContext) { 'guidedOnboardingFeature', 'monitoring', 'observabilityAIAssistant', - 'observabilityCases', + 'observabilityCasesV2', 'savedObjectsManagement', 'savedQueryManagement', 'savedObjectsTagging', @@ -169,7 +169,7 @@ export default function ({ getService }: FtrProviderContext) { 'apm', 'stackAlerts', 'canvas', - 'generalCases', + 'generalCasesV2', 'infrastructure', 'inventory', 'logs', @@ -183,7 +183,7 @@ export default function ({ getService }: FtrProviderContext) { 'slo', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', - 'securitySolutionCases', + 'securitySolutionCasesV2', 'fleet', 'fleetv2', ]; diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 1ff986829415b..b269aef6ae1cc 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -30,6 +30,16 @@ export default function ({ getService }: FtrProviderContext) { 'cases_delete', 'cases_settings', ], + generalCasesV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + ], observabilityCases: [ 'all', 'read', @@ -38,6 +48,16 @@ export default function ({ getService }: FtrProviderContext) { 'cases_delete', 'cases_settings', ], + observabilityCasesV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], searchInferenceEndpoints: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -89,6 +109,16 @@ export default function ({ getService }: FtrProviderContext) { 'cases_delete', 'cases_settings', ], + securitySolutionCasesV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + ], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], dataQuality: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 57a166ef4be9d..a97ee360062c0 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -32,7 +32,9 @@ export default function ({ getService }: FtrProviderContext) { graph: ['all', 'read', 'minimal_all', 'minimal_read'], maps: ['all', 'read', 'minimal_all', 'minimal_read'], generalCases: ['all', 'read', 'minimal_all', 'minimal_read'], + generalCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'], + observabilityCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], canvas: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -47,6 +49,7 @@ export default function ({ getService }: FtrProviderContext) { securitySolutionAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionAttackDiscovery: ['all', 'read', 'minimal_all', 'minimal_read'], securitySolutionCases: ['all', 'read', 'minimal_all', 'minimal_read'], + securitySolutionCasesV2: ['all', 'read', 'minimal_all', 'minimal_read'], searchInferenceEndpoints: ['all', 'read', 'minimal_all', 'minimal_read'], fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'], fleet: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -112,6 +115,16 @@ export default function ({ getService }: FtrProviderContext) { 'cases_delete', 'cases_settings', ], + generalCasesV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + ], observabilityCases: [ 'all', 'read', @@ -120,6 +133,16 @@ export default function ({ getService }: FtrProviderContext) { 'cases_delete', 'cases_settings', ], + observabilityCasesV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + ], observabilityAIAssistant: ['all', 'read', 'minimal_all', 'minimal_read'], slo: ['all', 'read', 'minimal_all', 'minimal_read'], searchInferenceEndpoints: ['all', 'read', 'minimal_all', 'minimal_read'], @@ -177,6 +200,16 @@ export default function ({ getService }: FtrProviderContext) { 'cases_delete', 'cases_settings', ], + securitySolutionCasesV2: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'cases_delete', + 'cases_settings', + 'create_comment', + 'case_reopen', + ], infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'], logs: ['all', 'read', 'minimal_all', 'minimal_read'], dataQuality: ['all', 'read', 'minimal_all', 'minimal_read'], diff --git a/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts b/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts index a39796f1f4448..2a85320d14edf 100644 --- a/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts +++ b/x-pack/test/api_integration_basic/apis/security_solution/cases_privileges.ts @@ -37,7 +37,7 @@ const secAll: Role = { { feature: { siem: ['all'], - securitySolutionCases: ['all'], + securitySolutionCasesV2: ['all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -68,7 +68,7 @@ const secRead: Role = { { feature: { siem: ['read'], - securitySolutionCases: ['read'], + securitySolutionCasesV2: ['read'], actions: ['all'], actionsSimulators: ['all'], }, diff --git a/x-pack/test/cases_api_integration/common/lib/api/case.ts b/x-pack/test/cases_api_integration/common/lib/api/case.ts index 759e2de460460..9f03a62032c89 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/case.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/case.ts @@ -6,8 +6,12 @@ */ import { CASES_URL } from '@kbn/cases-plugin/common'; -import { Case } from '@kbn/cases-plugin/common/types/domain'; -import { CasePostRequest, CasesFindResponse } from '@kbn/cases-plugin/common/types/api'; +import { Case, CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; +import { + CasePostRequest, + CasesFindResponse, + CasePatchRequest, +} from '@kbn/cases-plugin/common/types/api'; import type SuperTest from 'supertest'; import { ToolingLog } from '@kbn/tooling-log'; import { User } from '../authentication/types'; @@ -91,3 +95,32 @@ export const deleteCases = async ({ return body; }; + +export const updateCaseStatus = async ({ + supertest, + caseId, + version = '2', + status = 'open' as CaseStatuses, + expectedHttpCode = 204, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.Agent; + caseId: string; + version?: string; + status?: CaseStatuses; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}) => { + const updateRequest: CasePatchRequest = { + status, + version, + id: caseId, + }; + + const { body: updatedCase } = await supertest + .patch(`/api/cases/${caseId}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'xxx') + .send(updateRequest); + return updatedCase; +}; diff --git a/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts b/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts index d5969606dc414..a3b8b71d2fc97 100644 --- a/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/cases_api_integration/common/lib/authentication/roles.ts @@ -7,31 +7,28 @@ import { Role } from './types'; +const defaultElasticsearchPrivileges = { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, +}; + export const noKibanaPrivileges: Role = { name: 'no_kibana_privileges', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, }, }; export const noCasesPrivilegesSpace1: Role = { name: 'no_cases_kibana_privileges', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -47,14 +44,7 @@ export const noCasesPrivilegesSpace1: Role = { export const noCasesConnectors: Role = { name: 'no_cases_connectors', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -71,14 +61,7 @@ export const noCasesConnectors: Role = { export const globalRead: Role = { name: 'global_read', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -96,14 +79,7 @@ export const globalRead: Role = { export const testDisabledPluginAll: Role = { name: 'test_disabled_plugin_all', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -121,14 +97,7 @@ export const testDisabledPluginAll: Role = { export const securitySolutionOnlyAll: Role = { name: 'sec_only_all', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -145,14 +114,7 @@ export const securitySolutionOnlyAll: Role = { export const securitySolutionOnlyDelete: Role = { name: 'sec_only_delete', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -169,18 +131,11 @@ export const securitySolutionOnlyDelete: Role = { export const securitySolutionOnlyReadDelete: Role = { name: 'sec_only_read_delete', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { - securitySolutionFixture: ['read', 'cases_delete'], + securitySolutionFixture: ['minimal_read', 'cases_delete'], actions: ['all'], actionsSimulators: ['all'], }, @@ -193,14 +148,58 @@ export const securitySolutionOnlyReadDelete: Role = { export const securitySolutionOnlyNoDelete: Role = { name: 'sec_only_no_delete', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], + ...defaultElasticsearchPrivileges, + kibana: [ + { + feature: { + securitySolutionFixture: ['minimal_all'], + actions: ['all'], + actionsSimulators: ['all'], }, - ], - }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyCreateComment: Role = { + name: 'sec_only_create_comment', + privileges: { + ...defaultElasticsearchPrivileges, + kibana: [ + { + feature: { + securitySolutionFixture: ['create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadCreateComment: Role = { + name: 'sec_only_read_create_comment', + privileges: { + ...defaultElasticsearchPrivileges, + kibana: [ + { + feature: { + securitySolutionFixture: ['minimal_read', 'create_comment'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyNoCreateComment: Role = { + name: 'sec_only_no_create_comment', + privileges: { + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -217,14 +216,7 @@ export const securitySolutionOnlyNoDelete: Role = { export const securitySolutionOnlyRead: Role = { name: 'sec_only_read', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -241,14 +233,7 @@ export const securitySolutionOnlyRead: Role = { export const securitySolutionOnlyReadAlerts: Role = { name: 'sec_only_read_alerts', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -282,14 +267,7 @@ export const securitySolutionOnlyReadNoIndexAlerts: Role = { export const observabilityOnlyAll: Role = { name: 'obs_only_all', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -306,14 +284,7 @@ export const observabilityOnlyAll: Role = { export const observabilityOnlyRead: Role = { name: 'obs_only_read', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -353,14 +324,7 @@ export const observabilityOnlyReadAlerts: Role = { export const securitySolutionOnlyAllSpacesRole: Role = { name: 'sec_only_all_spaces', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -377,14 +341,7 @@ export const securitySolutionOnlyAllSpacesRole: Role = { export const onlyActions: Role = { name: 'only_actions', privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, + ...defaultElasticsearchPrivileges, kibana: [ { feature: { @@ -408,6 +365,9 @@ export const roles = [ securitySolutionOnlyDelete, securitySolutionOnlyReadDelete, securitySolutionOnlyNoDelete, + securitySolutionOnlyCreateComment, + securitySolutionOnlyReadCreateComment, + securitySolutionOnlyNoCreateComment, observabilityOnlyAll, observabilityOnlyRead, observabilityOnlyReadAlerts, diff --git a/x-pack/test/cases_api_integration/common/lib/authentication/users.ts b/x-pack/test/cases_api_integration/common/lib/authentication/users.ts index 9bf90665eb181..01489d878526c 100644 --- a/x-pack/test/cases_api_integration/common/lib/authentication/users.ts +++ b/x-pack/test/cases_api_integration/common/lib/authentication/users.ts @@ -23,6 +23,9 @@ import { securitySolutionOnlyReadDelete, noCasesConnectors as noCasesConnectorRole, onlyActions as onlyActionsRole, + securitySolutionOnlyCreateComment, + securitySolutionOnlyNoCreateComment, + securitySolutionOnlyReadCreateComment, } from './roles'; import { User } from './types'; @@ -62,6 +65,24 @@ export const secOnlyNoDelete: User = { roles: [securitySolutionOnlyNoDelete.name], }; +export const secOnlyCreateComment: User = { + username: 'sec_only_create_comment', + password: 'sec_only_create_comment', + roles: [securitySolutionOnlyCreateComment.name], +}; + +export const secOnlyReadCreateComment: User = { + username: 'sec_only_read_create_comment', + password: 'sec_only_read_create_comment', + roles: [securitySolutionOnlyReadCreateComment.name], +}; + +export const secOnlyNoCreateComment: User = { + username: 'sec_only_no_create_comment', + password: 'sec_only_no_create_comment', + roles: [securitySolutionOnlyNoCreateComment.name], +}; + export const secOnlyRead: User = { username: 'sec_only_read', password: 'sec_only_read', @@ -159,6 +180,9 @@ export const users = [ secOnlyDelete, secOnlyReadDelete, secOnlyNoDelete, + secOnlyCreateComment, + secOnlyReadCreateComment, + secOnlyNoCreateComment, obsOnly, obsOnlyRead, obsOnlyReadAlerts, diff --git a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts index e2c7cf4d88411..34f4c6d7423c0 100644 --- a/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/security_solution/server/plugin.ts @@ -115,6 +115,52 @@ export class FixturePlugin implements Plugin { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('createComment subprivilege', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('user comments', () => { + it('should not create user comments', async () => { + // No privileges + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnlyNoCreateComment, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnlyNoCreateComment, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + // Create + for (const scenario of [ + { user: secOnlyReadCreateComment, space: 'space1' }, + { user: secOnlyCreateComment, space: 'space1' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should create user comments`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: scenario, + expectedHttpCode: 200, + }); + }); + } + + // Update + it('should update comment without createComment privileges', async () => { + // Note: Not ideal behavior. A user unable to create should not be able to update, + // but it is a concession until the privileges are properly broken apart. + const commentUpdate = 'Heres an update because I do not want to make a new comment!'; + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + const updatedCommentCase = await updateComment({ + supertest, + caseId: postedCase.id, + auth: { user: secOnlyNoCreateComment, space: 'space1' }, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: commentUpdate, + type: AttachmentType.user, + owner: 'securitySolutionFixture', + }, + }); + + const userActions = await getCaseUserActions({ + supertest, + caseID: postedCase.id, + auth: { user: superUser, space: 'space1' }, + }); + const commentUserAction = userActions[2]; + + expect(userActions.length).to.eql(3); + expect(commentUserAction.type).to.eql('comment'); + expect(commentUserAction.action).to.eql('update'); + expect(commentUserAction.comment_id).to.eql(updatedCommentCase.comments![0].id); + expect(commentUserAction.payload).to.eql({ + comment: { + comment: commentUpdate, + type: AttachmentType.user, + owner: 'securitySolutionFixture', + }, + }); + }); + + // Update + for (const scenario of [ + { user: secOnlyCreateComment, space: 'space1' }, + { user: secOnlyReadCreateComment, space: 'space1' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should not update user comments`, async () => { + const commentUpdate = 'Heres an update because I do not want to make a new comment!'; + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space1' }, + }); + + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: scenario, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: commentUpdate, + type: AttachmentType.user, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 403, + }); + }); + } + }); + + describe('alerts', () => { + it('should not attach alerts to the case', async () => { + // No privileges + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentAlertMultipleIdsReq, + auth: { user: secOnlyNoCreateComment, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + // Create + for (const scenario of [ + { user: secOnlyCreateComment, space: 'space1' }, + { user: secOnlyReadCreateComment, space: 'space1' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should attach alerts`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentAlertMultipleIdsReq, + auth: scenario, + expectedHttpCode: 200, + }); + }); + } + + // Delete + for (const scenario of [ + { user: secOnlyNoCreateComment, space: 'space1' }, + { user: secOnlyCreateComment, space: 'space1' }, + { user: secOnlyReadCreateComment, space: 'space1' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should not delete attached alerts`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentAlertMultipleIdsReq, + auth: { user: superUser, space: 'space1' }, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: scenario, + expectedHttpCode: 403, + }); + }); + } + }); + + describe('files', () => { + it('should not attach files to the case', async () => { + // No privileges + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnlyNoCreateComment, space: 'space1' }, + params: getFilesAttachmentReq(), + expectedHttpCode: 403, + }); + }); + + // Create + for (const scenario of [ + { user: secOnlyCreateComment, space: 'space1' }, + { user: secOnlyReadCreateComment, space: 'space1' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should attach files`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + const caseWithAttachments = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: scenario, + params: getFilesAttachmentReq(), + expectedHttpCode: 200, + }); + + const fileAttachment = + caseWithAttachments.comments![0] as ExternalReferenceSOAttachmentPayload; + + expect(caseWithAttachments.totalComment).to.be(1); + expect(fileAttachment.externalReferenceMetadata).to.eql(fileAttachmentMetadata); + }); + } + + // Delete + for (const scenario of [ + { user: secOnlyCreateComment, space: 'space1' }, + { user: secOnlyReadCreateComment, space: 'space1' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should not delete attached files`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: superUser, space: 'space1' }, + params: getFilesAttachmentReq(), + expectedHttpCode: 200, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: scenario, + expectedHttpCode: 403, + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/delete_sub_privilege.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/delete_sub_privilege.ts index 75388fe0bfe19..22ac95050cffa 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/delete_sub_privilege.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/delete_sub_privilege.ts @@ -24,6 +24,7 @@ import { } from '../../../common/lib/api'; import { superUser, + secOnlyCreateComment, secOnlyDelete, secOnlyNoDelete, } from '../../../common/lib/authentication/users'; @@ -306,7 +307,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: { user: secOnlyNoDelete, space: 'space1' }, + auth: { user: secOnlyCreateComment, space: 'space1' }, }); await deleteComment({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts index c1038eb964313..3112dfab7ec66 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/trial/index.ts @@ -36,6 +36,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { loadTestFile(require.resolve('./attachments_framework/registered_persistable_state_trial')); // sub privileges are only available with a license above basic loadTestFile(require.resolve('./delete_sub_privilege')); + loadTestFile(require.resolve('./create_comment_sub_privilege.ts')); loadTestFile(require.resolve('./user_profiles/get_current')); // Internal routes diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index 6d9aee298beaa..05738e664796d 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -150,7 +150,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide savedObjectsManagement: ['all'], advancedSettings: ['all'], indexPatterns: ['all'], - generalCases: ['all'], + generalCasesV2: ['all'], ml: ['none'], }, spaces: ['*'], @@ -179,7 +179,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide savedObjectsManagement: ['all'], advancedSettings: ['all'], indexPatterns: ['all'], - generalCases: ['all'], + generalCasesV2: ['all'], }, spaces: ['*'], }, diff --git a/x-pack/test/functional/services/observability/users.ts b/x-pack/test/functional/services/observability/users.ts index 0e2915190d126..2386c08a4f90e 100644 --- a/x-pack/test/functional/services/observability/users.ts +++ b/x-pack/test/functional/services/observability/users.ts @@ -58,7 +58,7 @@ export function ObservabilityUsersProvider({ getPageObject, getService }: FtrPro */ const defineBasicObservabilityRole = ( features: Partial<{ - observabilityCases: string[]; + observabilityCasesV2: string[]; apm: string[]; logs: string[]; infrastructure: string[]; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/common/roles.ts b/x-pack/test/functional_with_es_ssl/apps/cases/common/roles.ts index f06c8745d6df6..0e8cb455ad299 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/common/roles.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/common/roles.ts @@ -25,7 +25,7 @@ export const casesReadDelete: Role = { kibana: [ { feature: { - generalCases: ['minimal_read', 'cases_delete'], + generalCasesV2: ['minimal_read', 'cases_delete'], actions: ['all'], actionsSimulators: ['all'], }, @@ -49,7 +49,7 @@ export const casesNoDelete: Role = { kibana: [ { feature: { - generalCases: ['minimal_all'], + generalCasesV2: ['minimal_all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -73,7 +73,7 @@ export const casesAll: Role = { kibana: [ { feature: { - generalCases: ['all'], + generalCasesV2: ['all'], actions: ['all'], actionsSimulators: ['all'], }, diff --git a/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx b/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx index 31c0b25f51e94..6ab6a1cce3610 100644 --- a/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx +++ b/x-pack/test/functional_with_es_ssl/plugins/cases/public/application.tsx @@ -42,6 +42,8 @@ const permissions = { push: true, connectors: true, settings: true, + createComment: true, + reopenCase: true, }; const attachments = [{ type: AttachmentType.user as const, comment: 'test' }]; diff --git a/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts index a71c83a5221c3..81fb1d23ba33e 100644 --- a/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts @@ -43,7 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCases: ['all'], + observabilityCasesV2: ['all'], logs: ['all'], }) ); @@ -96,7 +96,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCases: ['read'], + observabilityCasesV2: ['read'], logs: ['all'], }) ); diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts index 33b2ad3ba329a..ccb4264147523 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts @@ -29,7 +29,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCases: ['all'], + observabilityCasesV2: ['all'], logs: ['all'], }) ); @@ -75,7 +75,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCases: ['read'], + observabilityCasesV2: ['read'], logs: ['all'], }) ); diff --git a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts index ac6343f8e7170..90fc09af9c6ad 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts @@ -33,7 +33,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { await observability.users.setTestUserRole( observability.users.defineBasicObservabilityRole({ - observabilityCases: ['all'], + observabilityCasesV2: ['all'], logs: ['all'], }) ); diff --git a/x-pack/test/security_api_integration/tests/features/deprecated_features.ts b/x-pack/test/security_api_integration/tests/features/deprecated_features.ts index 6e868fc5946ec..29135ff2440b2 100644 --- a/x-pack/test/security_api_integration/tests/features/deprecated_features.ts +++ b/x-pack/test/security_api_integration/tests/features/deprecated_features.ts @@ -181,6 +181,9 @@ export default function ({ getService }: FtrProviderContext) { "case_3_feature_a", "case_4_feature_a", "case_4_feature_b", + "generalCases", + "observabilityCases", + "securitySolutionCases", ] `); }); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts index 826ca78228b61..9a60b4b281b07 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/timelines/export.cy.ts @@ -45,8 +45,9 @@ describe('Export timelines', { tags: ['@ess', '@serverless'] }, () => { /** * TODO: Good candidate for converting to a jest Test * https://github.com/elastic/kibana/issues/195612 + * Failing: https://github.com/elastic/kibana/issues/187550 */ - it('should export custom timeline(s)', function () { + it.skip('should export custom timeline(s)', function () { cy.log('Export a custom timeline via timeline actions'); exportTimeline(this.timelineId1); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts index 7f2d0dea8b545..bbbaaa1e240a6 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/privileges.ts @@ -66,6 +66,7 @@ export const secAll: Role = { securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], + securitySolutionCasesV2: ['all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -99,6 +100,7 @@ export const secReadCasesAll: Role = { securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['all'], + securitySolutionCasesV2: ['all'], actions: ['all'], actionsSimulators: ['all'], }, @@ -132,6 +134,7 @@ export const secAllCasesOnlyReadDelete: Role = { securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['cases_read', 'cases_delete'], + securitySolutionCasesV2: ['cases_read', 'cases_delete'], actions: ['all'], actionsSimulators: ['all'], }, @@ -165,6 +168,7 @@ export const secAllCasesNoDelete: Role = { securitySolutionAssistant: ['all'], securitySolutionAttackDiscovery: ['all'], securitySolutionCases: ['minimal_all'], + securitySolutionCasesV2: ['minimal_all'], actions: ['all'], actionsSimulators: ['all'], }, diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 795d177805f89..d84945fbfe032 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -81,9 +81,11 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) 'inventory', 'logs', 'observabilityCases', + 'observabilityCasesV2', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCases', + 'securitySolutionCasesV2', 'siem', 'slo', 'uptime', diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index b90128ab12c70..9d51cbb12e469 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -80,9 +80,11 @@ const ALL_SPACE_RESULTS: Space[] = [ 'inventory', 'logs', 'observabilityCases', + 'observabilityCasesV2', 'securitySolutionAssistant', 'securitySolutionAttackDiscovery', 'securitySolutionCases', + 'securitySolutionCasesV2', 'siem', 'slo', 'uptime', diff --git a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts index e691f84d7bdc7..4a43c3831627c 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/telemetry/telemetry.ts @@ -66,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) { maintenanceWindow: 0, stackAlerts: 0, generalCases: 0, + generalCasesV2: 0, maps: 2, canvas: 2, ml: 0, @@ -73,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { fleet: 0, osquery: 0, observabilityCases: 0, + observabilityCasesV2: 0, uptime: 0, slo: 0, infrastructure: 0, @@ -84,6 +86,7 @@ export default function ({ getService }: FtrProviderContext) { searchInferenceEndpoints: 0, siem: 0, securitySolutionCases: 0, + securitySolutionCasesV2: 0, securitySolutionAssistant: 0, securitySolutionAttackDiscovery: 0, discover: 0, diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 2ba14ceb1218c..9db41aecbb612 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -187,6 +187,6 @@ "@kbn/alerting-types", "@kbn/ai-assistant-common", "@kbn/core-deprecations-common", - "@kbn/usage-collection-plugin" + "@kbn/usage-collection-plugin", ] } diff --git a/x-pack/test_serverless/shared/lib/security/default_http_headers.ts b/x-pack/test_serverless/shared/lib/security/default_http_headers.ts index 03c96905d6b06..18293b74ce116 100644 --- a/x-pack/test_serverless/shared/lib/security/default_http_headers.ts +++ b/x-pack/test_serverless/shared/lib/security/default_http_headers.ts @@ -8,4 +8,5 @@ export const STANDARD_HTTP_HEADERS = Object.freeze({ 'kbn-xsrf': 'cypress-creds-via-env', 'x-elastic-internal-origin': 'security-solution', + 'elastic-api-version': '2023-10-31', }); diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml index 22b3fd31c423b..61d3378de4c68 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml +++ b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml @@ -493,6 +493,7 @@ soc_manager: - application: "kibana-.kibana" privileges: - feature_ml.read + - feature_generalCases.all - feature_siem.all - feature_siem.read_alerts - feature_siem.crud_alerts @@ -509,6 +510,7 @@ soc_manager: - feature_siem.execute_operations_all - feature_siem.scan_operations_all - feature_securitySolutionCases.all + - feature_observabilityCases.all - feature_securitySolutionAssistant.all - feature_securitySolutionAttackDiscovery.all - feature_actions.all From 91e6da2fb43df91d36b5e3de3c5578e685165b98 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:35:25 +1100 Subject: [PATCH 34/42] [8.x] fix: Change "Single Account" to "Single Project" in button text (#200327) (#200813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [fix: Change "Single Account" to "Single Project" in button text (#200327)](https://github.com/elastic/kibana/pull/200327) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Samantha Tan <96286575+samantha-t28@users.noreply.github.com> --- .../components/fleet_extensions/policy_template_form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 73d8ed22011dc..9d8deb5b9892d 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -148,7 +148,7 @@ const getGcpAccountTypeOptions = (isGcpOrgDisabled: boolean): CspRadioGroupProps { id: GCP_SINGLE_ACCOUNT, label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpSingleAccountLabel', { - defaultMessage: 'Single Account', + defaultMessage: 'Single Project', }), testId: 'gcpSingleAccountTestId', }, @@ -377,7 +377,7 @@ const GcpAccountTypeSelect = ({ From 26bfdaef960d09147ace4fff47a5eaa80f3f3ea0 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 08:46:13 +1100 Subject: [PATCH 35/42] [8.x] feat: update OTEL Node.js metrics dashboard (#199353) (#200810) # Backport This will backport the following commits from `main` to `8.x`: - [feat: update OTEL Node.js metrics dashboard (#199353)](https://github.com/elastic/kibana/pull/199353) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: David Luna --- .../metrics/static_dashboard/dashboards/dashboard_catalog.ts | 1 + .../static_dashboard/dashboards/opentelemetry_nodejs.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts index 6f81ef6db535b..ea3c468a6c829 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts @@ -8,6 +8,7 @@ export const AGENT_NAME_DASHBOARD_FILE_MAPPING: Record = { nodejs: 'nodejs', 'opentelemetry/nodejs': 'opentelemetry_nodejs', + 'opentelemetry/nodejs/elastic': 'opentelemetry_nodejs', java: 'java', 'opentelemetry/java': 'opentelemetry_java', 'opentelemetry/java/opentelemetry-java-instrumentation': 'opentelemetry_java', diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json index b9552e182893b..2eeb4d48b11d1 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json +++ b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json @@ -1 +1 @@ -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.10.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f3de253a-8c79-46d0-acb2-05eef8d056be\"},\"panelIndex\":\"f3de253a-8c79-46d0-acb2-05eef8d056be\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-ed2c0b0b-d8c5-415d-aec6-7681d578d3db\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"shouldTruncate\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"ed2c0b0b-d8c5-415d-aec6-7681d578d3db\",\"accessors\":[\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\",\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\"}],\"yTitle\":\"Usage [bytes]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"ed2c0b0b-d8c5-415d-aec6-7681d578d3db\":{\"columns\":{\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\":{\"label\":\"Free\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"free\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\":{\"label\":\"Used\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"free\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"add\",\"args\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\"],\"location\":{\"min\":0,\"max\":115},\"text\":\"average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\"}},\"references\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\"],\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\":{\"label\":\"Total\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"isFormulaBroken\":false,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"references\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\"],\"customLabel\":true}},\"columnOrder\":[\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\",\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\",\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"System Memory Usage\"},{\"version\":\"8.10.2\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"9dcf1114-6984-4238-8229-cb9e802e0bdb\"},\"panelIndex\":\"9dcf1114-6984-4238-8229-cb9e802e0bdb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"apm_static_index_pattern_id\",\"name\":\"indexpattern-datasource-layer-1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\",\"accessors\":[\"74908758-6e28-43d5-929a-b4070c9026a1\",\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"24a38611-9513-4e32-bb79-2a723c11a513\"}],\"yTitle\":\"Utilization [%]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\":{\"columns\":{\"24a38611-9513-4e32-bb79-2a723c11a513\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"74908758-6e28-43d5-929a-b4070c9026a1\":{\"label\":\"Average\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.utilization\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\":{\"label\":\"Max\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"system.memory.utilization\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true,\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"}}},\"columnOrder\":[\"24a38611-9513-4e32-bb79-2a723c11a513\",\"74908758-6e28-43d5-929a-b4070c9026a1\",\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"System Memory Utilization\"}]","timeRestore":false,"title":"OpenTelemetry Memory Metrics","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-11-06T12:28:32.691Z","id":"fef77323-303f-4e39-a81f-553c268d16ec","managed":false,"references":[{"id":"metrics-*","name":"f3de253a-8c79-46d0-acb2-05eef8d056be:indexpattern-datasource-layer-ed2c0b0b-d8c5-415d-aec6-7681d578d3db","type":"index-pattern"},{"id":"apm_static_index_pattern_id","name":"9dcf1114-6984-4238-8229-cb9e802e0bdb:indexpattern-datasource-layer-1633fa19-f9f3-4149-90c3-bffd1ba4e6c4","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"8.9.0","updated_at":"2023-11-06T12:28:32.691Z","version":"WzM4OSwyXQ=="} \ No newline at end of file +{"attributes":{"controlGroupInput":{"chainingSystem":"HIERARCHICAL","controlStyle":"oneLine","ignoreParentSettingsJSON":"{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}","panelsJSON":"{\"c27c120b-4674-4ad7-9972-cd10bd9c1a34\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"id\":\"c27c120b-4674-4ad7-9972-cd10bd9c1a34\",\"dataViewId\":\"metrics-*\",\"fieldName\":\"service.node.name\",\"title\":\"Node\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"},\"exclude\":true}}}","showApplySelections":false},"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"48508429-15ea-4628-aa6f-23ca15aa1f55\"},\"panelIndex\":\"48508429-15ea-4628-aa6f-23ca15aa1f55\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-969de0e0-6fd8-493b-b789-9edd46b5c67c\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"969de0e0-6fd8-493b-b789-9edd46b5c67c\",\"accessors\":[\"0c1e94e4-2029-4558-8963-ceeb3169486a\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\",\"splitAccessor\":\"bcb37e21-93fb-4913-a749-75bb1b594002\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"969de0e0-6fd8-493b-b789-9edd46b5c67c\":{\"columns\":{\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"0c1e94e4-2029-4558-8963-ceeb3169486a\":{\"label\":\"Usage[bytes]\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"process.memory.usage\",\"filter\":{\"query\":\"\\\"process.memory.usage\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"bcb37e21-93fb-4913-a749-75bb1b594002\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"0c1e94e4-2029-4558-8963-ceeb3169486a\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"bcb37e21-93fb-4913-a749-75bb1b594002\",\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\",\"0c1e94e4-2029-4558-8963-ceeb3169486a\"],\"incompleteColumns\":{},\"sampling\":1,\"indexPatternId\":\"metrics-*\"}},\"currentIndexPatternId\":\"metrics-*\"},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Process Memory Usage\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0\"},\"panelIndex\":\"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-ecea9aed-08bf-47a0-8050-cb8b168bfc05\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"showSingleSeries\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"ecea9aed-08bf-47a0-8050-cb8b168bfc05\",\"accessors\":[\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\",\"003ad900-7138-4989-9b8c-a7290d7243d1\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"ed17ee1c-af87-455d-96d9-ca21614b5eed\",\"splitAccessor\":\"a137d8d5-8283-45ac-b145-e364dcec6697\",\"collapseFn\":\"\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"}}],\"yTitle\":\"Delay [sec]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"ecea9aed-08bf-47a0-8050-cb8b168bfc05\":{\"columns\":{\"a137d8d5-8283-45ac-b145-e364dcec6697\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}},\"ed17ee1c-af87-455d-96d9-ca21614b5eed\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\":{\"label\":\"p90\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.delay.p90\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.delay.p90\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\"},\"customLabel\":true},\"003ad900-7138-4989-9b8c-a7290d7243d1\":{\"label\":\"p50\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.delay.p50\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.delay.p50\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\"},\"customLabel\":true}},\"columnOrder\":[\"a137d8d5-8283-45ac-b145-e364dcec6697\",\"ed17ee1c-af87-455d-96d9-ca21614b5eed\",\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\",\"003ad900-7138-4989-9b8c-a7290d7243d1\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Event Loop Delay\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2\"},\"panelIndex\":\"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-0b0f2810-b098-4db1-a3a9-607e0361b2f1\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"showSingleSeries\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"0b0f2810-b098-4db1-a3a9-607e0361b2f1\",\"accessors\":[\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"b9505654-28b7-4121-883b-6745d4660cd3\",\"splitAccessor\":\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\",\"collapseFn\":\"\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"}}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"0b0f2810-b098-4db1-a3a9-607e0361b2f1\":{\"columns\":{\"b9505654-28b7-4121-883b-6745d4660cd3\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"64d972ab-b5b0-4c18-884b-a245d99fdc39\":{\"label\":\"Utilization[%]\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.utilization\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.utilization\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\",\"b9505654-28b7-4121-883b-6745d4660cd3\",\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Event Loop Utilization\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"d0e1efe6-23b1-4e61-8602-6df59ac0609b\"},\"panelIndex\":\"d0e1efe6-23b1-4e61-8602-6df59ac0609b\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-176f3949-5c04-44e2-a200-810c8fe28183\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yTitle\":\"Utilization[%]\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"176f3949-5c04-44e2-a200-810c8fe28183\",\"accessors\":[\"d5901d17-43c6-44ff-acd3-02824a9aed37\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\",\"splitAccessor\":\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"176f3949-5c04-44e2-a200-810c8fe28183\":{\"columns\":{\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"d5901d17-43c6-44ff-acd3-02824a9aed37\":{\"label\":\"Last value of process.cpu.utilization\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"process.cpu.utilization\",\"filter\":{\"query\":\"\\\"process.cpu.utilization\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}}},\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"d5901d17-43c6-44ff-acd3-02824a9aed37\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\",\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\",\"d5901d17-43c6-44ff-acd3-02824a9aed37\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Process CPU Utilization\"}]","timeRestore":false,"title":"OTEL nodejs runtime metrics","version":2},"coreMigrationVersion":"8.8.0","created_at":"2024-10-15T10:37:56.030Z","id":"4fdfa1d4-6dfa-45fa-8f21-37b9f93d30d2","managed":false,"references":[{"id":"metrics-*","name":"48508429-15ea-4628-aa6f-23ca15aa1f55:indexpattern-datasource-layer-969de0e0-6fd8-493b-b789-9edd46b5c67c","type":"index-pattern"},{"id":"metrics-*","name":"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0:indexpattern-datasource-layer-ecea9aed-08bf-47a0-8050-cb8b168bfc05","type":"index-pattern"},{"id":"metrics-*","name":"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2:indexpattern-datasource-layer-0b0f2810-b098-4db1-a3a9-607e0361b2f1","type":"index-pattern"},{"id":"metrics-*","name":"d0e1efe6-23b1-4e61-8602-6df59ac0609b:indexpattern-datasource-layer-176f3949-5c04-44e2-a200-810c8fe28183","type":"index-pattern"},{"id":"metrics-*","name":"controlGroup_c27c120b-4674-4ad7-9972-cd10bd9c1a34:optionsListDataView","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"10.2.0","updated_at":"2024-11-07T18:37:51.713Z","updated_by":"u_elastic_found","version":"WzE1MzcsMTRd"} \ No newline at end of file From 8c0bbb09b427309d8eeaa93da1d3c878fc5f97c3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:33:28 +1100 Subject: [PATCH 36/42] [8.x] Shutdown Kibana on usages of PKCS12 truststore/keystore config (#192627) (#200818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [Shutdown Kibana on usages of PKCS12 truststore/keystore config (#192627)](https://github.com/elastic/kibana/pull/192627) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Kurt --- .../src/fips/fips.test.ts | 153 +++++++++++++----- .../src/fips/fips.ts | 56 ++++++- .../src/security_service.ts | 5 +- .../src/utils/index.ts | 11 ++ 4 files changed, 176 insertions(+), 49 deletions(-) diff --git a/packages/core/security/core-security-server-internal/src/fips/fips.test.ts b/packages/core/security/core-security-server-internal/src/fips/fips.test.ts index 8726e3b5a34ee..ff610493e1322 100644 --- a/packages/core/security/core-security-server-internal/src/fips/fips.test.ts +++ b/packages/core/security/core-security-server-internal/src/fips/fips.test.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { CriticalError } from '@kbn/core-base-server-internal'; + const mockGetFipsFn = jest.fn(); jest.mock('crypto', () => ({ randomBytes: jest.fn(), @@ -21,54 +23,41 @@ import { isFipsEnabled, checkFipsConfig } from './fips'; import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; describe('fips', () => { - let config: SecurityServiceConfigType; + let securityConfig: SecurityServiceConfigType; describe('#isFipsEnabled', () => { it('should return `true` if config.experimental.fipsMode.enabled is `true`', () => { - config = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { experimental: { fipsMode: { enabled: true } } }; - expect(isFipsEnabled(config)).toBe(true); + expect(isFipsEnabled(securityConfig)).toBe(true); }); it('should return `false` if config.experimental.fipsMode.enabled is `false`', () => { - config = { experimental: { fipsMode: { enabled: false } } }; + securityConfig = { experimental: { fipsMode: { enabled: false } } }; - expect(isFipsEnabled(config)).toBe(false); + expect(isFipsEnabled(securityConfig)).toBe(false); }); it('should return `false` if config.experimental.fipsMode.enabled is `undefined`', () => { - expect(isFipsEnabled(config)).toBe(false); + expect(isFipsEnabled(securityConfig)).toBe(false); }); }); describe('checkFipsConfig', () => { - let mockExit: jest.SpyInstance; - - beforeAll(() => { - mockExit = jest.spyOn(process, 'exit').mockImplementation((exitCode) => { - throw new Error(`Fake Exit: ${exitCode}`); - }); - }); - - afterAll(() => { - mockExit.mockRestore(); - }); - it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode false', async () => { - config = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { experimental: { fipsMode: { enabled: true } } }; const logger = loggingSystemMock.create().get(); + let fipsException: undefined | CriticalError; try { - checkFipsConfig(config, logger); + checkFipsConfig(securityConfig, {}, {}, logger); } catch (e) { - expect(mockExit).toHaveBeenNthCalledWith(1, 78); + fipsException = e; } - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` - Array [ - Array [ - "Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled", - ], - ] - `); + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled' + ); }); it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled false, Nodejs FIPS mode true', async () => { @@ -76,22 +65,20 @@ describe('fips', () => { return 1; }); - config = { experimental: { fipsMode: { enabled: false } } }; + securityConfig = { experimental: { fipsMode: { enabled: false } } }; const logger = loggingSystemMock.create().get(); + let fipsException: undefined | CriticalError; try { - checkFipsConfig(config, logger); + checkFipsConfig(securityConfig, {}, {}, logger); } catch (e) { - expect(mockExit).toHaveBeenNthCalledWith(1, 78); + fipsException = e; } - - expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` - Array [ - Array [ - "Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled", - ], - ] - `); + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled' + ); }); it('should log an info message if FIPS mode is properly configured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode true', async () => { @@ -99,11 +86,11 @@ describe('fips', () => { return 1; }); - config = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { experimental: { fipsMode: { enabled: true } } }; const logger = loggingSystemMock.create().get(); try { - checkFipsConfig(config, logger); + checkFipsConfig(securityConfig, {}, {}, logger); } catch (e) { logger.error('Should not throw error!'); } @@ -116,5 +103,89 @@ describe('fips', () => { ] `); }); + + describe('PKCS12 Config settings', function () { + let serverConfig = {}; + let elasticsearchConfig = {}; + + beforeEach(function () { + mockGetFipsFn.mockImplementationOnce(() => { + return 1; + }); + + securityConfig = { experimental: { fipsMode: { enabled: true } } }; + }); + + afterEach(function () { + serverConfig = {}; + elasticsearchConfig = {}; + }); + + it('should log an error message for each PKCS12 configuration option that is set', async () => { + elasticsearchConfig = { + ssl: { + keystore: { + path: '/test', + }, + truststore: { + path: '/test', + }, + }, + }; + + serverConfig = { + ssl: { + keystore: { + path: '/test', + }, + truststore: { + path: '/test', + }, + }, + }; + + const logger = loggingSystemMock.create().get(); + + let fipsException: undefined | CriticalError; + try { + checkFipsConfig(securityConfig, elasticsearchConfig, serverConfig, logger); + } catch (e) { + fipsException = e; + } + + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error: elasticsearch.ssl.keystore.path, elasticsearch.ssl.truststore.path, server.ssl.keystore.path, server.ssl.truststore.path are set, PKCS12 configurations are not allowed while running in FIPS mode.' + ); + }); + + it('should log an error message for one PKCS12 configuration option that is set', async () => { + elasticsearchConfig = { + ssl: { + keystore: { + path: '/test', + }, + }, + }; + + serverConfig = {}; + + const logger = loggingSystemMock.create().get(); + + let fipsException: undefined | CriticalError; + try { + checkFipsConfig(securityConfig, elasticsearchConfig, serverConfig, logger); + } catch (e) { + fipsException = e; + } + + expect(fipsException).toBeInstanceOf(CriticalError); + expect(fipsException!.processExitCode).toBe(78); + expect(fipsException!.message).toEqual( + 'Configuration mismatch error: elasticsearch.ssl.keystore.path is set, PKCS12 configurations are not allowed while running in FIPS mode.' + ); + }); + }); }); }); diff --git a/packages/core/security/core-security-server-internal/src/fips/fips.ts b/packages/core/security/core-security-server-internal/src/fips/fips.ts index 8f09facb554b5..0d9dea9e467fe 100644 --- a/packages/core/security/core-security-server-internal/src/fips/fips.ts +++ b/packages/core/security/core-security-server-internal/src/fips/fips.ts @@ -9,28 +9,70 @@ import type { Logger } from '@kbn/logging'; import { getFips } from 'crypto'; -import { SecurityServiceConfigType } from '../utils'; - +import { CriticalError } from '@kbn/core-base-server-internal'; +import { PKCS12ConfigType, SecurityServiceConfigType } from '../utils'; export function isFipsEnabled(config: SecurityServiceConfigType): boolean { return config?.experimental?.fipsMode?.enabled ?? false; } -export function checkFipsConfig(config: SecurityServiceConfigType, logger: Logger) { +export function checkFipsConfig( + config: SecurityServiceConfigType, + elasticsearchConfig: PKCS12ConfigType, + serverConfig: PKCS12ConfigType, + logger: Logger +) { const isFipsConfigEnabled = isFipsEnabled(config); const isNodeRunningWithFipsEnabled = getFips() === 1; // Check if FIPS is enabled in either setting if (isFipsConfigEnabled || isNodeRunningWithFipsEnabled) { - // FIPS must be enabled on both or log and error an exit Kibana + const definedPKCS12ConfigOptions = findDefinedPKCS12ConfigOptions( + elasticsearchConfig, + serverConfig + ); + // FIPS must be enabled on both, or, log/error an exit Kibana if (isFipsConfigEnabled !== isNodeRunningWithFipsEnabled) { - logger.error( + throw new CriticalError( `Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to ${isFipsConfigEnabled} and the configured Node.js environment has FIPS ${ isNodeRunningWithFipsEnabled ? 'enabled' : 'disabled' - }` + }`, + 'invalidConfig', + 78 + ); + } else if (definedPKCS12ConfigOptions.length > 0) { + throw new CriticalError( + `Configuration mismatch error: ${definedPKCS12ConfigOptions.join(', ')} ${ + definedPKCS12ConfigOptions.length > 1 ? 'are' : 'is' + } set, PKCS12 configurations are not allowed while running in FIPS mode.`, + 'invalidConfig', + 78 ); - process.exit(78); } else { logger.info('Kibana is running in FIPS mode.'); } } } + +function findDefinedPKCS12ConfigOptions( + elasticsearchConfig: PKCS12ConfigType, + serverConfig: PKCS12ConfigType +): string[] { + const result = []; + if (elasticsearchConfig?.ssl?.keystore?.path) { + result.push('elasticsearch.ssl.keystore.path'); + } + + if (elasticsearchConfig?.ssl?.truststore?.path) { + result.push('elasticsearch.ssl.truststore.path'); + } + + if (serverConfig?.ssl?.keystore?.path) { + result.push('server.ssl.keystore.path'); + } + + if (serverConfig?.ssl?.truststore?.path) { + result.push('server.ssl.truststore.path'); + } + + return result; +} diff --git a/packages/core/security/core-security-server-internal/src/security_service.ts b/packages/core/security/core-security-server-internal/src/security_service.ts index cf39664bd46a0..81a337db47569 100644 --- a/packages/core/security/core-security-server-internal/src/security_service.ts +++ b/packages/core/security/core-security-server-internal/src/security_service.ts @@ -21,6 +21,7 @@ import { getDefaultSecurityImplementation, convertSecurityApi, SecurityServiceConfigType, + PKCS12ConfigType, } from './utils'; export class SecurityService @@ -50,8 +51,10 @@ export class SecurityService public setup(): InternalSecurityServiceSetup { const config = this.getConfig(); const securityConfig: SecurityServiceConfigType = config.get(['xpack', 'security']); + const elasticsearchConfig: PKCS12ConfigType = config.get(['elasticsearch']); + const serverConfig: PKCS12ConfigType = config.get(['server']); - checkFipsConfig(securityConfig, this.log); + checkFipsConfig(securityConfig, elasticsearchConfig, serverConfig, this.log); return { registerSecurityDelegate: (api) => { diff --git a/packages/core/security/core-security-server-internal/src/utils/index.ts b/packages/core/security/core-security-server-internal/src/utils/index.ts index 1e3a370057135..666afcce38afd 100644 --- a/packages/core/security/core-security-server-internal/src/utils/index.ts +++ b/packages/core/security/core-security-server-internal/src/utils/index.ts @@ -17,3 +17,14 @@ export interface SecurityServiceConfigType { }; }; } + +export interface PKCS12ConfigType { + ssl?: { + keystore?: { + path?: string; + }; + truststore?: { + path?: string; + }; + }; +} From a65e130a319acd8aea3f0c61c0c6b41dcf539f59 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:35:25 +1100 Subject: [PATCH 37/42] [8.x] [Security solution] `ChatBedrockConverse` (#200042) (#200817) # Backport This will backport the following commits from `main` to `8.x`: - [[Security solution] `ChatBedrockConverse` (#200042)](https://github.com/elastic/kibana/pull/200042) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Steph Milovic --- package.json | 8 +- x-pack/packages/kbn-langchain/server/index.ts | 2 + .../server/language_models/bedrock_chat.ts | 58 +- .../bedrock_runtime_client.ts | 37 + .../chat_bedrock_converse.ts | 50 + .../chat_bedrock_converse/index.ts | 10 + .../node_http_handler.test.ts | 125 ++ .../node_http_handler.ts | 88 ++ .../kbn-langchain/server/utils/bedrock.ts | 24 + .../connector_types.test.ts.snap | 1108 ++++++++++++++++- .../nodes/translations.ts | 2 +- .../elastic_assistant/server/routes/utils.ts | 4 +- .../plugins/elastic_assistant/server/types.ts | 4 +- .../rules/task/util/actions_client_chat.ts | 7 +- .../common/bedrock/constants.ts | 2 + .../stack_connectors/common/bedrock/schema.ts | 56 + .../stack_connectors/common/bedrock/types.ts | 4 + .../server/connector_types/bedrock/bedrock.ts | 81 +- yarn.lock | 968 +++++++++++++- 19 files changed, 2482 insertions(+), 156 deletions(-) create mode 100644 x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/bedrock_runtime_client.ts create mode 100644 x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/chat_bedrock_converse.ts create mode 100644 x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/index.ts create mode 100644 x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.test.ts create mode 100644 x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.ts diff --git a/package.json b/package.json index 355f1db6c8967..65f775f56bdc6 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "@appland/sql-parser": "^1.5.1", "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/util": "^5.2.0", + "@aws-sdk/client-bedrock-runtime": "^3.687.0", "@babel/runtime": "^7.24.7", "@dagrejs/dagre": "^1.1.4", "@dnd-kit/core": "^6.1.0", @@ -1014,7 +1015,8 @@ "@kbn/xstate-utils": "link:packages/kbn-xstate-utils", "@kbn/zod": "link:packages/kbn-zod", "@kbn/zod-helpers": "link:packages/kbn-zod-helpers", - "@langchain/community": "0.3.11", + "@langchain/aws": "^0.1.2", + "@langchain/community": "0.3.14", "@langchain/core": "^0.3.16", "@langchain/google-common": "^0.1.1", "@langchain/google-genai": "^0.1.2", @@ -1049,7 +1051,9 @@ "@slack/webhook": "^7.0.1", "@smithy/eventstream-codec": "^3.1.1", "@smithy/eventstream-serde-node": "^3.0.3", - "@smithy/protocol-http": "^4.0.2", + "@smithy/middleware-stack": "^3.0.10", + "@smithy/node-http-handler": "^3.3.1", + "@smithy/protocol-http": "^4.1.7", "@smithy/signature-v4": "^3.1.1", "@smithy/types": "^3.2.0", "@smithy/util-utf8": "^3.0.0", diff --git a/x-pack/packages/kbn-langchain/server/index.ts b/x-pack/packages/kbn-langchain/server/index.ts index 4ffe3aec864d6..ebd1c0e5b49d4 100644 --- a/x-pack/packages/kbn-langchain/server/index.ts +++ b/x-pack/packages/kbn-langchain/server/index.ts @@ -11,6 +11,7 @@ import { ActionsClientLlm } from './language_models/llm'; import { ActionsClientSimpleChatModel } from './language_models/simple_chat_model'; import { ActionsClientGeminiChatModel } from './language_models/gemini_chat'; import { ActionsClientChatVertexAI } from './language_models/chat_vertex'; +import { ActionsClientChatBedrockConverse } from './language_models/chat_bedrock_converse'; import { parseBedrockStream } from './utils/bedrock'; import { parseGeminiResponse } from './utils/gemini'; import { getDefaultArguments } from './language_models/constants'; @@ -25,4 +26,5 @@ export { ActionsClientGeminiChatModel, ActionsClientLlm, ActionsClientSimpleChatModel, + ActionsClientChatBedrockConverse, }; diff --git a/x-pack/packages/kbn-langchain/server/language_models/bedrock_chat.ts b/x-pack/packages/kbn-langchain/server/language_models/bedrock_chat.ts index ac229b97c8757..70395298d3c98 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/bedrock_chat.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/bedrock_chat.ts @@ -9,11 +9,8 @@ import { BedrockChat as _BedrockChat } from '@langchain/community/chat_models/be import type { ActionsClient } from '@kbn/actions-plugin/server'; import { BaseChatModelParams } from '@langchain/core/language_models/chat_models'; import { Logger } from '@kbn/logging'; -import { Readable } from 'stream'; import { PublicMethodsOf } from '@kbn/utility-types'; - -export const DEFAULT_BEDROCK_MODEL = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; -export const DEFAULT_BEDROCK_REGION = 'us-east-1'; +import { prepareMessages, DEFAULT_BEDROCK_MODEL, DEFAULT_BEDROCK_REGION } from '../utils/bedrock'; export interface CustomChatModelInput extends BaseChatModelParams { actionsClient: PublicMethodsOf; @@ -25,6 +22,11 @@ export interface CustomChatModelInput extends BaseChatModelParams { maxTokens?: number; } +/** + * @deprecated Use the ActionsClientChatBedrockConverse chat model instead. + * ActionsClientBedrockChatModel chat model supports non-streaming only the Bedrock Invoke API. + * The LangChain team will support only the Bedrock Converse API in the future. + */ export class ActionsClientBedrockChatModel extends _BedrockChat { constructor({ actionsClient, connectorId, logger, ...params }: CustomChatModelInput) { super({ @@ -36,32 +38,10 @@ export class ActionsClientBedrockChatModel extends _BedrockChat { fetchFn: async (url, options) => { const inputBody = JSON.parse(options?.body as string); - if (this.streaming && !inputBody.tools?.length) { - const data = (await actionsClient.execute({ - actionId: connectorId, - params: { - subAction: 'invokeStream', - subActionParams: { - messages: inputBody.messages, - temperature: params.temperature ?? inputBody.temperature, - stopSequences: inputBody.stop_sequences, - system: inputBody.system, - maxTokens: params.maxTokens ?? inputBody.max_tokens, - tools: inputBody.tools, - anthropicVersion: inputBody.anthropic_version, - }, - }, - })) as { data: Readable; status: string; message?: string; serviceMessage?: string }; - - if (data.status === 'error') { - throw new Error( - `ActionsClientBedrockChat: action result status is error: ${data?.message} - ${data?.serviceMessage}` - ); - } - - return { - body: Readable.toWeb(data.data), - } as unknown as Response; + if (this.streaming) { + throw new Error( + `ActionsClientBedrockChat does not support streaming, use ActionsClientChatBedrockConverse instead` + ); } const data = (await actionsClient.execute({ @@ -84,7 +64,6 @@ export class ActionsClientBedrockChatModel extends _BedrockChat { message?: string; serviceMessage?: string; }; - if (data.status === 'error') { throw new Error( `ActionsClientBedrockChat: action result status is error: ${data?.message} - ${data?.serviceMessage}` @@ -99,20 +78,3 @@ export class ActionsClientBedrockChatModel extends _BedrockChat { }); } } - -const prepareMessages = (messages: Array<{ role: string; content: string[] }>) => - messages.reduce((acc, { role, content }) => { - const lastMessage = acc[acc.length - 1]; - - if (!lastMessage || lastMessage.role !== role) { - acc.push({ role, content }); - return acc; - } - - if (lastMessage.role === role) { - acc[acc.length - 1].content = lastMessage.content.concat(content); - return acc; - } - - return acc; - }, [] as Array<{ role: string; content: string[] }>); diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/bedrock_runtime_client.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/bedrock_runtime_client.ts new file mode 100644 index 0000000000000..359342870a8b9 --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/bedrock_runtime_client.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 { + BedrockRuntimeClient as _BedrockRuntimeClient, + BedrockRuntimeClientConfig, +} from '@aws-sdk/client-bedrock-runtime'; +import { constructStack } from '@smithy/middleware-stack'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; + +import { NodeHttpHandler } from './node_http_handler'; + +export interface CustomChatModelInput extends BedrockRuntimeClientConfig { + actionsClient: PublicMethodsOf; + connectorId: string; + streaming?: boolean; +} + +export class BedrockRuntimeClient extends _BedrockRuntimeClient { + middlewareStack: _BedrockRuntimeClient['middlewareStack']; + + constructor({ actionsClient, connectorId, ...fields }: CustomChatModelInput) { + super(fields ?? {}); + this.config.requestHandler = new NodeHttpHandler({ + streaming: fields.streaming ?? true, + actionsClient, + connectorId, + }); + // eliminate middleware steps that handle auth as Kibana connector handles auth + this.middlewareStack = constructStack() as _BedrockRuntimeClient['middlewareStack']; + } +} diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/chat_bedrock_converse.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/chat_bedrock_converse.ts new file mode 100644 index 0000000000000..bdc84130925d6 --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/chat_bedrock_converse.ts @@ -0,0 +1,50 @@ +/* + * 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 { ChatBedrockConverse } from '@langchain/aws'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import { BaseChatModelParams } from '@langchain/core/language_models/chat_models'; +import { Logger } from '@kbn/logging'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { BedrockRuntimeClient } from './bedrock_runtime_client'; +import { DEFAULT_BEDROCK_MODEL, DEFAULT_BEDROCK_REGION } from '../../utils/bedrock'; + +export interface CustomChatModelInput extends BaseChatModelParams { + actionsClient: PublicMethodsOf; + connectorId: string; + logger: Logger; + signal?: AbortSignal; + model?: string; +} + +/** + * Custom chat model class for Bedrock Converse API. + * The ActionsClientChatBedrockConverse chat model supports streaming and + * non-streaming via the Bedrock Converse and ConverseStream APIs. + * + * @param {Object} params - The parameters for the chat model. + * @param {ActionsClient} params.actionsClient - The actions client. + * @param {string} params.connectorId - The connector ID. + * @param {Logger} params.logger - The logger instance. + * @param {AbortSignal} [params.signal] - Optional abort signal. + * @param {string} [params.model] - Optional model name. + */ +export class ActionsClientChatBedrockConverse extends ChatBedrockConverse { + constructor({ actionsClient, connectorId, logger, ...fields }: CustomChatModelInput) { + super({ + ...(fields ?? {}), + credentials: { accessKeyId: '', secretAccessKey: '' }, + model: fields?.model ?? DEFAULT_BEDROCK_MODEL, + region: DEFAULT_BEDROCK_REGION, + }); + this.client = new BedrockRuntimeClient({ + actionsClient, + connectorId, + streaming: this.streaming, + region: DEFAULT_BEDROCK_REGION, + }); + } +} diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/index.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/index.ts new file mode 100644 index 0000000000000..2d22184224166 --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/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. + */ + +import { ActionsClientChatBedrockConverse } from './chat_bedrock_converse'; + +export { ActionsClientChatBedrockConverse }; diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.test.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.test.ts new file mode 100644 index 0000000000000..ba8a1db1fbb00 --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { NodeHttpHandler } from './node_http_handler'; +import { HttpRequest } from '@smithy/protocol-http'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; +import { Readable } from 'stream'; +import { fromUtf8 } from '@smithy/util-utf8'; + +const mockActionsClient = actionsClientMock.create(); +const connectorId = 'mock-connector-id'; +const mockOutput = { + output: { + message: { + role: 'assistant', + content: [{ text: 'This is a response from the assistant.' }], + }, + }, + stopReason: 'end_turn', + usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }, + metrics: { latencyMs: 123 }, + additionalModelResponseFields: {}, + trace: { guardrail: { modelOutput: ['Output text'] } }, +}; +describe('NodeHttpHandler', () => { + let handler: NodeHttpHandler; + + beforeEach(() => { + jest.clearAllMocks(); + handler = new NodeHttpHandler({ + streaming: false, + actionsClient: mockActionsClient, + connectorId, + }); + + mockActionsClient.execute.mockResolvedValue({ + data: mockOutput, + actionId: 'mock-action-id', + status: 'ok', + }); + }); + + it('handles non-streaming requests successfully', async () => { + const request = new HttpRequest({ + body: JSON.stringify({ messages: [] }), + }); + + const result = await handler.handle(request); + + expect(result.response.statusCode).toBe(200); + expect(result.response.headers['content-type']).toBe('application/json'); + expect(result.response.body).toStrictEqual(fromUtf8(JSON.stringify(mockOutput))); + }); + + it('handles streaming requests successfully', async () => { + handler = new NodeHttpHandler({ + streaming: true, + actionsClient: mockActionsClient, + connectorId, + }); + + const request = new HttpRequest({ + body: JSON.stringify({ messages: [] }), + }); + + const readable = new Readable(); + readable.push('streaming data'); + readable.push(null); + + mockActionsClient.execute.mockResolvedValue({ + data: readable, + status: 'ok', + actionId: 'mock-action-id', + }); + + const result = await handler.handle(request); + + expect(result.response.statusCode).toBe(200); + expect(result.response.body).toBe(readable); + }); + + it('throws an error for non-streaming requests with error status', async () => { + const request = new HttpRequest({ + body: JSON.stringify({ messages: [] }), + }); + + mockActionsClient.execute.mockResolvedValue({ + status: 'error', + message: 'error message', + serviceMessage: 'service error message', + actionId: 'mock-action-id', + }); + + await expect(handler.handle(request)).rejects.toThrow( + 'ActionsClientBedrockChat: action result status is error: error message - service error message' + ); + }); + + it('throws an error for streaming requests with error status', async () => { + handler = new NodeHttpHandler({ + streaming: true, + actionsClient: mockActionsClient, + connectorId, + }); + + const request = new HttpRequest({ + body: JSON.stringify({ messages: [] }), + }); + + mockActionsClient.execute.mockResolvedValue({ + status: 'error', + message: 'error message', + serviceMessage: 'service error message', + actionId: 'mock-action-id', + }); + + await expect(handler.handle(request)).rejects.toThrow( + 'ActionsClientBedrockChat: action result status is error: error message - service error message' + ); + }); +}); diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.ts new file mode 100644 index 0000000000000..bd5143ef45d4a --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_bedrock_converse/node_http_handler.ts @@ -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 { NodeHttpHandler as _NodeHttpHandler } from '@smithy/node-http-handler'; +import { HttpRequest, HttpResponse } from '@smithy/protocol-http'; +import { HttpHandlerOptions, NodeHttpHandlerOptions } from '@smithy/types'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; +import { Readable } from 'stream'; +import { fromUtf8 } from '@smithy/util-utf8'; +import { ConverseResponse } from '@aws-sdk/client-bedrock-runtime'; +import { prepareMessages } from '../../utils/bedrock'; + +interface NodeHandlerOptions extends NodeHttpHandlerOptions { + streaming: boolean; + actionsClient: PublicMethodsOf; + connectorId: string; +} + +export class NodeHttpHandler extends _NodeHttpHandler { + streaming: boolean; + actionsClient: PublicMethodsOf; + connectorId: string; + constructor(options: NodeHandlerOptions) { + super(options); + this.streaming = options.streaming; + this.actionsClient = options.actionsClient; + this.connectorId = options.connectorId; + } + + async handle( + request: HttpRequest, + options: HttpHandlerOptions = {} + ): Promise<{ response: HttpResponse }> { + const body = JSON.parse(request.body); + const messages = prepareMessages(body.messages); + + if (this.streaming) { + const data = (await this.actionsClient.execute({ + actionId: this.connectorId, + params: { + subAction: 'converseStream', + subActionParams: { ...body, messages, signal: options.abortSignal }, + }, + })) as { data: Readable; status: string; message?: string; serviceMessage?: string }; + + if (data.status === 'error') { + throw new Error( + `ActionsClientBedrockChat: action result status is error: ${data?.message} - ${data?.serviceMessage}` + ); + } + + return { + response: { + statusCode: 200, + headers: {}, + body: data.data, + }, + }; + } + + const data = (await this.actionsClient.execute({ + actionId: this.connectorId, + params: { + subAction: 'converse', + subActionParams: { ...body, messages, signal: options.abortSignal }, + }, + })) as { data: ConverseResponse; status: string; message?: string; serviceMessage?: string }; + + if (data.status === 'error') { + throw new Error( + `ActionsClientBedrockChat: action result status is error: ${data?.message} - ${data?.serviceMessage}` + ); + } + + return { + response: { + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: fromUtf8(JSON.stringify(data.data)), + }, + }; + } +} diff --git a/x-pack/packages/kbn-langchain/server/utils/bedrock.ts b/x-pack/packages/kbn-langchain/server/utils/bedrock.ts index 39e5e77864fef..7c8c069e5eb5a 100644 --- a/x-pack/packages/kbn-langchain/server/utils/bedrock.ts +++ b/x-pack/packages/kbn-langchain/server/utils/bedrock.ts @@ -222,3 +222,27 @@ function parseContent(content: Array<{ text?: string; type: string }>): string { } return parsedContent; } + +/** + * Prepare messages for the bedrock API by combining messages from the same role + * @param messages + */ +export const prepareMessages = (messages: Array<{ role: string; content: string[] }>) => + messages.reduce((acc, { role, content }) => { + const lastMessage = acc[acc.length - 1]; + + if (!lastMessage || lastMessage.role !== role) { + acc.push({ role, content }); + return acc; + } + + if (lastMessage.role === role) { + acc[acc.length - 1].content = lastMessage.content.concat(content); + return acc; + } + + return acc; + }, [] as Array<{ role: string; content: string[] }>); + +export const DEFAULT_BEDROCK_MODEL = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; +export const DEFAULT_BEDROCK_REGION = 'us-east-1'; diff --git a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap index fad093938de40..936c5ba61b701 100644 --- a/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap +++ b/x-pack/plugins/actions/server/integration_tests/__snapshots__/connector_types.test.ts.snap @@ -10,6 +10,45 @@ Object { "presence": "optional", }, "keys": Object { + "apiType": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "allow": Array [ + "converse", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + Object { + "schema": Object { + "allow": Array [ + "invoke", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "alternatives", + }, "body": Object { "flags": Object { "error": [Function], @@ -131,6 +170,45 @@ Object { "presence": "optional", }, "keys": Object { + "apiType": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "matches": Array [ + Object { + "schema": Object { + "allow": Array [ + "converse", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + Object { + "schema": Object { + "allow": Array [ + "invoke", + ], + "flags": Object { + "error": [Function], + "only": true, + }, + "type": "any", + }, + }, + ], + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "alternatives", + }, "body": Object { "flags": Object { "error": [Function], @@ -1393,85 +1471,1019 @@ Object { "presence": "optional", }, "keys": Object { - "apiUrl": Object { + "additionalModelRequestFields": Object { "flags": Object { + "default": [Function], "error": [Function], + "presence": "optional", }, - "rules": Array [ + "metas": Array [ Object { - "args": Object { - "method": [Function], - }, - "name": "custom", + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, }, ], - "type": "string", + "type": "any", }, - "defaultModel": Object { + "additionalModelResponseFieldPaths": Object { "flags": Object { - "default": "anthropic.claude-3-5-sonnet-20240620-v1:0", + "default": [Function], "error": [Function], "presence": "optional", }, - "rules": Array [ + "metas": Array [ Object { - "args": Object { - "method": [Function], - }, - "name": "custom", + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, }, ], - "type": "string", - }, - }, - "type": "object", -} -`; - -exports[`Connector type config checks detect connector type changes for: .bedrock 8`] = ` -Object { - "flags": Object { - "default": Object { - "special": "deep", + "type": "any", }, - "error": [Function], - "presence": "optional", - }, - "keys": Object { - "accessKey": Object { + "guardrailConfig": Object { "flags": Object { + "default": [Function], "error": [Function], + "presence": "optional", }, - "rules": Array [ + "metas": Array [ Object { - "args": Object { - "method": [Function], - }, - "name": "custom", + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, }, ], - "type": "string", + "type": "any", }, - "secret": Object { + "inferenceConfig": Object { "flags": Object { + "default": Object { + "special": "deep", + }, "error": [Function], + "presence": "optional", }, - "rules": Array [ + "keys": Object { + "maxTokens": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "number", + }, + "stopSequences": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "items": Array [ + Object { + "flags": Object { + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + ], + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "array", + }, + "temperature": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "number", + }, + "topP": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "number", + }, + }, + "type": "object", + }, + "messages": Object { + "flags": Object { + "error": [Function], + }, + "items": Array [ Object { - "args": Object { - "method": [Function], + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", }, - "name": "custom", + "keys": Object { + "content": Object { + "flags": Object { + "error": [Function], + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + ], + "type": "any", + }, + "role": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", }, ], - "type": "string", + "type": "array", }, - }, - "type": "object", -} -`; - + "modelId": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "signal": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, + }, + ], + "type": "any", + }, + "system": Object { + "flags": Object { + "error": [Function], + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "text": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + ], + "type": "array", + }, + "toolConfig": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "toolChoice": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + "unknown": true, + }, + "keys": Object {}, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "preferences": Object { + "stripUnknown": Object { + "objects": false, + }, + }, + "type": "object", + }, + "tools": Object { + "flags": Object { + "error": [Function], + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "toolSpec": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "description": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "inputSchema": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "json": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "$schema": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "additionalProperties": Object { + "flags": Object { + "error": [Function], + }, + "type": "boolean", + }, + "properties": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + "unknown": true, + }, + "keys": Object {}, + "preferences": Object { + "stripUnknown": Object { + "objects": false, + }, + }, + "type": "object", + }, + "required": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "items": Array [ + Object { + "flags": Object { + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + ], + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "array", + }, + "type": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + "name": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + ], + "type": "array", + }, + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "object", + }, + }, + "type": "object", +} +`; + +exports[`Connector type config checks detect connector type changes for: .bedrock 8`] = ` +Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "additionalModelRequestFields": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, + }, + ], + "type": "any", + }, + "additionalModelResponseFieldPaths": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, + }, + ], + "type": "any", + }, + "guardrailConfig": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, + }, + ], + "type": "any", + }, + "inferenceConfig": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "maxTokens": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "number", + }, + "stopSequences": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "items": Array [ + Object { + "flags": Object { + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + ], + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "array", + }, + "temperature": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "number", + }, + "topP": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "number", + }, + }, + "type": "object", + }, + "messages": Object { + "flags": Object { + "error": [Function], + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "content": Object { + "flags": Object { + "error": [Function], + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + ], + "type": "any", + }, + "role": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + ], + "type": "array", + }, + "modelId": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "signal": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-any-type": true, + }, + Object { + "x-oas-optional": true, + }, + ], + "type": "any", + }, + "system": Object { + "flags": Object { + "error": [Function], + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "text": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + ], + "type": "array", + }, + "toolConfig": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "toolChoice": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + "unknown": true, + }, + "keys": Object {}, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "preferences": Object { + "stripUnknown": Object { + "objects": false, + }, + }, + "type": "object", + }, + "tools": Object { + "flags": Object { + "error": [Function], + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "toolSpec": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "description": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "inputSchema": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "json": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "$schema": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "additionalProperties": Object { + "flags": Object { + "error": [Function], + }, + "type": "boolean", + }, + "properties": Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + "unknown": true, + }, + "keys": Object {}, + "preferences": Object { + "stripUnknown": Object { + "objects": false, + }, + }, + "type": "object", + }, + "required": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "items": Array [ + Object { + "flags": Object { + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + ], + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "array", + }, + "type": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + "name": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", + }, + }, + "type": "object", + }, + ], + "type": "array", + }, + }, + "metas": Array [ + Object { + "x-oas-optional": true, + }, + ], + "type": "object", + }, + }, + "type": "object", +} +`; + exports[`Connector type config checks detect connector type changes for: .bedrock 9`] = ` +Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "apiUrl": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "defaultModel": Object { + "flags": Object { + "default": "anthropic.claude-3-5-sonnet-20240620-v1:0", + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", +} +`; + +exports[`Connector type config checks detect connector type changes for: .bedrock 10`] = ` +Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "accessKey": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "secret": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + }, + "type": "object", +} +`; + +exports[`Connector type config checks detect connector type changes for: .bedrock 11`] = ` Object { "flags": Object { "default": Object { diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts index e5a1c14846e23..aee78c16920d8 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts @@ -18,7 +18,7 @@ const BASE_GEMINI_PROMPT = const KB_CATCH = 'If the knowledge base tool gives empty results, do your best to answer the question from the perspective of an expert security analyst.'; export const GEMINI_SYSTEM_PROMPT = `${BASE_GEMINI_PROMPT} ${KB_CATCH}`; -export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return tags in the response, but make sure to include tags content in the response. Do not reflect on the quality of the returned search results in your response.`; +export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from NaturalLanguageESQLTool as is. Never return tags in the response, but make sure to include tags content in the response. Do not reflect on the quality of the returned search results in your response.`; export const GEMINI_USER_PROMPT = `Now, always using the tools at your disposal, step by step, come up with a response to this request:\n\n`; export const STRUCTURED_SYSTEM_PROMPT = `Respond to the human as helpfully and accurately as possible. ${KNOWLEDGE_HISTORY} You have access to the following tools: diff --git a/x-pack/plugins/elastic_assistant/server/routes/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/utils.ts index 54f9ef2c04b90..4cc213f0e0db8 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/utils.ts @@ -15,7 +15,7 @@ import type { } from '@kbn/core/server'; import { ActionsClientChatOpenAI, - ActionsClientBedrockChatModel, + ActionsClientChatBedrockConverse, ActionsClientChatVertexAI, } from '@kbn/langchain/server'; import { Connector } from '@kbn/actions-plugin/server/application/connector/types'; @@ -184,7 +184,7 @@ export const getLlmType = (actionTypeId: string): string | undefined => { export const getLlmClass = (llmType?: string) => { switch (llmType) { case 'bedrock': - return ActionsClientBedrockChatModel; + return ActionsClientChatBedrockConverse; case 'gemini': return ActionsClientChatVertexAI; case 'openai': diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts index d2dad4f9f998f..8b3b565a0c08c 100755 --- a/x-pack/plugins/elastic_assistant/server/types.ts +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -37,7 +37,7 @@ import { LicensingPluginStart, } from '@kbn/licensing-plugin/server'; import { - ActionsClientBedrockChatModel, + ActionsClientChatBedrockConverse, ActionsClientChatOpenAI, ActionsClientChatVertexAI, ActionsClientGeminiChatModel, @@ -208,7 +208,7 @@ export interface AssistantTool { } export type AssistantToolLlm = - | ActionsClientBedrockChatModel + | ActionsClientChatBedrockConverse | ActionsClientChatOpenAI | ActionsClientGeminiChatModel | ActionsClientChatVertexAI; diff --git a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts index 204978c901df6..1659862543078 100644 --- a/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts +++ b/x-pack/plugins/security_solution/server/lib/siem_migrations/rules/task/util/actions_client_chat.ts @@ -33,10 +33,7 @@ export type ActionsClientChatModelClass = export type ChatModelParams = Partial & Partial & Partial & - Partial & { - /** Enables the streaming mode of the response, disabled by default */ - streaming?: boolean; - }; + Partial; const llmTypeDictionary: Record = { [`.gen-ai`]: `openai`, @@ -67,7 +64,7 @@ export class ActionsClientChat { llmType, model: connector.config?.defaultModel, ...params, - streaming: params?.streaming ?? false, // disabling streaming by default, for some reason is enabled when omitted + streaming: false, // disabling streaming by default }); return model; } diff --git a/x-pack/plugins/stack_connectors/common/bedrock/constants.ts b/x-pack/plugins/stack_connectors/common/bedrock/constants.ts index f3b133dd783f6..d2ffa0b116bda 100644 --- a/x-pack/plugins/stack_connectors/common/bedrock/constants.ts +++ b/x-pack/plugins/stack_connectors/common/bedrock/constants.ts @@ -21,6 +21,8 @@ export enum SUB_ACTION { INVOKE_STREAM = 'invokeStream', DASHBOARD = 'getDashboard', TEST = 'test', + CONVERSE = 'converse', + CONVERSE_STREAM = 'converseStream', } export const DEFAULT_TIMEOUT_MS = 120000; diff --git a/x-pack/plugins/stack_connectors/common/bedrock/schema.ts b/x-pack/plugins/stack_connectors/common/bedrock/schema.ts index 15ac45c0cf597..c444159c010b2 100644 --- a/x-pack/plugins/stack_connectors/common/bedrock/schema.ts +++ b/x-pack/plugins/stack_connectors/common/bedrock/schema.ts @@ -26,6 +26,11 @@ export const RunActionParamsSchema = schema.object({ signal: schema.maybe(schema.any()), timeout: schema.maybe(schema.number()), raw: schema.maybe(schema.boolean()), + apiType: schema.maybe( + schema.oneOf([schema.literal('converse'), schema.literal('invoke')], { + defaultValue: 'invoke', + }) + ), }); export const BedrockMessageSchema = schema.object( @@ -148,3 +153,54 @@ export const DashboardActionParamsSchema = schema.object({ export const DashboardActionResponseSchema = schema.object({ available: schema.boolean(), }); + +export const ConverseActionParamsSchema = schema.object({ + // Bedrock API Properties + modelId: schema.maybe(schema.string()), + messages: schema.arrayOf( + schema.object({ + role: schema.string(), + content: schema.any(), + }) + ), + system: schema.arrayOf( + schema.object({ + text: schema.string(), + }) + ), + inferenceConfig: schema.object({ + temperature: schema.maybe(schema.number()), + maxTokens: schema.maybe(schema.number()), + stopSequences: schema.maybe(schema.arrayOf(schema.string())), + topP: schema.maybe(schema.number()), + }), + toolConfig: schema.maybe( + schema.object({ + tools: schema.arrayOf( + schema.object({ + toolSpec: schema.object({ + name: schema.string(), + description: schema.string(), + inputSchema: schema.object({ + json: schema.object({ + type: schema.string(), + properties: schema.object({}, { unknowns: 'allow' }), + required: schema.maybe(schema.arrayOf(schema.string())), + additionalProperties: schema.boolean(), + $schema: schema.maybe(schema.string()), + }), + }), + }), + }) + ), + toolChoice: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }) + ), + additionalModelRequestFields: schema.maybe(schema.any()), + additionalModelResponseFieldPaths: schema.maybe(schema.any()), + guardrailConfig: schema.maybe(schema.any()), + // Kibana related properties + signal: schema.maybe(schema.any()), +}); + +export const ConverseActionResponseSchema = schema.object({}, { unknowns: 'allow' }); diff --git a/x-pack/plugins/stack_connectors/common/bedrock/types.ts b/x-pack/plugins/stack_connectors/common/bedrock/types.ts index 9d742e5f892a8..e3dd49538176f 100644 --- a/x-pack/plugins/stack_connectors/common/bedrock/types.ts +++ b/x-pack/plugins/stack_connectors/common/bedrock/types.ts @@ -21,6 +21,8 @@ import { RunApiLatestResponseSchema, BedrockMessageSchema, BedrockToolChoiceSchema, + ConverseActionParamsSchema, + ConverseActionResponseSchema, } from './schema'; export type Config = TypeOf; @@ -37,3 +39,5 @@ export type DashboardActionParams = TypeOf; export type DashboardActionResponse = TypeOf; export type BedrockMessage = TypeOf; export type BedrockToolChoice = TypeOf; +export type ConverseActionParams = TypeOf; +export type ConverseActionResponse = TypeOf; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts index 9bd5c64404f64..55b631ba9441c 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts @@ -21,8 +21,9 @@ import { StreamingResponseSchema, RunActionResponseSchema, RunApiLatestResponseSchema, + ConverseActionParamsSchema, } from '../../../common/bedrock/schema'; -import type { +import { Config, Secrets, RunActionParams, @@ -34,6 +35,8 @@ import type { RunApiLatestResponse, BedrockMessage, BedrockToolChoice, + ConverseActionParams, + ConverseActionResponse, } from '../../../common/bedrock/types'; import { SUB_ACTION, @@ -103,6 +106,18 @@ export class BedrockConnector extends SubActionConnector { method: 'invokeAIRaw', schema: InvokeAIRawActionParamsSchema, }); + + this.registerSubAction({ + name: SUB_ACTION.CONVERSE, + method: 'converse', + schema: ConverseActionParamsSchema, + }); + + this.registerSubAction({ + name: SUB_ACTION.CONVERSE_STREAM, + method: 'converseStream', + schema: ConverseActionParamsSchema, + }); } protected getResponseErrorMessage(error: AxiosError<{ message?: string }>): string { @@ -222,14 +237,18 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B * responsible for making a POST request to the external API endpoint and returning the response data * @param body The stringified request body to be sent in the POST request. * @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used. + * @param signal Optional signal to cancel the request. + * @param timeout Optional timeout for the request. + * @param raw Optional flag to indicate if the response should be returned as raw data. + * @param apiType Optional type of API to be called. Defaults to 'invoke', . */ public async runApi( - { body, model: reqModel, signal, timeout, raw }: RunActionParams, + { body, model: reqModel, signal, timeout, raw, apiType = 'invoke' }: RunActionParams, connectorUsageCollector: ConnectorUsageCollector ): Promise { // set model on per request basis const currentModel = reqModel ?? this.model; - const path = `/model/${currentModel}/invoke`; + const path = `/model/${currentModel}/${apiType}`; const signed = this.signRequest(body, path, false); const requestArgs = { ...signed, @@ -262,18 +281,22 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B /** * NOT INTENDED TO BE CALLED DIRECTLY - * call invokeStream instead + * call invokeStream or converseStream instead * responsible for making a POST request to a specified URL with a given request body. * The response is then processed based on whether it is a streaming response or a regular response. * @param body The stringified request body to be sent in the POST request. * @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used. */ private async streamApi( - { body, model: reqModel, signal, timeout }: RunActionParams, + { body, model: reqModel, signal, timeout, apiType = 'invoke' }: RunActionParams, connectorUsageCollector: ConnectorUsageCollector ): Promise { + const streamingApiRoute = { + invoke: 'invoke-with-response-stream', + converse: 'converse-stream', + }; // set model on per request basis - const path = `/model/${reqModel ?? this.model}/invoke-with-response-stream`; + const path = `/model/${reqModel ?? this.model}/${streamingApiRoute[apiType]}`; const signed = this.signRequest(body, path, true); const response = await this.request( @@ -312,7 +335,7 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B timeout, tools, toolChoice, - }: InvokeAIActionParams | InvokeAIRawActionParams, + }: InvokeAIRawActionParams, connectorUsageCollector: ConnectorUsageCollector ): Promise { const res = (await this.streamApi( @@ -411,6 +434,50 @@ The Kibana Connector in use may need to be reconfigured with an updated Amazon B ); return res; } + + /** + * Sends a request to the Bedrock API to perform a conversation action. + * @param input - The parameters for the conversation action. + * @param connectorUsageCollector - The usage collector for the connector. + * @returns A promise that resolves to the response of the conversation action. + */ + public async converse( + { signal, ...converseApiInput }: ConverseActionParams, + connectorUsageCollector: ConnectorUsageCollector + ): Promise { + const res = await this.runApi( + { + body: JSON.stringify(converseApiInput), + raw: true, + apiType: 'converse', + signal, + }, + connectorUsageCollector + ); + return res; + } + + /** + * Sends a request to the Bedrock API to perform a streaming conversation action. + * @param input - The parameters for the streaming conversation action. + * @param connectorUsageCollector - The usage collector for the connector. + * @returns A promise that resolves to the streaming response of the conversation action. + */ + public async converseStream( + { signal, ...converseApiInput }: ConverseActionParams, + connectorUsageCollector: ConnectorUsageCollector + ): Promise { + const res = await this.streamApi( + { + body: JSON.stringify(converseApiInput), + apiType: 'converse', + signal, + }, + connectorUsageCollector + ); + + return res; + } } const formatBedrockBody = ({ diff --git a/yarn.lock b/yarn.lock index b7ef7370c62b5..71c573991a54a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -86,7 +86,20 @@ "@aws-sdk/types" "^3.222.0" tslib "^2.6.2" -"@aws-crypto/sha256-js@^5.2.0": +"@aws-crypto/sha256-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" + integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw== + dependencies: + "@aws-crypto/sha256-js" "^5.2.0" + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + +"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz#c4fdb773fdbed9a664fc1a95724e206cf3860042" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== @@ -95,6 +108,13 @@ "@aws-sdk/types" "^3.222.0" tslib "^2.6.2" +"@aws-crypto/supports-web-crypto@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz#a1e399af29269be08e695109aa15da0a07b5b5fb" + integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg== + dependencies: + tslib "^2.6.2" + "@aws-crypto/util@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" @@ -104,12 +124,517 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-sdk/types@^3.222.0": - version "3.577.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.577.0.tgz#7700784d368ce386745f8c340d9d68cea4716f90" - integrity sha512-FT2JZES3wBKN/alfmhlo+3ZOq/XJ0C7QOZcDNrpKjB0kqYoKjhVKZ/Hx6ArR0czkKfHzBBEs6y40ebIHx2nSmA== +"@aws-sdk/client-bedrock-agent-runtime@^3.616.0": + version "3.688.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-bedrock-agent-runtime/-/client-bedrock-agent-runtime-3.688.0.tgz#81769a896ff678d913e2838a554a9060ce3db3ab" + integrity sha512-ZaIX7nBQm2QIrl0TNgPtYvEJbMDUfFB1AT/ToKQ1IEKI3gc0tIgSdcxqorpXer+s50ZB3j9ITF4WCyhWnxfNSw== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/client-sso-oidc" "3.687.0" + "@aws-sdk/client-sts" "3.687.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/credential-provider-node" "3.687.0" + "@aws-sdk/middleware-host-header" "3.686.0" + "@aws-sdk/middleware-logger" "3.686.0" + "@aws-sdk/middleware-recursion-detection" "3.686.0" + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/region-config-resolver" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@aws-sdk/util-user-agent-browser" "3.686.0" + "@aws-sdk/util-user-agent-node" "3.687.0" + "@smithy/config-resolver" "^3.0.10" + "@smithy/core" "^2.5.1" + "@smithy/eventstream-serde-browser" "^3.0.11" + "@smithy/eventstream-serde-config-resolver" "^3.0.8" + "@smithy/eventstream-serde-node" "^3.0.10" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/hash-node" "^3.0.8" + "@smithy/invalid-dependency" "^3.0.8" + "@smithy/middleware-content-length" "^3.0.10" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-retry" "^3.0.25" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.25" + "@smithy/util-defaults-mode-node" "^3.0.25" + "@smithy/util-endpoints" "^2.1.4" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-bedrock-runtime@^3.602.0", "@aws-sdk/client-bedrock-runtime@^3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.687.0.tgz#9c08850b2cebe62da0682f76c7a5559e53829325" + integrity sha512-ayFDpIOXVOeY84CPo9KCY2emEPjLBNFT8TFeZeUjz8KiV+K0LwAKnkbLQkTweHFN2sq2pa7XqAPZ70xMjt5w3w== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/client-sso-oidc" "3.687.0" + "@aws-sdk/client-sts" "3.687.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/credential-provider-node" "3.687.0" + "@aws-sdk/middleware-host-header" "3.686.0" + "@aws-sdk/middleware-logger" "3.686.0" + "@aws-sdk/middleware-recursion-detection" "3.686.0" + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/region-config-resolver" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@aws-sdk/util-user-agent-browser" "3.686.0" + "@aws-sdk/util-user-agent-node" "3.687.0" + "@smithy/config-resolver" "^3.0.10" + "@smithy/core" "^2.5.1" + "@smithy/eventstream-serde-browser" "^3.0.11" + "@smithy/eventstream-serde-config-resolver" "^3.0.8" + "@smithy/eventstream-serde-node" "^3.0.10" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/hash-node" "^3.0.8" + "@smithy/invalid-dependency" "^3.0.8" + "@smithy/middleware-content-length" "^3.0.10" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-retry" "^3.0.25" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.25" + "@smithy/util-defaults-mode-node" "^3.0.25" + "@smithy/util-endpoints" "^2.1.4" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + "@smithy/util-stream" "^3.2.1" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-kendra@^3.352.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-kendra/-/client-kendra-3.687.0.tgz#b55cd41694fb49ae3d0c4a47401752c322b5bafb" + integrity sha512-NreNmI6OIcuRGgtmjXiceXwcf1TPUIdg+rlPJwLFrTi6ukIu+P9e28g2ggNtZQ9pYmyUilBl2XntLIKHqvQAnQ== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/client-sso-oidc" "3.687.0" + "@aws-sdk/client-sts" "3.687.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/credential-provider-node" "3.687.0" + "@aws-sdk/middleware-host-header" "3.686.0" + "@aws-sdk/middleware-logger" "3.686.0" + "@aws-sdk/middleware-recursion-detection" "3.686.0" + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/region-config-resolver" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@aws-sdk/util-user-agent-browser" "3.686.0" + "@aws-sdk/util-user-agent-node" "3.687.0" + "@smithy/config-resolver" "^3.0.10" + "@smithy/core" "^2.5.1" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/hash-node" "^3.0.8" + "@smithy/invalid-dependency" "^3.0.8" + "@smithy/middleware-content-length" "^3.0.10" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-retry" "^3.0.25" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.25" + "@smithy/util-defaults-mode-node" "^3.0.25" + "@smithy/util-endpoints" "^2.1.4" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + "@smithy/util-utf8" "^3.0.0" + "@types/uuid" "^9.0.1" + tslib "^2.6.2" + uuid "^9.0.1" + +"@aws-sdk/client-sso-oidc@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.687.0.tgz#a327cc65b7bb2cbda305c4467bfae452b5d27927" + integrity sha512-Rdd8kLeTeh+L5ZuG4WQnWgYgdv7NorytKdZsGjiag1D8Wv3PcJvPqqWdgnI0Og717BSXVoaTYaN34FyqFYSx6Q== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/credential-provider-node" "3.687.0" + "@aws-sdk/middleware-host-header" "3.686.0" + "@aws-sdk/middleware-logger" "3.686.0" + "@aws-sdk/middleware-recursion-detection" "3.686.0" + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/region-config-resolver" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@aws-sdk/util-user-agent-browser" "3.686.0" + "@aws-sdk/util-user-agent-node" "3.687.0" + "@smithy/config-resolver" "^3.0.10" + "@smithy/core" "^2.5.1" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/hash-node" "^3.0.8" + "@smithy/invalid-dependency" "^3.0.8" + "@smithy/middleware-content-length" "^3.0.10" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-retry" "^3.0.25" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.25" + "@smithy/util-defaults-mode-node" "^3.0.25" + "@smithy/util-endpoints" "^2.1.4" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sso@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.687.0.tgz#4c71b818e718f632aa3dd4047961bededa23e4a7" + integrity sha512-dfj0y9fQyX4kFill/ZG0BqBTLQILKlL7+O5M4F9xlsh2WNuV2St6WtcOg14Y1j5UODPJiJs//pO+mD1lihT5Kw== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/middleware-host-header" "3.686.0" + "@aws-sdk/middleware-logger" "3.686.0" + "@aws-sdk/middleware-recursion-detection" "3.686.0" + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/region-config-resolver" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@aws-sdk/util-user-agent-browser" "3.686.0" + "@aws-sdk/util-user-agent-node" "3.687.0" + "@smithy/config-resolver" "^3.0.10" + "@smithy/core" "^2.5.1" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/hash-node" "^3.0.8" + "@smithy/invalid-dependency" "^3.0.8" + "@smithy/middleware-content-length" "^3.0.10" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-retry" "^3.0.25" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.25" + "@smithy/util-defaults-mode-node" "^3.0.25" + "@smithy/util-endpoints" "^2.1.4" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sts@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.687.0.tgz#fcb837080b225c5820f08326e98db54e48606fb1" + integrity sha512-SQjDH8O4XCTtouuCVYggB0cCCrIaTzUZIkgJUpOsIEJBLlTbNOb/BZqUShAQw2o9vxr2rCeOGjAQOYPysW/Pmg== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/client-sso-oidc" "3.687.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/credential-provider-node" "3.687.0" + "@aws-sdk/middleware-host-header" "3.686.0" + "@aws-sdk/middleware-logger" "3.686.0" + "@aws-sdk/middleware-recursion-detection" "3.686.0" + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/region-config-resolver" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@aws-sdk/util-user-agent-browser" "3.686.0" + "@aws-sdk/util-user-agent-node" "3.687.0" + "@smithy/config-resolver" "^3.0.10" + "@smithy/core" "^2.5.1" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/hash-node" "^3.0.8" + "@smithy/invalid-dependency" "^3.0.8" + "@smithy/middleware-content-length" "^3.0.10" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-retry" "^3.0.25" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.25" + "@smithy/util-defaults-mode-node" "^3.0.25" + "@smithy/util-endpoints" "^2.1.4" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/core@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.686.0.tgz#106a3733c250094db15ba765386db4643f5613b6" + integrity sha512-Xt3DV4DnAT3v2WURwzTxWQK34Ew+iiLzoUoguvLaZrVMFOqMMrwVjP+sizqIaHp1j7rGmFcN5I8saXnsDLuQLA== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/core" "^2.5.1" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/property-provider" "^3.1.7" + "@smithy/protocol-http" "^4.1.5" + "@smithy/signature-v4" "^4.2.0" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/util-middleware" "^3.0.8" + fast-xml-parser "4.4.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.686.0.tgz#71ce2df0be065dacddd873d1be7426bc8c6038ec" + integrity sha512-osD7lPO8OREkgxPiTWmA1i6XEmOth1uW9HWWj/+A2YGCj1G/t2sHu931w4Qj9NWHYZtbTTXQYVRg+TErALV7nQ== + dependencies: + "@aws-sdk/core" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/property-provider" "^3.1.7" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-http@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.686.0.tgz#fe84ea67fea6bb61effc0f10b99a0c3e9378d6c3" + integrity sha512-xyGAD/f3vR/wssUiZrNFWQWXZvI4zRm2wpHhoHA1cC2fbRMNFYtFn365yw6dU7l00ZLcdFB1H119AYIUZS7xbw== + dependencies: + "@aws-sdk/core" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/property-provider" "^3.1.7" + "@smithy/protocol-http" "^4.1.5" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/util-stream" "^3.2.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-ini@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.687.0.tgz#adb7f3fe381767ad1a4aee352162630f7b5f54de" + integrity sha512-6d5ZJeZch+ZosJccksN0PuXv7OSnYEmanGCnbhUqmUSz9uaVX6knZZfHCZJRgNcfSqg9QC0zsFA/51W5HCUqSQ== + dependencies: + "@aws-sdk/core" "3.686.0" + "@aws-sdk/credential-provider-env" "3.686.0" + "@aws-sdk/credential-provider-http" "3.686.0" + "@aws-sdk/credential-provider-process" "3.686.0" + "@aws-sdk/credential-provider-sso" "3.687.0" + "@aws-sdk/credential-provider-web-identity" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/credential-provider-imds" "^3.2.4" + "@smithy/property-provider" "^3.1.7" + "@smithy/shared-ini-file-loader" "^3.1.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-node@3.687.0", "@aws-sdk/credential-provider-node@^3.600.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.687.0.tgz#46bd8014bb68913ad285aed01e6920083a42d056" + integrity sha512-Pqld8Nx11NYaBUrVk3bYiGGpLCxkz8iTONlpQWoVWFhSOzlO7zloNOaYbD2XgFjjqhjlKzE91drs/f41uGeCTA== + dependencies: + "@aws-sdk/credential-provider-env" "3.686.0" + "@aws-sdk/credential-provider-http" "3.686.0" + "@aws-sdk/credential-provider-ini" "3.687.0" + "@aws-sdk/credential-provider-process" "3.686.0" + "@aws-sdk/credential-provider-sso" "3.687.0" + "@aws-sdk/credential-provider-web-identity" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/credential-provider-imds" "^3.2.4" + "@smithy/property-provider" "^3.1.7" + "@smithy/shared-ini-file-loader" "^3.1.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.686.0.tgz#7b02591d9b81fb16288618ce23d3244496c1b538" + integrity sha512-sXqaAgyzMOc+dm4CnzAR5Q6S9OWVHyZjLfW6IQkmGjqeQXmZl24c4E82+w64C+CTkJrFLzH1VNOYp1Hy5gE6Qw== + dependencies: + "@aws-sdk/core" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/property-provider" "^3.1.7" + "@smithy/shared-ini-file-loader" "^3.1.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-sso@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.687.0.tgz#2e5704bdaa3c420c2a00a1316cdbdf57d78ae649" + integrity sha512-N1YCoE7DovIRF2ReyRrA4PZzF0WNi4ObPwdQQkVxhvSm7PwjbWxrfq7rpYB+6YB1Uq3QPzgVwUFONE36rdpxUQ== + dependencies: + "@aws-sdk/client-sso" "3.687.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/token-providers" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/property-provider" "^3.1.7" + "@smithy/shared-ini-file-loader" "^3.1.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-web-identity@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.686.0.tgz#228be45b2f840ebf227d96ee5e326c1efa3c25a9" + integrity sha512-40UqCpPxyHCXDP7CGd9JIOZDgDZf+u1OyLaGBpjQJlz1HYuEsIWnnbTe29Yg3Ah/Zc3g4NBWcUdlGVotlnpnDg== dependencies: - "@smithy/types" "^3.0.0" + "@aws-sdk/core" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@smithy/property-provider" "^3.1.7" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-host-header@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.686.0.tgz#16f0be33fc738968a4e10ff77cb8a04e2b2c2359" + integrity sha512-+Yc6rO02z+yhFbHmRZGvEw1vmzf/ifS9a4aBjJGeVVU+ZxaUvnk+IUZWrj4YQopUQ+bSujmMUzJLXSkbDq7yuw== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-logger@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.686.0.tgz#4e094e42e10bf17d43b9c9afc3fc594f4aa72e02" + integrity sha512-cX43ODfA2+SPdX7VRxu6gXk4t4bdVJ9pkktbfnkE5t27OlwNfvSGGhnHrQL8xTOFeyQ+3T+oowf26gf1OI+vIg== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-recursion-detection@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.686.0.tgz#aba097d2dcc9d3b9d4523d7ae03ac3b387617db1" + integrity sha512-jF9hQ162xLgp9zZ/3w5RUNhmwVnXDBlABEUX8jCgzaFpaa742qR/KKtjjZQ6jMbQnP+8fOCSXFAVNMU+s6v81w== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-user-agent@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.687.0.tgz#a5feb5466d2926cd1ef5dd6f4778b33ce160ca7f" + integrity sha512-nUgsKiEinyA50CaDXojAkOasAU3Apdg7Qox6IjNUC4ZjgOu7QWsCDB5N28AYMUt06cNYeYQdfMX1aEzG85a1Mg== + dependencies: + "@aws-sdk/core" "3.686.0" + "@aws-sdk/types" "3.686.0" + "@aws-sdk/util-endpoints" "3.686.0" + "@smithy/core" "^2.5.1" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/region-config-resolver@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.686.0.tgz#3ef61e2cd95eb0ae80ecd5eef284744eb0a76d7c" + integrity sha512-6zXD3bSD8tcsMAVVwO1gO7rI1uy2fCD3czgawuPGPopeLiPpo6/3FoUWCQzk2nvEhj7p9Z4BbjwZGSlRkVrXTw== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/types" "^3.6.0" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.8" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.686.0.tgz#c7733a0a079adc9404bd9d8fc4ff52edef0a123a" + integrity sha512-9oL4kTCSePFmyKPskibeiOXV6qavPZ63/kXM9Wh9V6dTSvBtLeNnMxqGvENGKJcTdIgtoqyqA6ET9u0PJ5IRIg== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/property-provider" "^3.1.7" + "@smithy/shared-ini-file-loader" "^3.1.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/types@3.686.0", "@aws-sdk/types@^3.222.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.686.0.tgz#01aa5307c727de9e69969c538f99ae8b53f1074f" + integrity sha512-xFnrb3wxOoJcW2Xrh63ZgFo5buIu9DF7bOHnwoUxHdNpUXicUh0AHw85TjXxyxIAd0d1psY/DU7QHoNI3OswgQ== + dependencies: + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@aws-sdk/util-endpoints@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.686.0.tgz#c9a621961b8efda6d82ab3523d673acb0629d6d0" + integrity sha512-7msZE2oYl+6QYeeRBjlDgxQUhq/XRky3cXE0FqLFs2muLS7XSuQEXkpOXB3R782ygAP6JX0kmBxPTLurRTikZg== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/types" "^3.6.0" + "@smithy/util-endpoints" "^2.1.4" + tslib "^2.6.2" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.679.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.679.0.tgz#8d5898624691e12ccbad839e103562002bbec85e" + integrity sha512-zKTd48/ZWrCplkXpYDABI74rQlbR0DNHs8nH95htfSLj9/mWRSwaGptoxwcihaq/77vi/fl2X3y0a1Bo8bt7RA== + dependencies: + tslib "^2.6.2" + +"@aws-sdk/util-user-agent-browser@3.686.0": + version "3.686.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.686.0.tgz#953ef68c1b54e02f9de742310f47c33452f088bc" + integrity sha512-YiQXeGYZegF1b7B2GOR61orhgv79qmI0z7+Agm3NXLO6hGfVV3kFUJbXnjtH1BgWo5hbZYW7HQ2omGb3dnb6Lg== + dependencies: + "@aws-sdk/types" "3.686.0" + "@smithy/types" "^3.6.0" + bowser "^2.11.0" + tslib "^2.6.2" + +"@aws-sdk/util-user-agent-node@3.687.0": + version "3.687.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.687.0.tgz#6bdc45c2ef776a86614b002867aef37fc6f45b41" + integrity sha512-idkP6ojSTZ4ek1pJ8wIN7r9U3KR5dn0IkJn3KQBXQ58LWjkRqLtft2vxzdsktWwhPKjjmIKl1S0kbvqLawf8XQ== + dependencies: + "@aws-sdk/middleware-user-agent" "3.687.0" + "@aws-sdk/types" "3.686.0" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/types" "^3.6.0" tslib "^2.6.2" "@babel/cli@^7.24.7": @@ -7338,10 +7863,22 @@ resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== -"@langchain/community@0.3.11": - version "0.3.11" - resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.11.tgz#cb0f188f4e72c00beb1efdbd1fc7d7f47b70e636" - integrity sha512-hgnqsgWAhfUj9Kp0y+FGxlKot/qJFxat9GfIPJSJU4ViN434PgeMAQK53tkGZ361E2Zoo1V4RoGlSw4AjJILiA== +"@langchain/aws@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@langchain/aws/-/aws-0.1.2.tgz#607ab6d2f87c07a64176e6341ae2e9f857027b95" + integrity sha512-1cQvv8XSbaZXceAbYexSm/8WLqfEJ4VF6qbf/XLwkpUKMFGqpSBA00+Bn5p8K/Ms+PyMguZrxVNqd6daqxhDBQ== + dependencies: + "@aws-sdk/client-bedrock-agent-runtime" "^3.616.0" + "@aws-sdk/client-bedrock-runtime" "^3.602.0" + "@aws-sdk/client-kendra" "^3.352.0" + "@aws-sdk/credential-provider-node" "^3.600.0" + zod "^3.23.8" + zod-to-json-schema "^3.22.5" + +"@langchain/community@0.3.14": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.14.tgz#33c9c907f2a8cccc0af7fdeab50b2b69d85321ac" + integrity sha512-zadvK0pu15Jp028VEV4wV+lYB1ViojSolSdSNMdE82KuaK97kH/F1aynQ2W+ebHzjr0lG3dUF3OfOqHU37VgwA== dependencies: "@langchain/openai" ">=0.2.0 <0.4.0" binary-extensions "^2.2.0" @@ -8706,32 +9243,122 @@ "@types/node" ">=18.0.0" axios "^1.6.0" -"@smithy/eventstream-codec@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-3.1.1.tgz#b47f30bf4ad791ac7981b9fff58e599d18269cf9" - integrity sha512-s29NxV/ng1KXn6wPQ4qzJuQDjEtxLdS0+g5PQFirIeIZrp66FXVJ5IpZRowbt/42zB5dY8TqJ0G0L9KkgtsEZg== +"@smithy/abort-controller@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-3.1.8.tgz#ce0c10ddb2b39107d70b06bbb8e4f6e368bc551d" + integrity sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/config-resolver@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-3.0.10.tgz#d9529d9893e5fae1f14cb1ffd55517feb6d7e50f" + integrity sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw== + dependencies: + "@smithy/node-config-provider" "^3.1.9" + "@smithy/types" "^3.6.0" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.8" + tslib "^2.6.2" + +"@smithy/core@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-2.5.1.tgz#7f635b76778afca845bcb401d36f22fa37712f15" + integrity sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg== + dependencies: + "@smithy/middleware-serde" "^3.0.8" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-stream" "^3.2.1" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/credential-provider-imds@^3.2.4", "@smithy/credential-provider-imds@^3.2.5": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz#dbfd849a4a7ebd68519cd9fc35f78d091e126d0a" + integrity sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg== + dependencies: + "@smithy/node-config-provider" "^3.1.9" + "@smithy/property-provider" "^3.1.8" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + tslib "^2.6.2" + +"@smithy/eventstream-codec@^3.1.1", "@smithy/eventstream-codec@^3.1.7": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-3.1.7.tgz#5bfaffbc83ae374ffd85a755a8200ba3c7aed016" + integrity sha512-kVSXScIiRN7q+s1x7BrQtZ1Aa9hvvP9FeCqCdBxv37GimIHgBCOnZ5Ip80HLt0DhnAKpiobFdGqTFgbaJNrazA== dependencies: "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^3.2.0" + "@smithy/types" "^3.6.0" "@smithy/util-hex-encoding" "^3.0.0" tslib "^2.6.2" -"@smithy/eventstream-serde-node@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.3.tgz#51df0ca39f453d78a3d6607c1ac2e96cf900c824" - integrity sha512-v61Ftn7x/ubWFqH7GHFAL/RaU7QZImTbuV95DYugYYItzpO7KaHYEuO8EskCaBpZEfzOxhUGKm4teS9YUSt69Q== +"@smithy/eventstream-serde-browser@^3.0.11": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.11.tgz#019f3d1016d893b65ef6efec8c5e2fa925d0ac3d" + integrity sha512-Pd1Wnq3CQ/v2SxRifDUihvpXzirJYbbtXfEnnLV/z0OGCTx/btVX74P86IgrZkjOydOASBGXdPpupYQI+iO/6A== dependencies: - "@smithy/eventstream-serde-universal" "^3.0.3" - "@smithy/types" "^3.2.0" + "@smithy/eventstream-serde-universal" "^3.0.10" + "@smithy/types" "^3.6.0" tslib "^2.6.2" -"@smithy/eventstream-serde-universal@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.3.tgz#2ecac479ba84e10221b4b70545f3d7a223b5345e" - integrity sha512-YXYt3Cjhu9tRrahbTec2uOjwOSeCNfQurcWPGNEUspBhqHoA3KrDrVj+jGbCLWvwkwhzqDnnaeHAxm+IxAjOAQ== +"@smithy/eventstream-serde-config-resolver@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.8.tgz#bba17a358818e61993aaa73e36ea4023c5805556" + integrity sha512-zkFIG2i1BLbfoGQnf1qEeMqX0h5qAznzaZmMVNnvPZz9J5AWBPkOMckZWPedGUPcVITacwIdQXoPcdIQq5FRcg== dependencies: - "@smithy/eventstream-codec" "^3.1.1" - "@smithy/types" "^3.2.0" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/eventstream-serde-node@^3.0.10", "@smithy/eventstream-serde-node@^3.0.3": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.10.tgz#da40b872001390bb47807186855faba8172b3b5b" + integrity sha512-hjpU1tIsJ9qpcoZq9zGHBJPBOeBGYt+n8vfhDwnITPhEre6APrvqq/y3XMDEGUT2cWQ4ramNqBPRbx3qn55rhw== + dependencies: + "@smithy/eventstream-serde-universal" "^3.0.10" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/eventstream-serde-universal@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.10.tgz#b24e66fec9ec003eb0a1d6733fa22ded43129281" + integrity sha512-ewG1GHbbqsFZ4asaq40KmxCmXO+AFSM1b+DcO2C03dyJj/ZH71CiTg853FSE/3SHK9q3jiYQIFjlGSwfxQ9kww== + dependencies: + "@smithy/eventstream-codec" "^3.1.7" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/fetch-http-handler@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz#3763cb5178745ed630ed5bc3beb6328abdc31f36" + integrity sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g== + dependencies: + "@smithy/protocol-http" "^4.1.5" + "@smithy/querystring-builder" "^3.0.8" + "@smithy/types" "^3.6.0" + "@smithy/util-base64" "^3.0.0" + tslib "^2.6.2" + +"@smithy/hash-node@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-3.0.8.tgz#f451cc342f74830466b0b39bf985dc3022634065" + integrity sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng== + dependencies: + "@smithy/types" "^3.6.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/invalid-dependency@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz#4d381a4c24832371ade79e904a72c173c9851e5f" + integrity sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q== + dependencies: + "@smithy/types" "^3.6.0" tslib "^2.6.2" "@smithy/is-array-buffer@^2.0.0": @@ -8748,12 +9375,127 @@ dependencies: tslib "^2.6.2" -"@smithy/protocol-http@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-4.0.2.tgz#502ed3116cb0f1e3f207881df965bac620ccb2da" - integrity sha512-X/90xNWIOqSR2tLUyWxVIBdatpm35DrL44rI/xoeBWUuanE0iyCXJpTcnqlOpnEzgcu0xCKE06+g70TTu2j7RQ== +"@smithy/middleware-content-length@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz#738266f6d81436d7e3a86bea931bc64e04ae7dbf" + integrity sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg== dependencies: - "@smithy/types" "^3.2.0" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/middleware-endpoint@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz#b9ee42d29d8f3a266883d293c4d6a586f7b60979" + integrity sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA== + dependencies: + "@smithy/core" "^2.5.1" + "@smithy/middleware-serde" "^3.0.8" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/shared-ini-file-loader" "^3.1.9" + "@smithy/types" "^3.6.0" + "@smithy/url-parser" "^3.0.8" + "@smithy/util-middleware" "^3.0.8" + tslib "^2.6.2" + +"@smithy/middleware-retry@^3.0.25": + version "3.0.25" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz#a6b1081fc1a0991ffe1d15e567e76198af01f37c" + integrity sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg== + dependencies: + "@smithy/node-config-provider" "^3.1.9" + "@smithy/protocol-http" "^4.1.5" + "@smithy/service-error-classification" "^3.0.8" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-retry" "^3.0.8" + tslib "^2.6.2" + uuid "^9.0.1" + +"@smithy/middleware-serde@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz#a46d10dba3c395be0d28610d55c89ff8c07c0cd3" + integrity sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA== + dependencies: + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/middleware-stack@^3.0.10", "@smithy/middleware-stack@^3.0.8": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-3.0.10.tgz#73e2fde5d151440844161773a17ee13375502baf" + integrity sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/node-config-provider@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz#d27ba8e4753f1941c24ed0af824dbc6c492f510a" + integrity sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew== + dependencies: + "@smithy/property-provider" "^3.1.8" + "@smithy/shared-ini-file-loader" "^3.1.9" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/node-http-handler@^3.2.5", "@smithy/node-http-handler@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-3.3.1.tgz#788fc1c22c21a0cf982f4025ccf9f64217f3164f" + integrity sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg== + dependencies: + "@smithy/abort-controller" "^3.1.8" + "@smithy/protocol-http" "^4.1.7" + "@smithy/querystring-builder" "^3.0.10" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/property-provider@^3.1.7", "@smithy/property-provider@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-3.1.8.tgz#b1c5a3949effbb9772785ad7ddc5b4b235b10fbe" + integrity sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA== + dependencies: + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/protocol-http@^4.1.5", "@smithy/protocol-http@^4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-4.1.7.tgz#5c67e62beb5deacdb94f2127f9a344bdf1b2ed6e" + integrity sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/querystring-builder@^3.0.10", "@smithy/querystring-builder@^3.0.8": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-3.0.10.tgz#db8773af85ee3977c82b8e35a5cdd178c621306d" + integrity sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ== + dependencies: + "@smithy/types" "^3.7.1" + "@smithy/util-uri-escape" "^3.0.0" + tslib "^2.6.2" + +"@smithy/querystring-parser@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz#057a8e2d301eea8eac7071923100ba38a824d7df" + integrity sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg== + dependencies: + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/service-error-classification@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz#265ad2573b972f6c7bdd1ad6c5155a88aeeea1c4" + integrity sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g== + dependencies: + "@smithy/types" "^3.6.0" + +"@smithy/shared-ini-file-loader@^3.1.8", "@smithy/shared-ini-file-loader@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz#1b77852b5bb176445e1d80333fa3f739313a4928" + integrity sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA== + dependencies: + "@smithy/types" "^3.6.0" tslib "^2.6.2" "@smithy/signature-v4@^3.1.1": @@ -8769,10 +9511,69 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/types@^3.0.0", "@smithy/types@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.2.0.tgz#1350fe8a50d5e35e12ffb34be46d946860b2b5ab" - integrity sha512-cKyeKAPazZRVqm7QPvcPD2jEIt2wqDPAL1KJKb0f/5I7uhollvsWZuZKLclmyP6a+Jwmr3OV3t+X0pZUUHS9BA== +"@smithy/signature-v4@^4.2.0": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-4.2.1.tgz#a918fd7d99af9f60aa07617506fa54be408126ee" + integrity sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg== + dependencies: + "@smithy/is-array-buffer" "^3.0.0" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/util-middleware" "^3.0.8" + "@smithy/util-uri-escape" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/smithy-client@^3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-3.4.2.tgz#a6e3ed98330ce170cf482e765bd0c21e0fde8ae4" + integrity sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA== + dependencies: + "@smithy/core" "^2.5.1" + "@smithy/middleware-endpoint" "^3.2.1" + "@smithy/middleware-stack" "^3.0.8" + "@smithy/protocol-http" "^4.1.5" + "@smithy/types" "^3.6.0" + "@smithy/util-stream" "^3.2.1" + tslib "^2.6.2" + +"@smithy/types@^3.2.0", "@smithy/types@^3.6.0", "@smithy/types@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.7.1.tgz#4af54c4e28351e9101996785a33f2fdbf93debe7" + integrity sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA== + dependencies: + tslib "^2.6.2" + +"@smithy/url-parser@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-3.0.8.tgz#8057d91d55ba8df97d74576e000f927b42da9e18" + integrity sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg== + dependencies: + "@smithy/querystring-parser" "^3.0.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/util-base64@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-3.0.0.tgz#f7a9a82adf34e27a72d0719395713edf0e493017" + integrity sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ== + dependencies: + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/util-body-length-browser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz#86ec2f6256310b4845a2f064e2f571c1ca164ded" + integrity sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ== + dependencies: + tslib "^2.6.2" + +"@smithy/util-body-length-node@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz#99a291bae40d8932166907fe981d6a1f54298a6d" + integrity sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA== dependencies: tslib "^2.6.2" @@ -8792,6 +9593,46 @@ "@smithy/is-array-buffer" "^3.0.0" tslib "^2.6.2" +"@smithy/util-config-provider@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz#62c6b73b22a430e84888a8f8da4b6029dd5b8efe" + integrity sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ== + dependencies: + tslib "^2.6.2" + +"@smithy/util-defaults-mode-browser@^3.0.25": + version "3.0.25" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz#ef9b84272d1db23503ff155f9075a4543ab6dab7" + integrity sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA== + dependencies: + "@smithy/property-provider" "^3.1.8" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + bowser "^2.11.0" + tslib "^2.6.2" + +"@smithy/util-defaults-mode-node@^3.0.25": + version "3.0.25" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz#c16fe3995c8e90ae318e336178392173aebe1e37" + integrity sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g== + dependencies: + "@smithy/config-resolver" "^3.0.10" + "@smithy/credential-provider-imds" "^3.2.5" + "@smithy/node-config-provider" "^3.1.9" + "@smithy/property-provider" "^3.1.8" + "@smithy/smithy-client" "^3.4.2" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/util-endpoints@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz#a29134c2b1982442c5fc3be18d9b22796e8eb964" + integrity sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ== + dependencies: + "@smithy/node-config-provider" "^3.1.9" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + "@smithy/util-hex-encoding@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz#32938b33d5bf2a15796cd3f178a55b4155c535e6" @@ -8799,12 +9640,35 @@ dependencies: tslib "^2.6.2" -"@smithy/util-middleware@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-3.0.2.tgz#6daeb9db060552d851801cd7a0afd68769e2f98b" - integrity sha512-7WW5SD0XVrpfqljBYzS5rLR+EiDzl7wCVJZ9Lo6ChNFV4VYDk37Z1QI5w/LnYtU/QKnSawYoHRd7VjSyC8QRQQ== +"@smithy/util-middleware@^3.0.2", "@smithy/util-middleware@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-3.0.8.tgz#372bc7a2845408ad69da039d277fc23c2734d0c6" + integrity sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA== dependencies: - "@smithy/types" "^3.2.0" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/util-retry@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-3.0.8.tgz#9c607c175a4d8a87b5d8ebaf308f6b849e4dc4d0" + integrity sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow== + dependencies: + "@smithy/service-error-classification" "^3.0.8" + "@smithy/types" "^3.6.0" + tslib "^2.6.2" + +"@smithy/util-stream@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-3.2.1.tgz#f3055dc4c8caba8af4e47191ea7e773d0e5a429d" + integrity sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A== + dependencies: + "@smithy/fetch-http-handler" "^4.0.0" + "@smithy/node-http-handler" "^3.2.5" + "@smithy/types" "^3.6.0" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" "@smithy/util-uri-escape@^3.0.0": @@ -11576,6 +12440,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== +"@types/uuid@^9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/vinyl-fs@*", "@types/vinyl-fs@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-3.0.2.tgz#cbaef5160ad7695483af0aa1b4fe67f166c18feb" @@ -13564,6 +14433,11 @@ bowser@^1.7.3: resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + boxen@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" @@ -17918,6 +18792,13 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== +fast-xml-parser@4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== + dependencies: + strnum "^1.0.5" + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -29454,6 +30335,11 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + style-loader@^1.1.3, style-loader@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e" From 2298269001507903118c43b2c5e4297fcf04be93 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:13:50 +1100 Subject: [PATCH 38/42] [8.x] [ML] Migrate influencers list from SCSS to Emotion (#200019) (#200824) # Backport This will backport the following commits from `main` to `8.x`: - [[ML] Migrate influencers list from SCSS to Emotion (#200019)](https://github.com/elastic/kibana/pull/200019) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Robert Jaszczurek <92210485+rbrtj@users.noreply.github.com> --- .../plugins/ml/public/application/_index.scss | 1 - .../components/influencers_list/_index.scss | 1 - .../influencers_list/_influencers_list.scss | 109 ------------------ .../influencers_list/influencers_list.tsx | 27 +++-- .../influencers_list_styles.ts | 90 +++++++++++++++ 5 files changed, 103 insertions(+), 125 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/influencers_list/_index.scss delete mode 100644 x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss create mode 100644 x-pack/plugins/ml/public/application/components/influencers_list/influencers_list_styles.ts diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss index 029a422afaa9f..91201434b20b1 100644 --- a/x-pack/plugins/ml/public/application/_index.scss +++ b/x-pack/plugins/ml/public/application/_index.scss @@ -11,7 +11,6 @@ @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly @import 'components/entity_cell/index'; - @import 'components/influencers_list/index'; @import 'components/job_selector/index'; @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly diff --git a/x-pack/plugins/ml/public/application/components/influencers_list/_index.scss b/x-pack/plugins/ml/public/application/components/influencers_list/_index.scss deleted file mode 100644 index 90ff743d162f0..0000000000000 --- a/x-pack/plugins/ml/public/application/components/influencers_list/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'influencers_list'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss b/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss deleted file mode 100644 index 1b091e4046c50..0000000000000 --- a/x-pack/plugins/ml/public/application/components/influencers_list/_influencers_list.scss +++ /dev/null @@ -1,109 +0,0 @@ -.ml-influencers-list { - line-height: 1.45; // SASSTODO: Calc proper value - - .field-label { - font-size: $euiFontSizeXS; - text-align: left; - max-height: $euiFontSizeS; - max-width: calc(100% - 102px); // SASSTODO: Calc proper value - - .field-value { - @include euiTextTruncate; - display: inline-block; - vertical-align: bottom; - } - } - - .progress { - display: inline-block; - width: calc(100% - 34px); // SASSTODO: Calc proper value - height: 22px; - min-width: 70px; - margin-bottom: 0; - color: $euiColorDarkShade; - background-color: transparent; - - .progress-bar-holder { - width: calc(100% - 28px); // SASSTODO: Calc proper value - } - - .progress-bar { - height: calc($euiSizeXS / 2); - margin-top: $euiSizeM; - text-align: right; - line-height: 18px; // SASSTODO: Calc proper value - display: inline-block; - transition: none; - } - } - - // SASSTODO: This range of color is too large, needs to be rewritten and variablized - .progress.critical { - .progress-bar { - background-color: $mlColorCritical; - } - - .score-label { - border-color: $mlColorCritical; - } - } - - .progress.major { - .progress-bar { - background-color: $mlColorMajor; - } - - .score-label { - border-color: $mlColorMajor; - } - } - - .progress.minor { - .progress-bar { - background-color: $mlColorMinor; - } - - .score-label { - border-color: $mlColorMinor; - } - } - - .progress.warning { - .progress-bar { - background-color: $mlColorWarning; - } - - .score-label { - border-color: $mlColorWarning; - } - } - - .score-label { - text-align: center; - line-height: 14px; - white-space: nowrap; - font-size: $euiFontSizeXS; - display: inline; - margin-left: $euiSizeXS; - } - - // SASSTODO: Brittle sizing - .total-score-label { - width: $euiSizeXL; - vertical-align: top; - text-align: center; - color: $euiColorDarkShade; - font-size: 11px; - line-height: 14px; - border-radius: $euiBorderRadius; - padding: calc($euiSizeXS / 2); - margin-top: $euiSizeXS; - display: inline-block; - border: $euiBorderThin; - } -} - -// SASSTODO: Can .eui-textBreakAll -.ml-influencers-list-tooltip { - word-break: break-all; -} diff --git a/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list.tsx b/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list.tsx index 39556dfe6a0f4..35f3bb83ebb10 100644 --- a/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list.tsx +++ b/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list.tsx @@ -19,6 +19,7 @@ import { getSeverity, getFormattedSeverityScore } from '@kbn/ml-anomaly-utils'; import { abbreviateWholeNumber } from '../../formatters/abbreviate_whole_number'; import type { EntityCellFilter } from '../entity_cell'; import { EntityCell } from '../entity_cell'; +import { useInfluencersListStyles } from './influencers_list_styles'; export interface InfluencerValueData { influencerFieldValue: string; @@ -65,6 +66,7 @@ function getTooltipContent(maxScoreLabel: string, totalScoreLabel: string) { } const Influencer: FC = ({ influencerFieldName, influencerFilter, valueData }) => { + const styles = useInfluencersListStyles(); const maxScore = Math.floor(valueData.maxAnomalyScore); const maxScoreLabel = getFormattedSeverityScore(valueData.maxAnomalyScore); const severity = getSeverity(maxScore); @@ -73,29 +75,25 @@ const Influencer: FC = ({ influencerFieldName, influencerFilter // Ensure the bar has some width for 0 scores. const barScore = maxScore !== 0 ? maxScore : 1; - const barStyle = { - width: `${barScore}%`, - }; const tooltipContent = getTooltipContent(maxScoreLabel, totalScoreLabel); return (
-
+
-
-
-
+
+
+
-
+
@@ -103,10 +101,9 @@ const Influencer: FC = ({ influencerFieldName, influencerFilter
-
+
@@ -145,12 +142,14 @@ const InfluencersByName: FC = ({ }; export const InfluencersList: FC = ({ influencers, influencerFilter }) => { + const styles = useInfluencersListStyles(); + if (influencers === undefined || Object.keys(influencers).length === 0) { return ( - + - +

= ({ influencers, influen /> )); - return
{influencersByName}
; + return
{influencersByName}
; }; diff --git a/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list_styles.ts b/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list_styles.ts new file mode 100644 index 0000000000000..5a0732ceb8d70 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/influencers_list/influencers_list_styles.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 { css } from '@emotion/react'; +import { useCurrentEuiThemeVars } from '@kbn/ml-kibana-theme'; +import { mlColors } from '../../styles'; +import { useMlKibana } from '../../contexts/kibana'; + +export const useInfluencersListStyles = () => { + const { + services: { theme }, + } = useMlKibana(); + const { euiTheme } = useCurrentEuiThemeVars(theme); + + return { + influencersList: css({ + lineHeight: 1.45, + }), + fieldLabel: css({ + fontSize: euiTheme.euiFontSizeXS, + textAlign: 'left', + maxHeight: euiTheme.euiFontSizeS, + maxWidth: 'calc(100% - 102px)', + }), + progress: css({ + display: 'inline-block', + width: 'calc(100% - 34px)', + height: '22px', + minWidth: '70px', + marginBottom: 0, + color: euiTheme.euiColorDarkShade, + backgroundColor: 'transparent', + }), + progressBarHolder: css({ + width: `calc(100% - 28px)`, + }), + progressBar: (severity: string, barScore: number) => + css({ + height: `calc(${euiTheme.euiSizeXS} / 2)`, + float: 'left', + marginTop: euiTheme.euiSizeM, + textAlign: 'right', + lineHeight: '18px', + display: 'inline-block', + transition: 'none', + width: `${barScore}%`, + backgroundColor: + severity === 'critical' + ? mlColors.critical + : severity === 'major' + ? mlColors.major + : severity === 'minor' + ? mlColors.minor + : mlColors.warning, + }), + scoreLabel: (severity: string) => + css({ + textAlign: 'center', + lineHeight: '14px', + whiteSpace: 'nowrap', + fontSize: euiTheme.euiFontSizeXS, + marginLeft: euiTheme.euiSizeXS, + display: 'inline', + borderColor: + severity === 'critical' + ? mlColors.critical + : severity === 'major' + ? mlColors.major + : severity === 'minor' + ? mlColors.minor + : mlColors.warning, + }), + totalScoreLabel: css({ + width: euiTheme.euiSizeXL, + verticalAlign: 'top', + textAlign: 'center', + color: euiTheme.euiColorDarkShade, + fontSize: '11px', + lineHeight: '14px', + borderRadius: euiTheme.euiBorderRadius, + padding: `calc(${euiTheme.euiSizeXS} / 2)`, + display: 'inline-block', + border: euiTheme.euiBorderThin, + }), + }; +}; From 5f95241e486670c7b0828107d0442b3021d2ab02 Mon Sep 17 00:00:00 2001 From: Kurt Date: Tue, 19 Nov 2024 19:15:25 -0500 Subject: [PATCH 39/42] [8.x] Removing experimental for the FIPS mode config (#200734) (#200825) # Backport This will backport the following commits from `main` to `8.x`: - [Removing experimental for the FIPS mode config (#200734)](https://github.com/elastic/kibana/pull/200734) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- .buildkite/scripts/common/env.sh | 2 +- .devcontainer/scripts/env.sh | 4 +-- docs/user/security/fips-140-2.asciidoc | 7 +---- .../src/fips/fips.test.ts | 28 +++++++++---------- .../src/fips/fips.ts | 4 +-- .../src/security_service.test.ts | 6 ++-- .../src/utils/index.ts | 6 ++-- .../src/create_root.ts | 2 +- .../integration_tests/node/migrator.test.ts | 2 +- .../resources/base/bin/kibana-docker | 2 +- .../templates/base/Dockerfile | 4 +-- x-pack/plugins/security/server/config.test.ts | 24 ++++++---------- x-pack/plugins/security/server/config.ts | 6 ++-- .../server/config_deprecations.test.ts | 22 +++++++++++++++ .../security/server/config_deprecations.ts | 3 ++ .../security/server/fips/fips_service.test.ts | 26 ++++++++--------- .../security/server/fips/fips_service.ts | 4 +-- 17 files changed, 79 insertions(+), 73 deletions(-) diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 511f6ead2d43c..1eb86de0bc030 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -146,7 +146,7 @@ if [[ "${KBN_ENABLE_FIPS:-}" == "true" ]] || is_pr_with_label "ci:enable-fips-ag fi if [[ -f "$KIBANA_DIR/config/kibana.yml" ]]; then - echo -e '\nxpack.security.experimental.fipsMode.enabled: true' >>"$KIBANA_DIR/config/kibana.yml" + echo -e '\nxpack.security.fipsMode.enabled: true' >>"$KIBANA_DIR/config/kibana.yml" fi fi diff --git a/.devcontainer/scripts/env.sh b/.devcontainer/scripts/env.sh index 77c2000663e5f..dccc17130c99c 100755 --- a/.devcontainer/scripts/env.sh +++ b/.devcontainer/scripts/env.sh @@ -9,7 +9,7 @@ setup_fips() { fi if [ -n "$FIPS" ] && [ "$FIPS" = "1" ]; then - sed -i '/xpack.security.experimental.fipsMode.enabled:/ {s/.*/xpack.security.experimental.fipsMode.enabled: true/; t}; $a\xpack.security.experimental.fipsMode.enabled: true' "$KBN_CONFIG_FILE" + sed -i '/xpack.security.fipsMode.enabled:/ {s/.*/xpack.security.fipsMode.enabled: true/; t}; $a\xpack.security.fipsMode.enabled: true' "$KBN_CONFIG_FILE" # Patch node_modules so we can start Kibana in dev mode sed -i 's/hashType = hashType || '\''md5'\'';/hashType = hashType || '\''sha1'\'';/g' "${KBN_DIR}/node_modules/file-loader/node_modules/loader-utils/lib/getHashDigest.js" @@ -21,7 +21,7 @@ setup_fips() { echo "FIPS mode enabled" echo "If manually bootstrapping in FIPS mode use: NODE_OPTIONS='' yarn kbn bootstrap" else - sed -i '/xpack.security.experimental.fipsMode.enabled:/ {s/.*/xpack.security.experimental.fipsMode.enabled: false/; t}; $a\xpack.security.experimental.fipsMode.enabled: false' "$KBN_CONFIG_FILE" + sed -i '/xpack.security.fipsMode.enabled:/ {s/.*/xpack.security.fipsMode.enabled: false/; t}; $a\xpack.security.fipsMode.enabled: false' "$KBN_CONFIG_FILE" fi } diff --git a/docs/user/security/fips-140-2.asciidoc b/docs/user/security/fips-140-2.asciidoc index 2b4b195f38b05..eada7bcc59cc7 100644 --- a/docs/user/security/fips-140-2.asciidoc +++ b/docs/user/security/fips-140-2.asciidoc @@ -29,7 +29,7 @@ For {kib}, adherence to FIPS 140-2 is ensured by: ==== Configuring {kib} for FIPS 140-2 -Apart from setting `xpack.security.experimental.fipsMode.enabled` to `true` in your {kib} config, a number of security related +Apart from setting `xpack.security.fipsMode.enabled` to `true` in your {kib} config, a number of security related settings need to be reviewed and configured in order to run {kib} successfully in a FIPS 140-2 compliant Node.js environment. @@ -56,8 +56,3 @@ As an example, avoid PKCS#12 specific settings such as: * `server.ssl.truststore.path` * `elasticsearch.ssl.keystore.path` * `elasticsearch.ssl.truststore.path` - -===== Limitations - -Configuring {kib} to run in FIPS mode is still considered to be experimental. Not all features are guaranteed to -function as expected. diff --git a/packages/core/security/core-security-server-internal/src/fips/fips.test.ts b/packages/core/security/core-security-server-internal/src/fips/fips.test.ts index ff610493e1322..724f6accd5204 100644 --- a/packages/core/security/core-security-server-internal/src/fips/fips.test.ts +++ b/packages/core/security/core-security-server-internal/src/fips/fips.test.ts @@ -25,26 +25,26 @@ import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; describe('fips', () => { let securityConfig: SecurityServiceConfigType; describe('#isFipsEnabled', () => { - it('should return `true` if config.experimental.fipsMode.enabled is `true`', () => { - securityConfig = { experimental: { fipsMode: { enabled: true } } }; + it('should return `true` if config.fipsMode.enabled is `true`', () => { + securityConfig = { fipsMode: { enabled: true } }; expect(isFipsEnabled(securityConfig)).toBe(true); }); - it('should return `false` if config.experimental.fipsMode.enabled is `false`', () => { - securityConfig = { experimental: { fipsMode: { enabled: false } } }; + it('should return `false` if config.fipsMode.enabled is `false`', () => { + securityConfig = { fipsMode: { enabled: false } }; expect(isFipsEnabled(securityConfig)).toBe(false); }); - it('should return `false` if config.experimental.fipsMode.enabled is `undefined`', () => { + it('should return `false` if config.fipsMode.enabled is `undefined`', () => { expect(isFipsEnabled(securityConfig)).toBe(false); }); }); describe('checkFipsConfig', () => { - it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode false', async () => { - securityConfig = { experimental: { fipsMode: { enabled: true } } }; + it('should log an error message if FIPS mode is misconfigured - xpack.security.fipsMode.enabled true, Nodejs FIPS mode false', async () => { + securityConfig = { fipsMode: { enabled: true } }; const logger = loggingSystemMock.create().get(); let fipsException: undefined | CriticalError; try { @@ -56,16 +56,16 @@ describe('fips', () => { expect(fipsException).toBeInstanceOf(CriticalError); expect(fipsException!.processExitCode).toBe(78); expect(fipsException!.message).toEqual( - 'Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled' + 'Configuration mismatch error. xpack.security.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled' ); }); - it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled false, Nodejs FIPS mode true', async () => { + it('should log an error message if FIPS mode is misconfigured - xpack.security.fipsMode.enabled false, Nodejs FIPS mode true', async () => { mockGetFipsFn.mockImplementationOnce(() => { return 1; }); - securityConfig = { experimental: { fipsMode: { enabled: false } } }; + securityConfig = { fipsMode: { enabled: false } }; const logger = loggingSystemMock.create().get(); let fipsException: undefined | CriticalError; @@ -77,16 +77,16 @@ describe('fips', () => { expect(fipsException).toBeInstanceOf(CriticalError); expect(fipsException!.processExitCode).toBe(78); expect(fipsException!.message).toEqual( - 'Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled' + 'Configuration mismatch error. xpack.security.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled' ); }); - it('should log an info message if FIPS mode is properly configured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode true', async () => { + it('should log an info message if FIPS mode is properly configured - xpack.security.fipsMode.enabled true, Nodejs FIPS mode true', async () => { mockGetFipsFn.mockImplementationOnce(() => { return 1; }); - securityConfig = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { fipsMode: { enabled: true } }; const logger = loggingSystemMock.create().get(); try { @@ -113,7 +113,7 @@ describe('fips', () => { return 1; }); - securityConfig = { experimental: { fipsMode: { enabled: true } } }; + securityConfig = { fipsMode: { enabled: true } }; }); afterEach(function () { diff --git a/packages/core/security/core-security-server-internal/src/fips/fips.ts b/packages/core/security/core-security-server-internal/src/fips/fips.ts index 0d9dea9e467fe..5fa47d3afc062 100644 --- a/packages/core/security/core-security-server-internal/src/fips/fips.ts +++ b/packages/core/security/core-security-server-internal/src/fips/fips.ts @@ -12,7 +12,7 @@ import { getFips } from 'crypto'; import { CriticalError } from '@kbn/core-base-server-internal'; import { PKCS12ConfigType, SecurityServiceConfigType } from '../utils'; export function isFipsEnabled(config: SecurityServiceConfigType): boolean { - return config?.experimental?.fipsMode?.enabled ?? false; + return config?.fipsMode?.enabled ?? false; } export function checkFipsConfig( @@ -33,7 +33,7 @@ export function checkFipsConfig( // FIPS must be enabled on both, or, log/error an exit Kibana if (isFipsConfigEnabled !== isNodeRunningWithFipsEnabled) { throw new CriticalError( - `Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to ${isFipsConfigEnabled} and the configured Node.js environment has FIPS ${ + `Configuration mismatch error. xpack.security.fipsMode.enabled is set to ${isFipsConfigEnabled} and the configured Node.js environment has FIPS ${ isNodeRunningWithFipsEnabled ? 'enabled' : 'disabled' }`, 'invalidConfig', diff --git a/packages/core/security/core-security-server-internal/src/security_service.test.ts b/packages/core/security/core-security-server-internal/src/security_service.test.ts index 0ff1e59db71ec..d725d062b231e 100644 --- a/packages/core/security/core-security-server-internal/src/security_service.test.ts +++ b/packages/core/security/core-security-server-internal/src/security_service.test.ts @@ -32,10 +32,8 @@ describe('SecurityService', function () { const mockConfig = { xpack: { security: { - experimental: { - fipsMode: { - enabled: !!getFips(), - }, + fipsMode: { + enabled: !!getFips(), }, }, }, diff --git a/packages/core/security/core-security-server-internal/src/utils/index.ts b/packages/core/security/core-security-server-internal/src/utils/index.ts index 666afcce38afd..ad4ed95e685ee 100644 --- a/packages/core/security/core-security-server-internal/src/utils/index.ts +++ b/packages/core/security/core-security-server-internal/src/utils/index.ts @@ -11,10 +11,8 @@ export { convertSecurityApi } from './convert_security_api'; export { getDefaultSecurityImplementation } from './default_implementation'; export interface SecurityServiceConfigType { - experimental?: { - fipsMode?: { - enabled: boolean; - }; + fipsMode?: { + enabled: boolean; }; } diff --git a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts index 672dab2e2d70e..852464e67de65 100644 --- a/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts +++ b/packages/core/test-helpers/core-test-helpers-kbn-server/src/create_root.ts @@ -66,7 +66,7 @@ export function createRootWithSettings( */ let oss = true; if (getFips() === 1) { - set(settings, 'xpack.security.experimental.fipsMode.enabled', true); + set(settings, 'xpack.security.fipsMode.enabled', true); oss = false; delete cliArgs.oss; } diff --git a/src/core/server/integration_tests/node/migrator.test.ts b/src/core/server/integration_tests/node/migrator.test.ts index f899d7da5cde0..c0ae1aab8ef29 100644 --- a/src/core/server/integration_tests/node/migrator.test.ts +++ b/src/core/server/integration_tests/node/migrator.test.ts @@ -44,7 +44,7 @@ describe('migrator-only node', () => { '--no-optimizer', '--no-base-path', '--no-watch', - isFipsEnabled ? '--xpack.security.experimental.fipsMode.enabled=true' : '--oss', + isFipsEnabled ? '--xpack.security.fipsMode.enabled=true' : '--oss', ], { stdio: ['pipe', 'pipe', 'pipe'] } ); diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 3c1e7ebe857fa..43f6baccbe989 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -393,7 +393,7 @@ kibana_vars=( xpack.security.authc.selector.enabled xpack.security.cookieName xpack.security.encryptionKey - xpack.security.experimental.fipsMode.enabled + xpack.security.fipsMode.enabled xpack.security.loginAssistanceMessage xpack.security.loginHelp xpack.security.sameSiteCookies diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index ec5588b4c793e..94d604d726562 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -59,7 +59,7 @@ RUN set -e ; \ make install > /dev/null ; \ rm -rf "/usr/share/kibana/openssl-${OPENSSL_VERSION}" ; \ chown -R 1000:0 "${OPENSSL_PATH}"; - + {{/fips}} # Ensure that group permissions are the same as user permissions. # This will help when relying on GID-0 to run Kibana, rather than UID-1000. @@ -156,7 +156,7 @@ RUN /bin/echo -e '\n--enable-fips' >> config/node.options RUN echo '--openssl-config=/usr/share/kibana/config/nodejs.cnf' >> config/node.options COPY --chown=1000:0 openssl/nodejs.cnf "/usr/share/kibana/config/nodejs.cnf" ENV OPENSSL_MODULES=/usr/share/kibana/openssl/lib/ossl-modules -ENV XPACK_SECURITY_EXPERIMENTAL_FIPSMODE_ENABLED=true +ENV XPACK_SECURITY_FIPSMODE_ENABLED=true {{/fips}} RUN ln -s /usr/share/kibana /opt/kibana diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index f5a735fdfe8b7..4db6a41871ab3 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -62,10 +62,8 @@ describe('config schema', () => { }, "cookieName": "sid", "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "experimental": Object { - "fipsMode": Object { - "enabled": false, - }, + "fipsMode": Object { + "enabled": false, }, "loginAssistanceMessage": "", "public": Object {}, @@ -121,10 +119,8 @@ describe('config schema', () => { }, "cookieName": "sid", "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "experimental": Object { - "fipsMode": Object { - "enabled": false, - }, + "fipsMode": Object { + "enabled": false, }, "loginAssistanceMessage": "", "public": Object {}, @@ -179,10 +175,8 @@ describe('config schema', () => { "selector": Object {}, }, "cookieName": "sid", - "experimental": Object { - "fipsMode": Object { - "enabled": false, - }, + "fipsMode": Object { + "enabled": false, }, "loginAssistanceMessage": "", "public": Object {}, @@ -240,10 +234,8 @@ describe('config schema', () => { "selector": Object {}, }, "cookieName": "sid", - "experimental": Object { - "fipsMode": Object { - "enabled": false, - }, + "fipsMode": Object { + "enabled": false, }, "loginAssistanceMessage": "", "public": Object {}, diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 5618186459566..80e904362b1fa 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -314,10 +314,8 @@ export const ConfigSchema = schema.object({ roleMappingManagementEnabled: schema.boolean({ defaultValue: true }), }), }), - experimental: schema.object({ - fipsMode: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), + fipsMode: schema.object({ + enabled: schema.boolean({ defaultValue: false }), }), }); diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 1245ef3978212..3be46e5ddeb79 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -46,6 +46,28 @@ describe('Config Deprecations', () => { expect(messages).toHaveLength(0); }); + it('renames `xpack.security.experimental.fipsMode.enabled` to `xpack.security.fipsMode.enabled`', () => { + const config = { + xpack: { + security: { + experimental: { + fipsMode: { + enabled: true, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.experimental?.fipsMode?.enabled).not.toBeDefined(); + expect(migrated.xpack.security.fipsMode.enabled).toEqual(true); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting \\"xpack.security.experimental.fipsMode.enabled\\" has been replaced by \\"xpack.security.fipsMode.enabled\\"", + ] + `); + }); + it('renames sessionTimeout to session.idleTimeout', () => { const config = { xpack: { diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index 2e6a14b2028a2..2ee7d05c78b8e 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -21,6 +21,9 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ rename('audit.appender.policy.kind', 'audit.appender.policy.type', { level: 'warning' }), rename('audit.appender.strategy.kind', 'audit.appender.strategy.type', { level: 'warning' }), rename('audit.appender.path', 'audit.appender.fileName', { level: 'warning' }), + rename('experimental.fipsMode.enabled', 'fipsMode.enabled', { + level: 'critical', + }), renameFromRoot( 'security.showInsecureClusterWarning', diff --git a/x-pack/plugins/security/server/fips/fips_service.test.ts b/x-pack/plugins/security/server/fips/fips_service.test.ts index a3f74e058268a..6bdc0fea35acb 100644 --- a/x-pack/plugins/security/server/fips/fips_service.test.ts +++ b/x-pack/plugins/security/server/fips/fips_service.test.ts @@ -43,7 +43,7 @@ function buildMockFipsServiceSetupParams( let mockConfig = {}; if (isFipsConfigured) { - mockConfig = { experimental: { fipsMode: { enabled: true } } }; + mockConfig = { fipsMode: { enabled: true } }; } return { @@ -84,7 +84,7 @@ describe('FipsService', () => { describe('#validateLicenseForFips', () => { describe('start-up check', () => { - it('should not throw Error/log.error if license features allowFips and `experimental.fipsMode.enabled` is `false`', () => { + it('should not throw Error/log.error if license features allowFips and `fipsMode.enabled` is `false`', () => { fipsServiceSetup = fipsService.setup( buildMockFipsServiceSetupParams('platinum', false, of({ allowFips: true })) ); @@ -93,7 +93,7 @@ describe('FipsService', () => { expect(logger.error).not.toHaveBeenCalled(); }); - it('should not throw Error/log.error if license features allowFips and `experimental.fipsMode.enabled` is `true`', () => { + it('should not throw Error/log.error if license features allowFips and `fipsMode.enabled` is `true`', () => { fipsServiceSetup = fipsService.setup( buildMockFipsServiceSetupParams('platinum', true, of({ allowFips: true })) ); @@ -102,7 +102,7 @@ describe('FipsService', () => { expect(logger.error).not.toHaveBeenCalled(); }); - it('should not throw Error/log.error if license features do not allowFips and `experimental.fipsMode.enabled` is `false`', () => { + it('should not throw Error/log.error if license features do not allowFips and `fipsMode.enabled` is `false`', () => { fipsServiceSetup = fipsService.setup( buildMockFipsServiceSetupParams('basic', false, of({ allowFips: false })) ); @@ -111,7 +111,7 @@ describe('FipsService', () => { expect(logger.error).not.toHaveBeenCalled(); }); - it('should throw Error/log.error if license features do not allowFips and `experimental.fipsMode.enabled` is `true`', () => { + it('should throw Error/log.error if license features do not allowFips and `fipsMode.enabled` is `true`', () => { fipsServiceSetup = fipsService.setup( buildMockFipsServiceSetupParams('basic', true, of({ allowFips: false })) ); @@ -124,7 +124,7 @@ describe('FipsService', () => { }); describe('monitoring check', () => { - describe('with experimental.fipsMode.enabled', () => { + describe('with fipsMode.enabled', () => { let mockFeaturesSubject: BehaviorSubject>; let mockIsAvailableSubject: BehaviorSubject; let mockFeatures$: Observable>; @@ -149,23 +149,23 @@ describe('FipsService', () => { mockIsAvailableSubject.next(true); }); - it('should not log.error if license changes to unavailable and `experimental.fipsMode.enabled` is `true`', () => { + it('should not log.error if license changes to unavailable and `fipsMode.enabled` is `true`', () => { mockIsAvailableSubject.next(false); expect(logger.error).not.toHaveBeenCalled(); }); - it('should not log.error if license features continue to allowFips and `experimental.fipsMode.enabled` is `true`', () => { + it('should not log.error if license features continue to allowFips and `fipsMode.enabled` is `true`', () => { mockFeaturesSubject.next({ allowFips: true }); expect(logger.error).not.toHaveBeenCalled(); }); - it('should log.error if license features change to not allowFips and `experimental.fipsMode.enabled` is `true`', () => { + it('should log.error if license features change to not allowFips and `fipsMode.enabled` is `true`', () => { mockFeaturesSubject.next({ allowFips: false }); expect(logger.error).toHaveBeenCalledTimes(1); }); }); - describe('with not experimental.fipsMode.enabled', () => { + describe('with not fipsMode.enabled', () => { let mockFeaturesSubject: BehaviorSubject>; let mockIsAvailableSubject: BehaviorSubject; let mockFeatures$: Observable>; @@ -191,17 +191,17 @@ describe('FipsService', () => { mockIsAvailableSubject.next(true); }); - it('should not log.error if license changes to unavailable and `experimental.fipsMode.enabled` is `false`', () => { + it('should not log.error if license changes to unavailable and `fipsMode.enabled` is `false`', () => { mockIsAvailableSubject.next(false); expect(logger.error).not.toHaveBeenCalled(); }); - it('should not log.error if license features continue to allowFips and `experimental.fipsMode.enabled` is `false`', () => { + it('should not log.error if license features continue to allowFips and `fipsMode.enabled` is `false`', () => { mockFeaturesSubject.next({ allowFips: true }); expect(logger.error).not.toHaveBeenCalled(); }); - it('should not log.error if license change to not allowFips and `experimental.fipsMode.enabled` is `false`', () => { + it('should not log.error if license change to not allowFips and `fipsMode.enabled` is `false`', () => { mockFeaturesSubject.next({ allowFips: false }); expect(logger.error).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/security/server/fips/fips_service.ts b/x-pack/plugins/security/server/fips/fips_service.ts index aa351ab48828d..9f9c01254bca2 100644 --- a/x-pack/plugins/security/server/fips/fips_service.ts +++ b/x-pack/plugins/security/server/fips/fips_service.ts @@ -40,7 +40,7 @@ export class FipsService { const errorMessage = `Your current license level is ${license.getLicenseType()} and does not support running in FIPS mode.`; if (license.isLicenseAvailable() && !this.isInitialLicenseLoaded) { - if (config?.experimental.fipsMode.enabled && !license.getFeatures().allowFips) { + if (config?.fipsMode.enabled && !license.getFeatures().allowFips) { this.logger.error(errorMessage); throw new Error(errorMessage); } @@ -51,7 +51,7 @@ export class FipsService { if ( this.isInitialLicenseLoaded && license.isLicenseAvailable() && - config?.experimental.fipsMode.enabled && + config?.fipsMode.enabled && !features.allowFips ) { this.logger.error( From ffe7f23580661355380a906c70fa90d0cbb6102d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 20 Nov 2024 01:15:13 +0000 Subject: [PATCH 40/42] skip flaky suite (#197912) --- .../service_group_count/service_group_count.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts index 8b43114ba0ed6..24a38cfa8e356 100644 --- a/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_groups/service_group_count/service_group_count.spec.ts @@ -45,8 +45,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); } - // FLAKY: https://github.com/elastic/kibana/issues/177655 - registry.when('Service group counts', { config: 'basic', archives: [] }, () => { + // FLAKY: https://github.com/elastic/kibana/issues/197912 + registry.when.skip('Service group counts', { config: 'basic', archives: [] }, () => { let synthbeansServiceGroupId: string; let opbeansServiceGroupId: string; before(async () => { From 33670f590d5da19ee0fae7988552ba6f419bafa6 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:29:49 +1100 Subject: [PATCH 41/42] [8.x] [CLOUD-UI] Cloud onboarding token (#198444) (#200832) # Backport This will backport the following commits from `main` to `8.x`: - [[CLOUD-UI] Cloud onboarding token (#198444)](https://github.com/elastic/kibana/pull/198444) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Xavier Mouligneau --- .buildkite/ftr_platform_stateful_configs.yml | 1 + .../current_fields.json | 1 + .../current_mappings.json | 4 + .../check_registered_types.test.ts | 1 + .../group3/type_registrations.test.ts | 1 + x-pack/plugins/cloud/server/plugin.ts | 14 ++- .../plugins/cloud/server/routes/constants.ts | 8 ++ ...earch_routes.ts => elasticsearch_route.ts} | 2 +- .../server/routes/get_cloud_data_route.ts | 43 +++++++ x-pack/plugins/cloud/server/routes/index.ts | 27 ++++ .../routes/set_cloud_data_route.test.ts | 119 ++++++++++++++++++ .../server/routes/set_cloud_data_route.ts | 92 ++++++++++++++ x-pack/plugins/cloud/server/routes/types.ts | 20 +++ .../cloud/server/saved_objects/index.ts | 27 ++++ .../cloud_data_model_versions.ts | 19 +++ .../saved_objects/model_versions/index.ts | 8 ++ x-pack/plugins/cloud/tsconfig.json | 1 + .../test/api_integration/apis/cloud/config.ts | 26 ++++ .../test/api_integration/apis/cloud/index.ts | 14 +++ .../apis/cloud/set_cloud_data_route.ts | 41 ++++++ 20 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/cloud/server/routes/constants.ts rename x-pack/plugins/cloud/server/routes/{elasticsearch_routes.ts => elasticsearch_route.ts} (95%) create mode 100644 x-pack/plugins/cloud/server/routes/get_cloud_data_route.ts create mode 100644 x-pack/plugins/cloud/server/routes/index.ts create mode 100644 x-pack/plugins/cloud/server/routes/set_cloud_data_route.test.ts create mode 100644 x-pack/plugins/cloud/server/routes/set_cloud_data_route.ts create mode 100644 x-pack/plugins/cloud/server/routes/types.ts create mode 100644 x-pack/plugins/cloud/server/saved_objects/index.ts create mode 100644 x-pack/plugins/cloud/server/saved_objects/model_versions/cloud_data_model_versions.ts create mode 100644 x-pack/plugins/cloud/server/saved_objects/model_versions/index.ts create mode 100644 x-pack/test/api_integration/apis/cloud/config.ts create mode 100644 x-pack/test/api_integration/apis/cloud/index.ts create mode 100644 x-pack/test/api_integration/apis/cloud/set_cloud_data_route.ts diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index b015b1c96c73a..e12e18c520ae3 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -374,3 +374,4 @@ enabled: - x-pack/test/custom_branding/config.ts # stateful config files that run deployment-agnostic tests - x-pack/test/api_integration/deployment_agnostic/configs/stateful/platform.stateful.config.ts + - x-pack/test/api_integration/apis/cloud/config.ts diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index ab8d23b6a5a8a..d404437f9802a 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -233,6 +233,7 @@ "payload.connector.type", "type" ], + "cloud": [], "cloud-security-posture-settings": [], "config": [ "buildNum" diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index f2ad165cc4b72..a92681aa25a86 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -788,6 +788,10 @@ } } }, + "cloud": { + "dynamic": false, + "properties": {} + }, "cloud-security-posture-settings": { "dynamic": false, "properties": {} diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 3890a9fe360e4..cad5e23781703 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -80,6 +80,7 @@ describe('checking migration metadata changes on all registered SO types', () => "cases-rules": "6d1776f5c46a99e1a0f3085c537146c1cdfbc829", "cases-telemetry": "f219eb7e26772884342487fc9602cfea07b3cedc", "cases-user-actions": "483f10db9b3bd1617948d7032a98b7791bf87414", + "cloud": "b549f4f7ab1fd41aab366a66afa52a2a008aefea", "cloud-security-posture-settings": "e0f61c68bbb5e4cfa46ce8994fa001e417df51ca", "config": "179b3e2bc672626aafce3cf92093a113f456af38", "config-global": "8e8a134a2952df700d7d4ec51abb794bbd4cf6da", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 3ceba522d08cb..cee7f307ee67d 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -32,6 +32,7 @@ const previouslyRegisteredTypes = [ 'canvas-element', 'canvas-workpad', 'canvas-workpad-template', + 'cloud', 'cloud-security-posture-settings', 'cases', 'cases-comments', diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index 9821aa318e264..8b20906c30f89 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -9,6 +9,7 @@ import type { Logger } from '@kbn/logging'; import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import type { SolutionId } from '@kbn/core-chrome-browser'; + import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import type { CloudConfigType } from './config'; import { registerCloudUsageCollector } from './collectors'; @@ -18,7 +19,9 @@ import { decodeCloudId, DecodedCloudId } from '../common/decode_cloud_id'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; import { getFullCloudUrl } from '../common/utils'; import { readInstanceSizeMb } from './env'; -import { defineRoutes } from './routes/elasticsearch_routes'; +import { defineRoutes } from './routes'; +import { CloudRequestHandlerContext } from './routes/types'; +import { setupSavedObjects } from './saved_objects'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; @@ -202,10 +205,15 @@ export class CloudPlugin implements Plugin { if (this.config.id) { decodedId = decodeCloudId(this.config.id, this.logger); } - const router = core.http.createRouter(); + const router = core.http.createRouter(); const elasticsearchUrl = core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl; - defineRoutes({ logger: this.logger, router, elasticsearchUrl }); + defineRoutes({ + logger: this.logger, + router, + elasticsearchUrl, + }); + setupSavedObjects(core.savedObjects, this.logger); return { ...this.getCloudUrls(), cloudId: this.config.id, diff --git a/x-pack/plugins/cloud/server/routes/constants.ts b/x-pack/plugins/cloud/server/routes/constants.ts new file mode 100644 index 0000000000000..a1bfb699ac6b1 --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 const CLOUD_DATA_SAVED_OBJECT_ID = 'cloud-data-saved-object-id'; diff --git a/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts b/x-pack/plugins/cloud/server/routes/elasticsearch_route.ts similarity index 95% rename from x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts rename to x-pack/plugins/cloud/server/routes/elasticsearch_route.ts index 5cdc2f90559cc..4050910db9569 100644 --- a/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts +++ b/x-pack/plugins/cloud/server/routes/elasticsearch_route.ts @@ -10,7 +10,7 @@ import { Logger } from '@kbn/logging'; import { ElasticsearchConfigType } from '../../common/types'; import { ELASTICSEARCH_CONFIG_ROUTE } from '../../common/constants'; -export function defineRoutes({ +export function setElasticsearchRoute({ elasticsearchUrl, logger, router, diff --git a/x-pack/plugins/cloud/server/routes/get_cloud_data_route.ts b/x-pack/plugins/cloud/server/routes/get_cloud_data_route.ts new file mode 100644 index 0000000000000..c905e4b641c0c --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/get_cloud_data_route.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RouteOptions } from '.'; +import { CLOUD_DATA_SAVED_OBJECT_ID } from './constants'; +import { CLOUD_DATA_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { CloudDataAttributes } from './types'; + +export const setGetCloudSolutionDataRoute = ({ router }: RouteOptions) => { + router.versioned + .get({ + path: `/internal/cloud/solution`, + access: 'internal', + summary: 'Get cloud data for solutions', + }) + .addVersion( + { + version: '1', + validate: { + request: {}, + }, + }, + async (context, request, response) => { + const coreContext = await context.core; + const savedObjectsClient = coreContext.savedObjects.getClient({ + includedHiddenTypes: [CLOUD_DATA_SAVED_OBJECT_TYPE], + }); + try { + const cloudDataSo = await savedObjectsClient.get( + CLOUD_DATA_SAVED_OBJECT_TYPE, + CLOUD_DATA_SAVED_OBJECT_ID + ); + return response.ok({ body: cloudDataSo?.attributes ?? null }); + } catch (error) { + return response.customError(error); + } + } + ); +}; diff --git a/x-pack/plugins/cloud/server/routes/index.ts b/x-pack/plugins/cloud/server/routes/index.ts new file mode 100644 index 0000000000000..5db24b880881c --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '@kbn/core/server'; +import { Logger } from '@kbn/logging'; +import { setPostCloudSolutionDataRoute } from './set_cloud_data_route'; +import { CloudRequestHandlerContext } from './types'; +import { setElasticsearchRoute } from './elasticsearch_route'; +import { setGetCloudSolutionDataRoute } from './get_cloud_data_route'; + +export interface RouteOptions { + logger: Logger; + router: IRouter; + elasticsearchUrl?: string; +} + +export function defineRoutes(opts: RouteOptions) { + const { logger, elasticsearchUrl, router } = opts; + + setElasticsearchRoute({ logger, elasticsearchUrl, router }); + setGetCloudSolutionDataRoute({ logger, router }); + setPostCloudSolutionDataRoute({ logger, router }); +} diff --git a/x-pack/plugins/cloud/server/routes/set_cloud_data_route.test.ts b/x-pack/plugins/cloud/server/routes/set_cloud_data_route.test.ts new file mode 100644 index 0000000000000..c36e49206a287 --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/set_cloud_data_route.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; +import { + RequestHandlerContext, + RouteValidatorConfig, + SavedObjectsErrorHelpers, + kibanaResponseFactory, +} from '@kbn/core/server'; +import { CLOUD_DATA_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { CLOUD_DATA_SAVED_OBJECT_ID } from './constants'; +import { setPostCloudSolutionDataRoute } from './set_cloud_data_route'; +import { RouteOptions } from '.'; + +const mockSavedObjectsClientGet = jest.fn(); +const mockSavedObjectsClientCreate = jest.fn(); +const mockSavedObjectsClientUpdate = jest.fn(); + +const mockRouteContext = { + core: { + savedObjects: { + getClient: () => ({ + get: mockSavedObjectsClientGet, + create: mockSavedObjectsClientCreate, + update: mockSavedObjectsClientUpdate, + }), + }, + }, +} as unknown as RequestHandlerContext; + +describe('POST /internal/cloud/solution', () => { + const setup = async () => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter(); + + setPostCloudSolutionDataRoute({ + router, + } as unknown as RouteOptions); + + const [routeDefinition, routeHandler] = + router.versioned.post.mock.results[0].value.addVersion.mock.calls[0]; + + return { + routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, + routeHandler, + }; + }; + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create cloud data if it does not exist', async () => { + const { routeHandler } = await setup(); + + mockSavedObjectsClientGet.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError() + ); + + const request = httpServerMock.createKibanaRequest({ + body: { + onboardingData: { + solutionType: 'security', + token: 'test-token', + }, + }, + method: 'post', + }); + + await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(mockSavedObjectsClientGet).toHaveBeenCalledWith( + CLOUD_DATA_SAVED_OBJECT_TYPE, + CLOUD_DATA_SAVED_OBJECT_ID + ); + expect(mockSavedObjectsClientCreate).toHaveBeenCalledWith( + CLOUD_DATA_SAVED_OBJECT_TYPE, + { onboardingData: request.body.onboardingData }, + { id: CLOUD_DATA_SAVED_OBJECT_ID } + ); + }); + + it('should update cloud data if it exists', async () => { + const { routeHandler } = await setup(); + + mockSavedObjectsClientGet.mockResolvedValue({ + id: CLOUD_DATA_SAVED_OBJECT_ID, + attributes: { + onboardingData: { solutionType: 'o11y', token: 'test-33' }, + }, + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + onboardingData: { + solutionType: 'security', + token: 'test-token', + }, + }, + method: 'post', + }); + + await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(mockSavedObjectsClientGet).toHaveBeenCalledWith( + CLOUD_DATA_SAVED_OBJECT_TYPE, + CLOUD_DATA_SAVED_OBJECT_ID + ); + expect(mockSavedObjectsClientUpdate).toHaveBeenCalledWith( + CLOUD_DATA_SAVED_OBJECT_TYPE, + CLOUD_DATA_SAVED_OBJECT_ID, + { onboardingData: request.body.onboardingData } + ); + }); +}); diff --git a/x-pack/plugins/cloud/server/routes/set_cloud_data_route.ts b/x-pack/plugins/cloud/server/routes/set_cloud_data_route.ts new file mode 100644 index 0000000000000..511c8dc2081f0 --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/set_cloud_data_route.ts @@ -0,0 +1,92 @@ +/* + * 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 { ReservedPrivilegesSet, SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { RouteOptions } from '.'; +import { CLOUD_DATA_SAVED_OBJECT_ID } from './constants'; +import { CLOUD_DATA_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { CloudDataAttributes } from './types'; + +const createBodySchemaV1 = schema.object({ + onboardingData: schema.object({ + solutionType: schema.oneOf([ + schema.literal('security'), + schema.literal('observability'), + schema.literal('search'), + schema.literal('elasticsearch'), + ]), + token: schema.string(), + }), +}); + +export const setPostCloudSolutionDataRoute = ({ router }: RouteOptions) => { + router.versioned + .post({ + path: `/internal/cloud/solution`, + access: 'internal', + summary: 'Save cloud data for solutions', + security: { + authz: { + requiredPrivileges: [ReservedPrivilegesSet.superuser], + }, + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: createBodySchemaV1, + }, + }, + }, + async (context, request, response) => { + const coreContext = await context.core; + const savedObjectsClient = coreContext.savedObjects.getClient({ + includedHiddenTypes: [CLOUD_DATA_SAVED_OBJECT_TYPE], + }); + let cloudDataSo = null; + try { + cloudDataSo = await savedObjectsClient.get( + CLOUD_DATA_SAVED_OBJECT_TYPE, + CLOUD_DATA_SAVED_OBJECT_ID + ); + } catch (error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + cloudDataSo = null; + } else { + return response.customError(error); + } + } + + try { + if (cloudDataSo === null) { + await savedObjectsClient.create( + CLOUD_DATA_SAVED_OBJECT_TYPE, + { + onboardingData: request.body.onboardingData, + }, + { id: CLOUD_DATA_SAVED_OBJECT_ID } + ); + } else { + await savedObjectsClient.update( + CLOUD_DATA_SAVED_OBJECT_TYPE, + CLOUD_DATA_SAVED_OBJECT_ID, + { + onboardingData: request.body.onboardingData, + } + ); + } + } catch (error) { + return response.badRequest(error); + } + + return response.ok(); + } + ); +}; diff --git a/x-pack/plugins/cloud/server/routes/types.ts b/x-pack/plugins/cloud/server/routes/types.ts new file mode 100644 index 0000000000000..d69877c7b326e --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { CustomRequestHandlerContext } from '@kbn/core/server'; + +/** + * @internal + */ +export type CloudRequestHandlerContext = CustomRequestHandlerContext<{}>; + +export interface CloudDataAttributes { + onboardingData: { + solutionType: 'security' | 'observability' | 'search' | 'elasticsearch'; + token: string; + }; +} diff --git a/x-pack/plugins/cloud/server/saved_objects/index.ts b/x-pack/plugins/cloud/server/saved_objects/index.ts new file mode 100644 index 0000000000000..295e6d81a39fb --- /dev/null +++ b/x-pack/plugins/cloud/server/saved_objects/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectsServiceSetup } from '@kbn/core/server'; + +export const CLOUD_DATA_SAVED_OBJECT_TYPE = 'cloud' as const; + +export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup, logger: Logger) { + savedObjects.registerType({ + name: CLOUD_DATA_SAVED_OBJECT_TYPE, + hidden: true, + hiddenFromHttpApis: true, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: {}, + }, + management: { + importableAndExportable: false, + }, + modelVersions: {}, + }); +} diff --git a/x-pack/plugins/cloud/server/saved_objects/model_versions/cloud_data_model_versions.ts b/x-pack/plugins/cloud/server/saved_objects/model_versions/cloud_data_model_versions.ts new file mode 100644 index 0000000000000..051a733d39178 --- /dev/null +++ b/x-pack/plugins/cloud/server/saved_objects/model_versions/cloud_data_model_versions.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsModelVersionMap } from '@kbn/core-saved-objects-server'; +import { schema } from '@kbn/config-schema'; + +export const cloudDataModelVersions: SavedObjectsModelVersionMap = { + '1': { + changes: [], + schemas: { + forwardCompatibility: schema.object({}).extends({}, { unknowns: 'ignore' }), + create: schema.object({}), + }, + }, +}; diff --git a/x-pack/plugins/cloud/server/saved_objects/model_versions/index.ts b/x-pack/plugins/cloud/server/saved_objects/model_versions/index.ts new file mode 100644 index 0000000000000..51e8b5431c547 --- /dev/null +++ b/x-pack/plugins/cloud/server/saved_objects/model_versions/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { cloudDataModelVersions } from './cloud_data_model_versions'; diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index dd25064897758..37d0b6f4b4de0 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -17,6 +17,7 @@ "@kbn/config-schema", "@kbn/logging-mocks", "@kbn/logging", + "@kbn/core-saved-objects-server", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/api_integration/apis/cloud/config.ts b/x-pack/test/api_integration/apis/cloud/config.ts new file mode 100644 index 0000000000000..87000e8fc5427 --- /dev/null +++ b/x-pack/test/api_integration/apis/cloud/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + kbnTestServer: { + ...baseIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...baseIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.cloud.id="ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM="', + '--xpack.cloud.base_url="https://cloud.elastic.co"', + '--xpack.spaces.allowSolutionVisibility=true', + ], + }, + }; +} diff --git a/x-pack/test/api_integration/apis/cloud/index.ts b/x-pack/test/api_integration/apis/cloud/index.ts new file mode 100644 index 0000000000000..819a9474e0752 --- /dev/null +++ b/x-pack/test/api_integration/apis/cloud/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('cloud data', function () { + loadTestFile(require.resolve('./set_cloud_data_route')); + }); +} diff --git a/x-pack/test/api_integration/apis/cloud/set_cloud_data_route.ts b/x-pack/test/api_integration/apis/cloud/set_cloud_data_route.ts new file mode 100644 index 0000000000000..84331ab4c129d --- /dev/null +++ b/x-pack/test/api_integration/apis/cloud/set_cloud_data_route.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('POST /internal/cloud/solution', () => { + it('set solution data', async () => { + await supertest + .post('/internal/cloud/solution') + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'cloud') + .set('elastic-api-version', '1') + .send({ + onboardingData: { + solutionType: 'search', + token: 'connectors', + }, + }) + .expect(200); + + const { + body: { onboardingData }, + } = await supertest + .get('/internal/cloud/solution') + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'cloud') + .set('elastic-api-version', '1') + .expect(200); + + expect(onboardingData).to.eql({ solutionType: 'search', token: 'connectors' }); + }); + }); +} From 94bb900d723d0d937dffb22a7decc9cde12e9132 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:55:51 +1100 Subject: [PATCH 42/42] [8.x] [uiActions] Catch errors in isCompatible (#200261) (#200839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Backport This will backport the following commits from `main` to `8.x`: - [[uiActions] Catch errors in isCompatible (#200261)](https://github.com/elastic/kibana/pull/200261) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Catherine Liu --- .../ui_actions/public/actions/action_internal.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index ccef920ecc465..6f979849bdc41 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -29,6 +29,7 @@ export class ActionInternal public readonly subscribeToCompatibilityChanges?: Action['subscribeToCompatibilityChanges']; public readonly couldBecomeCompatible?: Action['couldBecomeCompatible']; + public errorLogged?: boolean; constructor(public readonly definition: ActionDefinition) { this.id = this.definition.id; @@ -38,6 +39,7 @@ export class ActionInternal this.grouping = this.definition.grouping; this.showNotification = this.definition.showNotification; this.disabled = this.definition.disabled; + this.errorLogged = false; if (this.definition.subscribeToCompatibilityChanges) { this.subscribeToCompatibilityChanges = definition.subscribeToCompatibilityChanges; @@ -77,7 +79,16 @@ export class ActionInternal public async isCompatible(context: Context): Promise { if (!this.definition.isCompatible) return true; - return await this.definition.isCompatible(context); + try { + return await this.definition.isCompatible(context); + } catch (e) { + if (!this.errorLogged) { + // eslint-disable-next-line no-console + console.error(e); + this.errorLogged = true; + } + return false; + } } public async getHref(context: Context): Promise {