diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index d7b1b6d02323a..1ee5e2e149a1f 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -35266,6 +35266,10 @@ paths: schema: nullable: true type: string + - in: query + name: associatedFilter + schema: + $ref: '#/components/schemas/Security_Timeline_API_AssociatedFilterType' responses: '200': content: @@ -49419,6 +49423,14 @@ components: Security_Osquery_API_VersionOrUndefined: $ref: '#/components/schemas/Security_Osquery_API_Version' nullable: true + Security_Timeline_API_AssociatedFilterType: + description: Filter notes based on their association with a document or saved object. + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + type: string Security_Timeline_API_BareNote: type: object properties: diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index d7b1b6d02323a..1ee5e2e149a1f 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -35266,6 +35266,10 @@ paths: schema: nullable: true type: string + - in: query + name: associatedFilter + schema: + $ref: '#/components/schemas/Security_Timeline_API_AssociatedFilterType' responses: '200': content: @@ -49419,6 +49423,14 @@ components: Security_Osquery_API_VersionOrUndefined: $ref: '#/components/schemas/Security_Osquery_API_Version' nullable: true + Security_Timeline_API_AssociatedFilterType: + description: Filter notes based on their association with a document or saved object. + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + type: string Security_Timeline_API_BareNote: type: object properties: diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 24b0462ae93ef..8323fc524ebce 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -38697,6 +38697,10 @@ paths: schema: nullable: true type: string + - in: query + name: associatedFilter + schema: + $ref: '#/components/schemas/Security_Timeline_API_AssociatedFilterType' responses: '200': content: @@ -58185,6 +58189,14 @@ components: Security_Osquery_API_VersionOrUndefined: $ref: '#/components/schemas/Security_Osquery_API_Version' nullable: true + Security_Timeline_API_AssociatedFilterType: + description: Filter notes based on their association with a document or saved object. + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + type: string Security_Timeline_API_BareNote: type: object properties: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 24b0462ae93ef..8323fc524ebce 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -38697,6 +38697,10 @@ paths: schema: nullable: true type: string + - in: query + name: associatedFilter + schema: + $ref: '#/components/schemas/Security_Timeline_API_AssociatedFilterType' responses: '200': content: @@ -58185,6 +58189,14 @@ components: Security_Osquery_API_VersionOrUndefined: $ref: '#/components/schemas/Security_Osquery_API_Version' nullable: true + Security_Timeline_API_AssociatedFilterType: + description: Filter notes based on their association with a document or saved object. + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + type: string Security_Timeline_API_BareNote: type: object properties: diff --git a/packages/kbn-es-types/src/search.ts b/packages/kbn-es-types/src/search.ts index 3e67208bc2793..4c780fb2a2986 100644 --- a/packages/kbn-es-types/src/search.ts +++ b/packages/kbn-es-types/src/search.ts @@ -692,6 +692,7 @@ export interface ESQLSearchParams { query: string; filter?: unknown; locale?: string; + include_ccs_metadata?: boolean; dropNullColumns?: boolean; params?: Array>; } diff --git a/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts index e79c418eeb3cc..38e98104d41bd 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/columns.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('Column Identifier Expressions', () => { it('can parse un-quoted identifiers', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts index 30d44d447387e..6fb176c9624f7 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/commands.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('commands', () => { describe('correctly formatted, basic usage', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts index 101661973a692..f2f0fded57ca5 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/from.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('FROM', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts index 8ec533816a56e..9d822f78f9333 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/function.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { Walker } from '../../walker'; describe('function AST nodes', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts index d0650ab3f3213..889ca2a2ecf3d 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/inlinecast.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { ESQLFunction, ESQLInlineCast, ESQLSingleAstItem } from '../../types'; describe('Inline cast (::)', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts index 514d769d5c45e..7f50198c96047 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/literal.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { ESQLLiteral } from '../../types'; describe('literal expression', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts index 54ddc49c5d048..d33c94e8903ac 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/metrics.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('METRICS', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts index 8586236eeb2f9..e4b1a892d32d9 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/params.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; import { Walker } from '../../walker'; /** diff --git a/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts index 4acad891150b2..214e5c1d36882 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/rename.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('RENAME', () => { /** diff --git a/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts index cfaec0a6e39e9..981eac40b68ae 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/sort.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('SORT', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts b/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts index d507b559fd407..f3f6aeb886ec0 100644 --- a/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts +++ b/packages/kbn-esql-ast/src/parser/__tests__/where.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { getAstAndSyntaxErrors as parse } from '..'; +import { parse } from '..'; describe('WHERE', () => { describe('correctly formatted', () => { diff --git a/packages/kbn-esql-ast/src/parser/parser.ts b/packages/kbn-esql-ast/src/parser/parser.ts index ad263a49ebd00..f99e00e92d1e0 100644 --- a/packages/kbn-esql-ast/src/parser/parser.ts +++ b/packages/kbn-esql-ast/src/parser/parser.ts @@ -100,33 +100,66 @@ export interface ParseResult { } export const parse = (text: string | undefined, options: ParseOptions = {}): ParseResult => { - if (text == null) { - const commands: ESQLAstQueryExpression['commands'] = []; - return { ast: commands, root: Builder.expression.query(commands), errors: [], tokens: [] }; + try { + if (text == null) { + const commands: ESQLAstQueryExpression['commands'] = []; + return { ast: commands, root: Builder.expression.query(commands), errors: [], tokens: [] }; + } + const errorListener = new ESQLErrorListener(); + const parseListener = new ESQLAstBuilderListener(); + const { tokens, parser } = getParser( + CharStreams.fromString(text), + errorListener, + parseListener + ); + + parser[GRAMMAR_ROOT_RULE](); + + const errors = errorListener.getErrors().filter((error) => { + return !SYNTAX_ERRORS_TO_IGNORE.includes(error.message); + }); + const { ast: commands } = parseListener.getAst(); + const root = Builder.expression.query(commands, { + location: { + min: 0, + max: text.length - 1, + }, + }); + + if (options.withFormatting) { + const decorations = collectDecorations(tokens); + attachDecorations(root, tokens.tokens, decorations.lines); + } + + return { root, ast: commands, errors, tokens: tokens.tokens }; + } catch (error) { + /** + * Parsing should never fail, meaning this branch should never execute. But + * if it does fail, we want to log the error message for easier debugging. + */ + // eslint-disable-next-line no-console + console.error(error); + + const root = Builder.expression.query(); + + return { + root, + ast: root.commands, + errors: [ + { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 0, + message: + 'Parsing internal error: ' + + (!!error && typeof error === 'object' ? String(error.message) : String(error)), + severity: 'error', + }, + ], + tokens: [], + }; } - const errorListener = new ESQLErrorListener(); - const parseListener = new ESQLAstBuilderListener(); - const { tokens, parser } = getParser(CharStreams.fromString(text), errorListener, parseListener); - - parser[GRAMMAR_ROOT_RULE](); - - const errors = errorListener.getErrors().filter((error) => { - return !SYNTAX_ERRORS_TO_IGNORE.includes(error.message); - }); - const { ast: commands } = parseListener.getAst(); - const root = Builder.expression.query(commands, { - location: { - min: 0, - max: text.length - 1, - }, - }); - - if (options.withFormatting) { - const decorations = collectDecorations(tokens); - attachDecorations(root, tokens.tokens, decorations.lines); - } - - return { root, ast: commands, errors, tokens: tokens.tokens }; }; export const parseErrors = (text: string) => { diff --git a/src/plugins/data/common/search/expressions/esql.ts b/src/plugins/data/common/search/expressions/esql.ts index 966500710fd45..a93996f163962 100644 --- a/src/plugins/data/common/search/expressions/esql.ts +++ b/src/plugins/data/common/search/expressions/esql.ts @@ -164,6 +164,7 @@ export const getEsqlFn = ({ getStartDependencies }: EsqlFnArguments) => { query, // time_zone: timezone, locale, + include_ccs_metadata: true, }; if (input) { const esQueryConfigs = getEsQueryConfig( diff --git a/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts b/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts index 0757304199d8a..6a7a3fc80343e 100644 --- a/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts +++ b/test/functional/apps/discover/group2_data_grid1/_data_grid_context.ts @@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const security = getService('security'); - describe('discover data grid context tests', () => { + // Failing: See https://github.com/elastic/kibana/issues/196120 + describe.skip('discover data grid context tests', () => { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); diff --git a/x-pack/packages/security/plugin_types_public/index.ts b/x-pack/packages/security/plugin_types_public/index.ts index a48511441382a..fc8829ad8a5f8 100644 --- a/x-pack/packages/security/plugin_types_public/index.ts +++ b/x-pack/packages/security/plugin_types_public/index.ts @@ -24,3 +24,4 @@ export type { } from './src/roles'; export { PrivilegesAPIClientPublicContract } from './src/privileges'; export type { PrivilegesAPIClientGetAllArgs } from './src/privileges'; +export type { SecurityLicense } from './src/license'; diff --git a/x-pack/packages/security/plugin_types_public/src/license/index.ts b/x-pack/packages/security/plugin_types_public/src/license/index.ts new file mode 100644 index 0000000000000..0c1ec0431c10a --- /dev/null +++ b/x-pack/packages/security/plugin_types_public/src/license/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 type { SecurityPluginSetup } from '../plugin'; + +export type SecurityLicense = SecurityPluginSetup['license']; diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx index d34e3c112d9fb..9f48783fde24d 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx @@ -54,10 +54,10 @@ describe('useCasesBreadcrumbs', () => { describe('set all_cases breadcrumbs', () => { it('call setBreadcrumbs with all items', async () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.cases), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { text: 'Cases' }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [{ href: '/test', onClick: expect.any(Function), text: 'Test' }, { text: 'Cases' }], + { project: { value: [] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); @@ -76,11 +76,14 @@ describe('useCasesBreadcrumbs', () => { describe('set create_case breadcrumbs', () => { it('call setBreadcrumbs with all items', () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesCreate), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, - { text: 'Create' }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/test', onClick: expect.any(Function), text: 'Test' }, + { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, + { text: 'Create' }, + ], + { project: { value: [] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); @@ -100,11 +103,14 @@ describe('useCasesBreadcrumbs', () => { const title = 'Fake Title'; it('call setBreadcrumbs with title', () => { renderHook(() => useCasesTitleBreadcrumbs(title), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, - { text: title }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/test', onClick: expect.any(Function), text: 'Test' }, + { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, + { text: title }, + ], + { project: { value: [{ text: title }] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([{ text: title }]); }); @@ -123,11 +129,14 @@ describe('useCasesBreadcrumbs', () => { describe('set settings breadcrumbs', () => { it('call setBreadcrumbs with all items', () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesConfigure), { wrapper }); - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - { href: '/test', onClick: expect.any(Function), text: 'Test' }, - { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, - { text: 'Settings' }, - ]); + expect(mockSetBreadcrumbs).toHaveBeenCalledWith( + [ + { href: '/test', onClick: expect.any(Function), text: 'Test' }, + { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, + { text: 'Settings' }, + ], + { project: { value: [] } } + ); expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts index 6312918842ba3..1750cd54e7d53 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts @@ -29,34 +29,48 @@ function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] { return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse(); } +const useGetBreadcrumbsWithNavigation = () => { + const { navigateToUrl } = useKibana().services.application; + + return useCallback( + (breadcrumbs: ChromeBreadcrumb[]): ChromeBreadcrumb[] => { + return breadcrumbs.map((breadcrumb) => { + const { href, onClick } = breadcrumb; + if (!href || onClick) { + return breadcrumb; + } + return { + ...breadcrumb, + onClick: (event) => { + if (event) { + event.preventDefault(); + } + navigateToUrl(href); + }, + }; + }); + }, + [navigateToUrl] + ); +}; + const useApplyBreadcrumbs = () => { - const { - chrome: { docTitle, setBreadcrumbs }, - application: { navigateToUrl }, - } = useKibana().services; + const { docTitle, setBreadcrumbs } = useKibana().services.chrome; + const getBreadcrumbsWithNavigation = useGetBreadcrumbsWithNavigation(); + return useCallback( - (breadcrumbs: ChromeBreadcrumb[]) => { - docTitle.change(getTitleFromBreadcrumbs(breadcrumbs)); - setBreadcrumbs( - breadcrumbs.map((breadcrumb) => { - const { href, onClick } = breadcrumb; - return { - ...breadcrumb, - ...(href && !onClick - ? { - onClick: (event) => { - if (event) { - event.preventDefault(); - } - navigateToUrl(href); - }, - } - : {}), - }; - }) - ); + ( + leadingRawBreadcrumbs: ChromeBreadcrumb[], + trailingRawBreadcrumbs: ChromeBreadcrumb[] = [] + ) => { + const leadingBreadcrumbs = getBreadcrumbsWithNavigation(leadingRawBreadcrumbs); + const trailingBreadcrumbs = getBreadcrumbsWithNavigation(trailingRawBreadcrumbs); + const allBreadcrumbs = [...leadingBreadcrumbs, ...trailingBreadcrumbs]; + + docTitle.change(getTitleFromBreadcrumbs(allBreadcrumbs)); + setBreadcrumbs(allBreadcrumbs, { project: { value: trailingBreadcrumbs } }); }, - [docTitle, setBreadcrumbs, navigateToUrl] + [docTitle, setBreadcrumbs, getBreadcrumbsWithNavigation] ); }; @@ -103,9 +117,8 @@ export const useCasesTitleBreadcrumbs = (caseTitle: string) => { text: casesBreadcrumbTitle[CasesDeepLinkId.cases], href: getAppUrl({ deepLinkId: CasesDeepLinkId.cases }), }, - titleBreadcrumb, ]; - applyBreadcrumbs(casesBreadcrumbs); + applyBreadcrumbs(casesBreadcrumbs, [titleBreadcrumb]); KibanaServices.get().serverless?.setBreadcrumbs([titleBreadcrumb]); }, [caseTitle, appTitle, getAppUrl, applyBreadcrumbs]); }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index 4d453f64b6954..4f486d3337632 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -13,11 +13,11 @@ import { ConfigSchema } from '../config'; import { encryptionKeyRotationServiceMock } from '../crypto/index.mock'; export const routeDefinitionParamsMock = { - create: (config: Record = {}) => ({ + create: (config: Record = {}, buildFlavor: BuildFlavor = 'traditional') => ({ router: httpServiceMock.createRouter(), logger: loggingSystemMock.create().get(), config: ConfigSchema.validate(config) as ConfigType, encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(), - buildFlavor: 'traditional' as BuildFlavor, + buildFlavor, }), }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts index 9b9dba6108ff3..f387e94e80990 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.test.ts @@ -44,6 +44,7 @@ describe('Key rotation routes', () => { it('correctly defines route.', () => { expect(routeConfig.options).toEqual({ + access: 'public', tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], summary: `Rotate a key for encrypted saved objects`, description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. @@ -83,6 +84,25 @@ describe('Key rotation routes', () => { ); }); + it('defines route as internal when build flavor is serverless', () => { + const routeParamsMock = routeDefinitionParamsMock.create( + { keyRotation: { decryptionOnlyKeys: ['b'.repeat(32)] } }, + 'serverless' + ); + defineKeyRotationRoutes(routeParamsMock); + const [config] = routeParamsMock.router.post.mock.calls.find( + ([{ path }]) => path === '/api/encrypted_saved_objects/_rotate_key' + )!; + + expect(config.options).toEqual({ + access: 'internal', + tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], + summary: `Rotate a key for encrypted saved objects`, + description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. + NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`, + }); + }); + it('returns 400 if decryption only keys are not specified.', async () => { const routeParamsMock = routeDefinitionParamsMock.create(); defineKeyRotationRoutes(routeParamsMock); diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts index 80907497010da..272e74c3a69cb 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts @@ -41,7 +41,7 @@ export function defineKeyRotationRoutes({ }, options: { tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], - access: buildFlavor === 'serverless' ? 'internal' : undefined, + access: buildFlavor === 'serverless' ? 'internal' : 'public', summary: `Rotate a key for encrypted saved objects`, description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`, diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index b1473c56b37ed..efc564089fb43 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -173,7 +173,7 @@ export const SearchBar: FC = (opts) => { reportEvent.searchRequest(); } - const rawParams = parseSearchParams(searchValue.toLowerCase()); + const rawParams = parseSearchParams(searchValue.toLowerCase(), searchableTypes); let tagIds: string[] | undefined; if (taggingApi && rawParams.filters.tags) { tagIds = rawParams.filters.tags.map( diff --git a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts index c6df745be847f..8e24f599ce1d2 100644 --- a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts +++ b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.test.ts @@ -9,12 +9,12 @@ import { parseSearchParams } from './parse_search_params'; describe('parseSearchParams', () => { it('returns the correct term', () => { - const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello'); + const searchParams = parseSearchParams('tag:(my-tag OR other-tag) hello', []); expect(searchParams.term).toEqual('hello'); }); it('returns the raw query as `term` in case of parsing error', () => { - const searchParams = parseSearchParams('tag:((()^invalid'); + const searchParams = parseSearchParams('tag:((()^invalid', []); expect(searchParams).toEqual({ term: 'tag:((()^invalid', filters: {}, @@ -22,12 +22,12 @@ describe('parseSearchParams', () => { }); it('returns `undefined` term if query only contains field clauses', () => { - const searchParams = parseSearchParams('tag:(my-tag OR other-tag)'); + const searchParams = parseSearchParams('tag:(my-tag OR other-tag)', []); expect(searchParams.term).toBeUndefined(); }); it('returns correct filters when no field clause is defined', () => { - const searchParams = parseSearchParams('hello'); + const searchParams = parseSearchParams('hello', []); expect(searchParams.filters).toEqual({ tags: undefined, types: undefined, @@ -35,7 +35,7 @@ describe('parseSearchParams', () => { }); it('returns correct filters when field clauses are present', () => { - const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly'); + const searchParams = parseSearchParams('tag:foo type:bar hello tag:dolly', []); expect(searchParams).toEqual({ term: 'hello', filters: { @@ -46,7 +46,7 @@ describe('parseSearchParams', () => { }); it('considers unknown field clauses to be part of the raw search term', () => { - const searchParams = parseSearchParams('tag:foo unknown:bar hello'); + const searchParams = parseSearchParams('tag:foo unknown:bar hello', []); expect(searchParams).toEqual({ term: 'unknown:bar hello', filters: { @@ -56,7 +56,7 @@ describe('parseSearchParams', () => { }); it('handles aliases field clauses', () => { - const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello'); + const searchParams = parseSearchParams('tag:foo tags:bar type:dash types:board hello', []); expect(searchParams).toEqual({ term: 'hello', filters: { @@ -67,7 +67,7 @@ describe('parseSearchParams', () => { }); it('converts boolean and number values to string for known filters', () => { - const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello'); + const searchParams = parseSearchParams('tag:42 tags:true type:69 types:false hello', []); expect(searchParams).toEqual({ term: 'hello', filters: { @@ -76,4 +76,74 @@ describe('parseSearchParams', () => { }, }); }); + + it('converts multiword searchable types to phrases so they get picked up as types', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad', 'enterprise search']; + const searchParams = parseSearchParams( + 'type:canvas workpad types:canvas-workpad hello type:enterprise search type:not multiword', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello multiword', + filters: { + types: ['canvas workpad', 'enterprise search', 'not'], + }, + }); + }); + + it('parses correctly when multiword types are already quoted', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad']; + const searchParams = parseSearchParams( + `type:"canvas workpad" hello type:"dashboard"`, + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['canvas workpad', 'dashboard'], + }, + }); + }); + + it('parses correctly when there is whitespace between type keyword and value', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad']; + const searchParams = parseSearchParams( + 'type: canvas workpad hello type: dashboard', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['canvas workpad', 'dashboard'], + }, + }); + }); + + it('dedupes duplicate types', () => { + const mockSearchableMultiwordTypes = ['canvas-workpad']; + const searchParams = parseSearchParams( + 'type:canvas workpad hello type:dashboard type:canvas-workpad type:canvas workpad type:dashboard', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['canvas workpad', 'dashboard'], + }, + }); + }); + + it('handles whitespace removal even if there are no multiword types', () => { + const mockSearchableMultiwordTypes: string[] = []; + const searchParams = parseSearchParams( + 'hello type: dashboard', + mockSearchableMultiwordTypes + ); + expect(searchParams).toEqual({ + term: 'hello', + filters: { + types: ['dashboard'], + }, + }); + }); }); diff --git a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts index 1df6c1123a328..90ba36cce5fcb 100644 --- a/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts +++ b/x-pack/plugins/global_search_bar/public/search_syntax/parse_search_params.ts @@ -16,12 +16,54 @@ const aliasMap = { type: ['types'], }; -export const parseSearchParams = (term: string): ParsedSearchParams => { +// Converts multiword types to phrases by wrapping them in quotes and trimming whitespace after type keyword. Example: type: canvas workpad -> type:"canvas workpad". If the type is already wrapped in quotes or is a single word, it will only trim whitespace after type keyword. +const convertMultiwordTypesToPhrasesAndTrimWhitespace = ( + term: string, + multiWordTypes: string[] +): string => { + if (!multiWordTypes.length) { + return term.replace( + /(type:|types:)\s*([^"']*?)\b([^"'\s]+)/gi, + (_, typeKeyword, whitespace, typeValue) => `${typeKeyword}${whitespace.trim()}${typeValue}` + ); + } + + const typesPattern = multiWordTypes.join('|'); + const termReplaceRegex = new RegExp( + `(type:|types:)\\s*([^"']*?)\\b((${typesPattern})\\b|[^\\s"']+)`, + 'gi' + ); + + return term.replace(termReplaceRegex, (_, typeKeyword, whitespace, typeValue) => { + const trimmedTypeKeyword = `${typeKeyword}${whitespace.trim()}`; + + // If the type value is already wrapped in quotes, leave it as is + return /['"]/.test(typeValue) + ? `${trimmedTypeKeyword}${typeValue}` + : `${trimmedTypeKeyword}"${typeValue}"`; + }); +}; + +const dedupeTypes = (types: FilterValues): FilterValues => [ + ...new Set(types.map((item) => item.replace(/[-\s]+/g, ' ').trim())), +]; + +export const parseSearchParams = (term: string, searchableTypes: string[]): ParsedSearchParams => { const recognizedFields = knownFilters.concat(...Object.values(aliasMap)); let query: Query; + // Finds all multiword types that are separated by whitespace or hyphens + const multiWordSearchableTypesWhitespaceSeperated = searchableTypes + .filter((item) => /[ -]/.test(item)) + .map((item) => item.replace(/-/g, ' ')); + + const modifiedTerm = convertMultiwordTypesToPhrasesAndTrimWhitespace( + term, + multiWordSearchableTypesWhitespaceSeperated + ); + try { - query = Query.parse(term, { + query = Query.parse(modifiedTerm, { schema: { recognizedFields }, }); } catch (e) { @@ -42,7 +84,7 @@ export const parseSearchParams = (term: string): ParsedSearchParams => { term: searchTerm, filters: { tags: tags ? valuesToString(tags) : undefined, - types: types ? valuesToString(types) : undefined, + types: types ? dedupeTypes(valuesToString(types)) : undefined, }, }; }; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx index 6f13c33585bca..7941881ff2c51 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_enablement/enable_entity_model_button.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { EntityManagerUnauthorizedError } from '@kbn/entityManager-plugin/public'; import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { TechnicalPreviewBadge } from '@kbn/observability-shared-plugin/public'; import { useKibana } from '../../hooks/use_kibana'; import { Unauthorized } from './unauthorized_modal'; @@ -57,6 +58,8 @@ export function EnableEntityModelButton({ onSuccess }: { onSuccess: () => void } data-test-subj="inventoryInventoryPageTemplateFilledButton" fill onClick={handleEnablement} + iconType={() => } + iconSide="right" > {i18n.translate('xpack.inventory.noData.card.button', { defaultMessage: 'Enable', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx index 3b12e11d2ba7c..79db53f39c346 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/no_data_config.tsx @@ -33,8 +33,20 @@ export function getEntityManagerEnablement({ description: ( + {i18n.translate('xpack.inventory.noData.card.description.inventory', { + defaultMessage: 'Inventory', + })} + + ), link: ( { access: 'public', authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], + excludeFromOAS: true, }); expect(routeConfig.validate).toEqual({ body: undefined, @@ -170,7 +171,7 @@ describe('Common authentication routes', () => { }); it('correctly defines route.', async () => { - expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.options).toEqual({ access: 'internal' }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index e54d6e35f1669..b519171fd4fe6 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -48,6 +48,7 @@ export function defineCommonRoutes({ validate: { query: schema.object({}, { unknowns: 'allow' }) }, options: { access: 'public', + excludeFromOAS: true, authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], }, @@ -89,10 +90,11 @@ export function defineCommonRoutes({ '/internal/security/me', ...(buildFlavor !== 'serverless' ? ['/api/security/v1/me'] : []), ]) { + const deprecated = path === '/api/security/v1/me'; router.get( - { path, validate: false }, + { path, validate: false, options: { access: deprecated ? 'public' : 'internal' } }, createLicensedRouteHandler(async (context, request, response) => { - if (path === '/api/security/v1/me') { + if (deprecated) { logger.warn( `The "${basePath.serverBasePath}${path}" endpoint is deprecated and will be removed in the next major version.`, { tags: ['deprecation'] } diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts index 2c4ab9de1491b..69c3ce1700671 100644 --- a/x-pack/plugins/security/server/routes/authentication/oidc.ts +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -37,7 +37,7 @@ export function defineOIDCRoutes({ { path, validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => { const serverBasePath = basePath.serverBasePath; @@ -68,7 +68,7 @@ export function defineOIDCRoutes({ { path: '/internal/security/oidc/implicit.js', validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => { const serverBasePath = basePath.serverBasePath; @@ -106,7 +106,12 @@ export function defineOIDCRoutes({ { unknowns: 'allow' } ), }, - options: { authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW] }, + options: { + access: 'public', + excludeFromOAS: true, + authRequired: false, + tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], + }, }, createLicensedRouteHandler(async (context, request, response) => { const serverBasePath = basePath.serverBasePath; @@ -184,6 +189,8 @@ export function defineOIDCRoutes({ ), }, options: { + access: 'public', + excludeFromOAS: true, authRequired: false, xsrfRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], @@ -227,6 +234,8 @@ export function defineOIDCRoutes({ ), }, options: { + access: 'public', + excludeFromOAS: true, authRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], }, diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index e952d98a38649..f693d20354e89 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -56,6 +56,7 @@ describe('SAML authentication routes', () => { expect(routeConfig.options).toEqual({ access: 'public', authRequired: false, + excludeFromOAS: true, xsrfRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], }); diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index ddc31fbc88b89..3c72fd908e6c4 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -38,6 +38,7 @@ export function defineSAMLRoutes({ }, options: { access: 'public', + excludeFromOAS: true, authRequired: false, xsrfRequired: false, tags: [ROUTE_TAG_CAN_REDIRECT, ROUTE_TAG_AUTH_FLOW], diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts index 1d278aa676ac3..b7204faaa7ca4 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.ts @@ -26,6 +26,7 @@ export function defineGetPrivilegesRoutes({ router, authz }: RouteDefinitionPara ), }), }, + options: { access: 'public' }, }, createLicensedRouteHandler((context, request, response) => { const respectLicenseLevel = request.query.respectLicenseLevel !== 'false'; // if undefined resolve to true by default diff --git a/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts b/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts index 67254735b9a16..0af24ad8d8397 100644 --- a/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts +++ b/x-pack/plugins/security/server/routes/authorization/reset_session_page.ts @@ -12,7 +12,7 @@ export function resetSessionPageRoutes({ httpResources }: RouteDefinitionParams) { path: '/internal/security/reset_session_page.js', validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => { return response.renderJs({ diff --git a/x-pack/plugins/security/server/routes/session_management/index.ts b/x-pack/plugins/security/server/routes/session_management/index.ts index c095a77409975..041feea8a62fd 100644 --- a/x-pack/plugins/security/server/routes/session_management/index.ts +++ b/x-pack/plugins/security/server/routes/session_management/index.ts @@ -13,12 +13,5 @@ import type { RouteDefinitionParams } from '..'; export function defineSessionManagementRoutes(params: RouteDefinitionParams) { defineSessionInfoRoutes(params); defineSessionExtendRoutes(params); - - // The invalidate session API was introduced to address situations where the session index - // could grow rapidly - when session timeouts are disabled, or with anonymous access. - // In the serverless environment, sessions timeouts are always be enabled, and there is no - // anonymous access. This eliminates the need for an invalidate session HTTP API. - if (params.buildFlavor !== 'serverless') { - defineInvalidateSessionsRoutes(params); - } + defineInvalidateSessionsRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/session_management/invalidate.ts b/x-pack/plugins/security/server/routes/session_management/invalidate.ts index c7d27b835edf2..a45d8f00c1ca4 100644 --- a/x-pack/plugins/security/server/routes/session_management/invalidate.ts +++ b/x-pack/plugins/security/server/routes/session_management/invalidate.ts @@ -12,7 +12,11 @@ import type { RouteDefinitionParams } from '..'; /** * Defines routes required for session invalidation. */ -export function defineInvalidateSessionsRoutes({ router, getSession }: RouteDefinitionParams) { +export function defineInvalidateSessionsRoutes({ + router, + getSession, + buildFlavor, +}: RouteDefinitionParams) { router.post( { path: '/api/security/session/_invalidate', @@ -34,7 +38,12 @@ export function defineInvalidateSessionsRoutes({ router, getSession }: RouteDefi }), }, options: { - access: 'public', + // The invalidate session API was introduced to address situations where the session index + // could grow rapidly - when session timeouts are disabled, or with anonymous access. + // In the serverless environment, sessions timeouts are always be enabled, and there is no + // anonymous access. However, keeping this endpoint available internally in serverless would + // be useful in situations where we need to batch-invalidate user sessions. + access: buildFlavor === 'serverless' ? 'internal' : 'public', tags: ['access:sessionManagement'], summary: `Invalidate user sessions`, }, diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts index ef588ae1cfcfc..74eee1e129e2c 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -71,7 +71,7 @@ describe('Access agreement view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.options).toEqual({ excludeFromOAS: true }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index 3724892edd6df..823fbb0286f33 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -24,7 +24,7 @@ export function defineAccessAgreementRoutes({ const canHandleRequest = () => license.getFeatures().allowAccessAgreement; httpResources.register( - { path: '/security/access_agreement', validate: false }, + { path: '/security/access_agreement', validate: false, options: { excludeFromOAS: true } }, createLicensedRouteHandler(async (context, request, response) => canHandleRequest() ? response.renderCoreApp() diff --git a/x-pack/plugins/security/server/routes/views/account_management.ts b/x-pack/plugins/security/server/routes/views/account_management.ts index af49f325a25d2..4b3fbb78fed90 100644 --- a/x-pack/plugins/security/server/routes/views/account_management.ts +++ b/x-pack/plugins/security/server/routes/views/account_management.ts @@ -11,7 +11,8 @@ import type { RouteDefinitionParams } from '..'; * Defines routes required for the Account Management view. */ export function defineAccountManagementRoutes({ httpResources }: RouteDefinitionParams) { - httpResources.register({ path: '/security/account', validate: false }, (context, req, res) => - res.renderCoreApp() + httpResources.register( + { path: '/security/account', validate: false, options: { excludeFromOAS: true } }, + (context, req, res) => res.renderCoreApp() ); } diff --git a/x-pack/plugins/security/server/routes/views/capture_url.test.ts b/x-pack/plugins/security/server/routes/views/capture_url.test.ts index 1893ad6c9cb5f..4496ab341b085 100644 --- a/x-pack/plugins/security/server/routes/views/capture_url.test.ts +++ b/x-pack/plugins/security/server/routes/views/capture_url.test.ts @@ -34,7 +34,7 @@ describe('Capture URL view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.options).toEqual({ authRequired: false, excludeFromOAS: true }); expect(routeConfig.validate).toEqual({ body: undefined, diff --git a/x-pack/plugins/security/server/routes/views/capture_url.ts b/x-pack/plugins/security/server/routes/views/capture_url.ts index 8eff92d78999d..394b799ca1f9d 100644 --- a/x-pack/plugins/security/server/routes/views/capture_url.ts +++ b/x-pack/plugins/security/server/routes/views/capture_url.ts @@ -19,7 +19,7 @@ export function defineCaptureURLRoutes({ httpResources }: RouteDefinitionParams) validate: { query: schema.object({ next: schema.maybe(schema.string()) }, { unknowns: 'ignore' }), }, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, (context, request, response) => response.renderAnonymousCoreApp() ); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 850a533e3d93a..9aecb39750b1b 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -35,7 +35,7 @@ describe('LoggedOut view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.options).toEqual({ authRequired: false, excludeFromOAS: true }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.ts b/x-pack/plugins/security/server/routes/views/logged_out.ts index 360c0fb2c9b7c..66581f574def8 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.ts @@ -20,7 +20,7 @@ export function defineLoggedOutRoutes({ { path: '/security/logged_out', validate: false, - options: { authRequired: false }, + options: { authRequired: false, excludeFromOAS: true }, }, async (context, request, response) => { // Authentication flow isn't triggered automatically for this route, so we should explicitly diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index b19ef41ca9098..11797e20523e9 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -52,7 +52,7 @@ describe('Login view routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ authRequired: 'optional' }); + expect(routeConfig.options).toEqual({ authRequired: 'optional', excludeFromOAS: true }); expect(routeConfig.validate).toEqual({ body: undefined, diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 5d4468fcbba57..8cf8459d523b8 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -39,7 +39,7 @@ export function defineLoginRoutes({ { unknowns: 'allow' } ), }, - options: { authRequired: 'optional' }, + options: { authRequired: 'optional', excludeFromOAS: true }, }, async (context, request, response) => { // Default to true if license isn't available or it can't be resolved for some reason. diff --git a/x-pack/plugins/security/server/routes/views/logout.ts b/x-pack/plugins/security/server/routes/views/logout.ts index 3fb905ee10d37..d61f4e83083d2 100644 --- a/x-pack/plugins/security/server/routes/views/logout.ts +++ b/x-pack/plugins/security/server/routes/views/logout.ts @@ -12,7 +12,7 @@ import type { RouteDefinitionParams } from '..'; */ export function defineLogoutRoutes({ httpResources }: RouteDefinitionParams) { httpResources.register( - { path: '/logout', validate: false, options: { authRequired: false } }, + { path: '/logout', validate: false, options: { authRequired: false, excludeFromOAS: true } }, (context, request, response) => response.renderAnonymousCoreApp() ); } diff --git a/x-pack/plugins/security/server/routes/views/overwritten_session.ts b/x-pack/plugins/security/server/routes/views/overwritten_session.ts index 115f7ea0a093f..4ab57f2cc9e72 100644 --- a/x-pack/plugins/security/server/routes/views/overwritten_session.ts +++ b/x-pack/plugins/security/server/routes/views/overwritten_session.ts @@ -12,7 +12,7 @@ import type { RouteDefinitionParams } from '..'; */ export function defineOverwrittenSessionRoutes({ httpResources }: RouteDefinitionParams) { httpResources.register( - { path: '/security/overwritten_session', validate: false }, + { path: '/security/overwritten_session', validate: false, options: { excludeFromOAS: true } }, (context, req, res) => res.renderCoreApp() ); } diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts index a4659d8d98d5a..41615f24d011c 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts @@ -18,6 +18,19 @@ import { z } from '@kbn/zod'; import { Note } from '../model/components.gen'; +/** + * Filter notes based on their association with a document or saved object. + */ +export type AssociatedFilterType = z.infer; +export const AssociatedFilterType = z.enum([ + 'document_only', + 'saved_object_only', + 'document_and_saved_object', + 'orphan', +]); +export type AssociatedFilterTypeEnum = typeof AssociatedFilterType.enum; +export const AssociatedFilterTypeEnum = AssociatedFilterType.enum; + export type DocumentIds = z.infer; export const DocumentIds = z.union([z.array(z.string()), z.string()]); @@ -41,6 +54,7 @@ export const GetNotesRequestQuery = z.object({ sortOrder: z.string().nullable().optional(), filter: z.string().nullable().optional(), userFilter: z.string().nullable().optional(), + associatedFilter: AssociatedFilterType.optional(), }); export type GetNotesRequestQueryInput = z.input; diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml index cc8681c6f8f64..734a9580dcd23 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml @@ -56,6 +56,10 @@ paths: schema: nullable: true type: string + - name: associatedFilter + in: query + schema: + $ref: '#/components/schemas/AssociatedFilterType' responses: '200': description: Indicates the requested notes were returned. @@ -68,6 +72,14 @@ paths: components: schemas: + AssociatedFilterType: + type: string + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + description: Filter notes based on their association with a document or saved object. DocumentIds: oneOf: - type: array diff --git a/x-pack/plugins/security_solution/common/notes/constants.ts b/x-pack/plugins/security_solution/common/notes/constants.ts new file mode 100644 index 0000000000000..c296e377d1c4f --- /dev/null +++ b/x-pack/plugins/security_solution/common/notes/constants.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 enum AssociatedFilter { + all = 'all', + documentOnly = 'document_only', + savedObjectOnly = 'saved_object_only', + documentAndSavedObject = 'document_and_saved_object', + orphan = 'orphan', +} diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 8de192ce26826..e48dafbdc0e05 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -102,6 +102,10 @@ paths: schema: nullable: true type: string + - in: query + name: associatedFilter + schema: + $ref: '#/components/schemas/AssociatedFilterType' responses: '200': content: @@ -921,6 +925,14 @@ paths: - access:securitySolution components: schemas: + AssociatedFilterType: + description: Filter notes based on their association with a document or saved object. + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + type: string BareNote: type: object properties: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 66127d5b8cd52..fab5e022c6b06 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -102,6 +102,10 @@ paths: schema: nullable: true type: string + - in: query + name: associatedFilter + schema: + $ref: '#/components/schemas/AssociatedFilterType' responses: '200': content: @@ -921,6 +925,14 @@ paths: - access:securitySolution components: schemas: + AssociatedFilterType: + description: Filter notes based on their association with a document or saved object. + enum: + - document_only + - saved_object_only + - document_and_saved_object + - orphan + type: string BareNote: type: object properties: diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 01eec48ed7718..5874062f05523 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -7,6 +7,7 @@ import { TableId } from '@kbn/securitysolution-data-table'; import type { DataViewSpec } from '@kbn/data-views-plugin/public'; +import { AssociatedFilter } from '../../../common/notes/constants'; import { ReqStatus } from '../../notes/store/notes.slice'; import { HostsFields } from '../../../common/api/search_strategy/hosts/model/sort'; import { InputsModelId } from '../store/inputs/constants'; @@ -550,6 +551,7 @@ export const mockGlobalState: State = { }, filter: '', userFilter: '', + associatedFilter: AssociatedFilter.all, search: '', selectedIds: [], pendingDeleteIds: [], diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx index 0ce75c714f89f..fa22892f21db8 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock'; import { ScheduleRiskEngineCallout } from './schedule_risk_engine_callout'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; const oneHourFromNow = () => { const date = new Date(); @@ -48,7 +49,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: oneHourFromNow().toISOString(), @@ -70,7 +71,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'running', runAt: oneHourFromNow().toISOString(), @@ -89,7 +90,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: new Date().toISOString(), // past date @@ -110,7 +111,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValueOnce({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: oneHourFromNow(), @@ -127,7 +128,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValueOnce({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: thirtyMinutesFromNow(), @@ -143,7 +144,7 @@ describe('ScheduleRiskEngineCallout', () => { mockUseRiskEngineStatus.mockReturnValue({ data: { isNewRiskScoreModuleInstalled: true, - + risk_engine_status: RiskEngineStatusEnum.ENABLED, risk_engine_task_status: { status: 'idle', runAt: new Date().toISOString(), // past date @@ -173,4 +174,19 @@ describe('ScheduleRiskEngineCallout', () => { expect(queryByTestId('risk-engine-callout')).toBeNull(); }); + + it('should not show the callout if the risk engine is disabled', () => { + mockUseRiskEngineStatus.mockReturnValue({ + data: { + isNewRiskScoreModuleInstalled: true, + risk_engine_status: RiskEngineStatusEnum.DISABLED, + }, + }); + + const { queryByTestId } = render(, { + wrapper: TestProviders, + }); + + expect(queryByTestId('risk-engine-callout')).toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx index 2e8722cd20005..432e03c231a4d 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/asset_criticality_file_uploader/components/schedule_risk_engine_callout.tsx @@ -16,6 +16,7 @@ import React, { useCallback, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { RiskEngineStatusEnum } from '../../../../../common/api/entity_analytics'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { formatTimeFromNow } from '../helpers'; import { useScheduleNowRiskEngineMutation } from '../../../api/hooks/use_schedule_now_risk_engine_mutation'; @@ -76,7 +77,10 @@ export const ScheduleRiskEngineCallout: React.FC = () => { scheduleRiskEngineMutation(); }, [scheduleRiskEngineMutation]); - if (!riskEngineStatus?.isNewRiskScoreModuleInstalled) { + if ( + !riskEngineStatus?.isNewRiskScoreModuleInstalled || + riskEngineStatus?.risk_engine_status !== RiskEngineStatusEnum.ENABLED + ) { return null; } diff --git a/x-pack/plugins/security_solution/public/notes/api/api.ts b/x-pack/plugins/security_solution/public/notes/api/api.ts index 3bac1a0a2d7df..917974a154884 100644 --- a/x-pack/plugins/security_solution/public/notes/api/api.ts +++ b/x-pack/plugins/security_solution/public/notes/api/api.ts @@ -11,6 +11,7 @@ import type { GetNotesResponse, PersistNoteRouteResponse, } from '../../../common/api/timeline'; +import type { AssociatedFilter } from '../../../common/notes/constants'; import { KibanaServices } from '../../common/lib/kibana'; import { NOTE_URL } from '../../../common/constants'; @@ -43,6 +44,7 @@ export const fetchNotes = async ({ sortOrder, filter, userFilter, + associatedFilter, search, }: { page: number; @@ -51,6 +53,7 @@ export const fetchNotes = async ({ sortOrder: string; filter: string; userFilter: string; + associatedFilter: AssociatedFilter; search: string; }) => { const response = await KibanaServices.get().http.get(NOTE_URL, { @@ -61,6 +64,7 @@ export const fetchNotes = async ({ sortOrder, filter, userFilter, + associatedFilter, search, }, version: '2023-10-31', diff --git a/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx b/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx index 71693edb81724..be9546c77525b 100644 --- a/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx @@ -9,7 +9,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { SearchRow } from './search_row'; -import { SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; +import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; +import { AssociatedFilter } from '../../../common/notes/constants'; import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; jest.mock('../../common/components/user_profiles/use_suggest_users'); @@ -38,6 +39,7 @@ describe('SearchRow', () => { expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); expect(getByTestId(USER_SELECT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(ASSOCIATED_NOT_SELECT_TEST_ID)).toBeInTheDocument(); }); it('should call the correct action when entering a value in the search bar', async () => { @@ -62,4 +64,13 @@ describe('SearchRow', () => { expect(mockDispatch).toHaveBeenCalled(); }); + + it('should call the correct action when select a value in the associated note dropdown', async () => { + const { getByTestId } = render(); + + const associatedNoteSelect = getByTestId(ASSOCIATED_NOT_SELECT_TEST_ID); + await userEvent.selectOptions(associatedNoteSelect, [AssociatedFilter.documentOnly]); + + expect(mockDispatch).toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/security_solution/public/notes/components/search_row.tsx b/x-pack/plugins/security_solution/public/notes/components/search_row.tsx index 9a33c84cbec58..d540a586814d8 100644 --- a/x-pack/plugins/security_solution/public/notes/components/search_row.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/search_row.tsx @@ -5,30 +5,48 @@ * 2.0. */ -import { EuiComboBox, EuiFlexGroup, EuiFlexItem, EuiSearchBar } from '@elastic/eui'; import React, { useMemo, useCallback, useState } from 'react'; +import type { EuiSelectOption } from '@elastic/eui'; +import { + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiSearchBar, + EuiSelect, + useGeneratedHtmlId, +} from '@elastic/eui'; import { useDispatch } from 'react-redux'; import type { UserProfileWithAvatar } from '@kbn/user-profile-components'; import { i18n } from '@kbn/i18n'; import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; -import { SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; +import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; -import { userFilterUsers, userSearchedNotes } from '..'; +import { userFilterAssociatedNotes, userFilterUsers, userSearchedNotes } from '..'; +import { AssociatedFilter } from '../../../common/notes/constants'; export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', { defaultMessage: 'Users', }); +const FILTER_SELECT = i18n.translate('xpack.securitySolution.notes.management.filterSelect', { + defaultMessage: 'Select filter', +}); + +const searchBox = { + placeholder: 'Search note contents', + incremental: false, + 'data-test-subj': SEARCH_BAR_TEST_ID, +}; +const associatedNoteSelectOptions: EuiSelectOption[] = [ + { value: AssociatedFilter.all, text: 'All' }, + { value: AssociatedFilter.documentOnly, text: 'Attached to document only' }, + { value: AssociatedFilter.savedObjectOnly, text: 'Attached to timeline only' }, + { value: AssociatedFilter.documentAndSavedObject, text: 'Attached to document and timeline' }, + { value: AssociatedFilter.orphan, text: 'Orphan' }, +]; export const SearchRow = React.memo(() => { const dispatch = useDispatch(); - const searchBox = useMemo( - () => ({ - placeholder: 'Search note contents', - incremental: false, - 'data-test-subj': SEARCH_BAR_TEST_ID, - }), - [] - ); + const associatedSelectId = useGeneratedHtmlId({ prefix: 'associatedSelectId' }); const onQueryChange = useCallback( ({ queryText }: { queryText: string }) => { @@ -57,6 +75,13 @@ export const SearchRow = React.memo(() => { [dispatch] ); + const onAssociatedNoteSelectChange = useCallback( + (e: React.ChangeEvent) => { + dispatch(userFilterAssociatedNotes(e.target.value as AssociatedFilter)); + }, + [dispatch] + ); + return ( @@ -73,6 +98,16 @@ export const SearchRow = React.memo(() => { data-test-subj={USER_SELECT_TEST_ID} /> + + + ); }); diff --git a/x-pack/plugins/security_solution/public/notes/components/test_ids.ts b/x-pack/plugins/security_solution/public/notes/components/test_ids.ts index 1464ed17d8764..e056ca19d6a2e 100644 --- a/x-pack/plugins/security_solution/public/notes/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/notes/components/test_ids.ts @@ -21,3 +21,4 @@ export const NOTE_CONTENT_BUTTON_TEST_ID = `${PREFIX}NoteContentButton` as const export const NOTE_CONTENT_POPOVER_TEST_ID = `${PREFIX}NoteContentPopover` as const; export const SEARCH_BAR_TEST_ID = `${PREFIX}SearchBar` as const; export const USER_SELECT_TEST_ID = `${PREFIX}UserSelect` as const; +export const ASSOCIATED_NOT_SELECT_TEST_ID = `${PREFIX}AssociatedNoteSelect` as const; diff --git a/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx b/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx index e34824d1ad814..83c581507a2f9 100644 --- a/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/utility_bar.tsx @@ -24,6 +24,7 @@ import { selectNotesTableSearch, userSelectedBulkDelete, selectNotesTableUserFilters, + selectNotesTableAssociatedFilter, } from '..'; export const BATCH_ACTIONS = i18n.translate( @@ -53,6 +54,7 @@ export const NotesUtilityBar = React.memo(() => { const sort = useSelector(selectNotesTableSort); const selectedItems = useSelector(selectNotesTableSelectedIds); const notesUserFilters = useSelector(selectNotesTableUserFilters); + const notesAssociatedFilters = useSelector(selectNotesTableAssociatedFilter); const resultsCount = useMemo(() => { const { perPage, page, total } = pagination; const startOfCurrentPage = perPage * (page - 1) + 1; @@ -86,6 +88,7 @@ export const NotesUtilityBar = React.memo(() => { sortOrder: sort.direction, filter: '', userFilter: notesUserFilters, + associatedFilter: notesAssociatedFilters, search: notesSearch, }) ); @@ -96,6 +99,7 @@ export const NotesUtilityBar = React.memo(() => { sort.field, sort.direction, notesUserFilters, + notesAssociatedFilters, notesSearch, ]); return ( diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx index e329f0d75b911..4795d6146be4d 100644 --- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx +++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx @@ -37,6 +37,7 @@ import { selectFetchNotesError, ReqStatus, selectNotesTableUserFilters, + selectNotesTableAssociatedFilter, } from '..'; import type { NotesState } from '..'; import { SearchRow } from '../components/search_row'; @@ -121,6 +122,7 @@ export const NoteManagementPage = () => { const sort = useSelector(selectNotesTableSort); const notesSearch = useSelector(selectNotesTableSearch); const notesUserFilters = useSelector(selectNotesTableUserFilters); + const notesAssociatedFilters = useSelector(selectNotesTableAssociatedFilter); const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds); const isDeleteModalVisible = pendingDeleteIds.length > 0; const fetchNotesStatus = useSelector(selectFetchNotesStatus); @@ -137,6 +139,7 @@ export const NoteManagementPage = () => { sortOrder: sort.direction, filter: '', userFilter: notesUserFilters, + associatedFilter: notesAssociatedFilters, search: notesSearch, }) ); @@ -147,6 +150,7 @@ export const NoteManagementPage = () => { sort.field, sort.direction, notesUserFilters, + notesAssociatedFilters, notesSearch, ]); diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts index 7cbaecf7d7135..65fa293bd824a 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts @@ -5,13 +5,15 @@ * 2.0. */ import * as uuid from 'uuid'; -import { miniSerializeError } from '@reduxjs/toolkit'; import type { SerializedError } from '@reduxjs/toolkit'; +import { miniSerializeError } from '@reduxjs/toolkit'; +import type { NotesState } from './notes.slice'; import { createNote, deleteNotes, - fetchNotesByDocumentIds, fetchNotes, + fetchNotesByDocumentIds, + fetchNotesBySavedObjectIds, initialNotesState, notesReducer, ReqStatus, @@ -20,6 +22,7 @@ import { selectCreateNoteStatus, selectDeleteNotesError, selectDeleteNotesStatus, + selectDocumentNotesBySavedObjectId, selectFetchNotesByDocumentIdsError, selectFetchNotesByDocumentIdsStatus, selectFetchNotesError, @@ -27,12 +30,16 @@ import { selectNoteById, selectNoteIds, selectNotesByDocumentId, - selectDocumentNotesBySavedObjectId, + selectNotesBySavedObjectId, selectNotesPagination, selectNotesTablePendingDeleteIds, selectNotesTableSearch, selectNotesTableSelectedIds, selectNotesTableSort, + selectSortedNotesByDocumentId, + selectSortedNotesBySavedObjectId, + selectNotesTableUserFilters, + selectNotesTableAssociatedFilter, userClosedDeleteModal, userFilteredNotes, userSearchedNotes, @@ -42,17 +49,13 @@ import { userSelectedRow, userSelectedNotesForDeletion, userSortedNotes, - selectSortedNotesByDocumentId, - fetchNotesBySavedObjectIds, - selectNotesBySavedObjectId, - selectSortedNotesBySavedObjectId, userFilterUsers, - selectNotesTableUserFilters, userClosedCreateErrorToast, + userFilterAssociatedNotes, } from './notes.slice'; -import type { NotesState } from './notes.slice'; import { mockGlobalState } from '../../common/mock'; import type { Note } from '../../../common/api/timeline'; +import { AssociatedFilter } from '../../../common/notes/constants'; const initalEmptyState = initialNotesState; @@ -102,6 +105,7 @@ const initialNonEmptyState: NotesState = { }, filter: '', userFilter: '', + associatedFilter: AssociatedFilter.all, search: '', selectedIds: [], pendingDeleteIds: [], @@ -515,6 +519,17 @@ describe('notesSlice', () => { }); }); + describe('userFilterAssociatedNotes', () => { + it('should set correct value to filter associated notes', () => { + const action = { type: userFilterAssociatedNotes.type, payload: 'abc' }; + + expect(notesReducer(initalEmptyState, action)).toEqual({ + ...initalEmptyState, + associatedFilter: 'abc', + }); + }); + }); + describe('userSearchedNotes', () => { it('should set correct value to search notes', () => { const action = { type: userSearchedNotes.type, payload: 'abc' }; @@ -851,7 +866,7 @@ describe('notesSlice', () => { expect(selectNotesTableSearch(state)).toBe('test search'); }); - it('should select associated filter', () => { + it('should select user filter', () => { const state = { ...mockGlobalState, notes: { ...initialNotesState, userFilter: 'abc' }, @@ -859,6 +874,14 @@ describe('notesSlice', () => { expect(selectNotesTableUserFilters(state)).toBe('abc'); }); + it('should select associated filter', () => { + const state = { + ...mockGlobalState, + notes: { ...initialNotesState, associatedFilter: AssociatedFilter.all }, + }; + expect(selectNotesTableAssociatedFilter(state)).toBe(AssociatedFilter.all); + }); + it('should select notes table pending delete ids', () => { const state = { ...mockGlobalState, diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts index d5a4e7d4ab14e..28bf609a4f210 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts @@ -8,6 +8,7 @@ import type { EntityState, SerializedError } from '@reduxjs/toolkit'; import { createAsyncThunk, createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { createSelector } from 'reselect'; +import { AssociatedFilter } from '../../../common/notes/constants'; import type { State } from '../../common/store'; import { createNote as createNoteApi, @@ -59,6 +60,7 @@ export interface NotesState extends EntityState { filter: string; userFilter: string; search: string; + associatedFilter: AssociatedFilter; selectedIds: string[]; pendingDeleteIds: string[]; } @@ -93,6 +95,7 @@ export const initialNotesState: NotesState = notesAdapter.getInitialState({ }, filter: '', userFilter: '', + associatedFilter: AssociatedFilter.all, search: '', selectedIds: [], pendingDeleteIds: [], @@ -127,11 +130,13 @@ export const fetchNotes = createAsyncThunk< sortOrder: string; filter: string; userFilter: string; + associatedFilter: AssociatedFilter; search: string; }, {} >('notes/fetchNotes', async (args) => { - const { page, perPage, sortField, sortOrder, filter, userFilter, search } = args; + const { page, perPage, sortField, sortOrder, filter, userFilter, associatedFilter, search } = + args; const res = await fetchNotesApi({ page, perPage, @@ -139,6 +144,7 @@ export const fetchNotes = createAsyncThunk< sortOrder, filter, userFilter, + associatedFilter, search, }); return { @@ -163,7 +169,7 @@ export const deleteNotes = createAsyncThunk { state.userFilter = action.payload; }, + userFilterAssociatedNotes: (state: NotesState, action: { payload: AssociatedFilter }) => { + state.associatedFilter = action.payload; + }, userSearchedNotes: (state: NotesState, action: { payload: string }) => { state.search = action.payload; }, @@ -324,6 +334,8 @@ export const selectNotesTableSearch = (state: State) => state.notes.search; export const selectNotesTableUserFilters = (state: State) => state.notes.userFilter; +export const selectNotesTableAssociatedFilter = (state: State) => state.notes.associatedFilter; + export const selectNotesTablePendingDeleteIds = (state: State) => state.notes.pendingDeleteIds; export const selectFetchNotesError = (state: State) => state.notes.error.fetchNotes; @@ -412,6 +424,7 @@ export const { userSortedNotes, userFilteredNotes, userFilterUsers, + userFilterAssociatedNotes, userSearchedNotes, userSelectedRow, userClosedDeleteModal, diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts index 6699e160634fd..32cb52a61d469 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/united_entity_definitions/get_united_definition.ts @@ -48,9 +48,7 @@ export const getUnitedEntityDefinition = memoize( namespace, indexPatterns, }); - }, - ({ entityType, namespace, fieldHistoryLength }: Options) => - `${entityType}-${namespace}-${fieldHistoryLength}` + } ); export const getUnitedEntityDefinitionVersion = (entityType: EntityType): string => 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 bc6c83e2b159c..7b8c732ae54ca 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 @@ -9,9 +9,13 @@ import type { IKibanaResponse } from '@kbn/core-http-server'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; -import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server'; -import { nodeBuilder } from '@kbn/es-query'; +import type { + SavedObjectsFindOptions, + SavedObjectsFindOptionsReference, +} from '@kbn/core-saved-objects-api-server'; import type { KueryNode } from '@kbn/es-query'; +import { nodeBuilder, nodeTypes } from '@kbn/es-query'; +import { AssociatedFilter } from '../../../../../common/notes/constants'; import { timelineSavedObjectType } from '../../saved_object_mappings'; import type { SecuritySolutionPluginRouter } from '../../../../types'; import { MAX_UNASSOCIATED_NOTES, NOTE_URL } from '../../../../../common/constants'; @@ -22,6 +26,7 @@ import { getAllSavedNote } from '../../saved_object/notes'; import { noteSavedObjectType } from '../../saved_object_mappings/notes'; import { GetNotesRequestQuery, type GetNotesResponse } from '../../../../../common/api/timeline'; +/* eslint-disable complexity */ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => { router.versioned .get({ @@ -128,21 +133,70 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => { filter, }; + // we need to combine the associatedFilter with the filter query + // we have to type case here because the filter is a string (from the schema) and that cannot be changed as it would be a breaking change + const filterAsKueryNode: KueryNode = (filter || '') as unknown as KueryNode; + const filterKueryNodeArray = [filterAsKueryNode]; + // retrieve all the notes created by a specific user const userFilter = queryParams?.userFilter; if (userFilter) { - // we need to combine the associatedFilter with the filter query - // we have to type case here because the filter is a string (from the schema) and that cannot be changed as it would be a breaking change - const filterAsKueryNode: KueryNode = (filter || '') as unknown as KueryNode; - - options.filter = nodeBuilder.and([ - nodeBuilder.is(`${noteSavedObjectType}.attributes.createdBy`, userFilter), - filterAsKueryNode, - ]); - } else { - options.filter = filter; + filterKueryNodeArray.push( + nodeBuilder.is(`${noteSavedObjectType}.attributes.createdBy`, userFilter) + ); + } + + const associatedFilter = queryParams?.associatedFilter; + if (associatedFilter) { + // select documents that have or don't have a reference to an empty value + // used in combination with hasReference (not associated with a timeline) or hasNoReference (associated with a timeline) + const referenceToATimeline: SavedObjectsFindOptionsReference = { + type: timelineSavedObjectType, + id: '', + }; + + // select documents that don't have a value in the eventId field (not associated with a document) + const emptyDocumentIdFilter: KueryNode = nodeBuilder.is( + `${noteSavedObjectType}.attributes.eventId`, + '' + ); + + switch (associatedFilter) { + case AssociatedFilter.documentOnly: + // select documents that have a reference to an empty saved object id (not associated with a timeline) + // and have a value in the eventId field (associated with a document) + options.hasReference = referenceToATimeline; + filterKueryNodeArray.push( + nodeTypes.function.buildNode('not', emptyDocumentIdFilter) + ); + break; + case AssociatedFilter.savedObjectOnly: + // select documents that don't have a reference to an empty saved object id (associated with a timeline) + // and don't have a value in the eventId field (not associated with a document) + options.hasNoReference = referenceToATimeline; + filterKueryNodeArray.push(emptyDocumentIdFilter); + break; + case AssociatedFilter.documentAndSavedObject: + // select documents that don't have a reference to an empty saved object id (associated with a timeline) + // and have a value in the eventId field (associated with a document) + options.hasNoReference = referenceToATimeline; + filterKueryNodeArray.push( + nodeTypes.function.buildNode('not', emptyDocumentIdFilter) + ); + break; + case AssociatedFilter.orphan: + // select documents that have a reference to an empty saved object id (not associated with a timeline) + // and don't have a value in the eventId field (not associated with a document) + options.hasReference = referenceToATimeline; + // TODO we might want to also check for the existence of the eventId field, on top of getting eventId having empty values + filterKueryNodeArray.push(emptyDocumentIdFilter); + break; + } } + // combine all filters + options.filter = nodeBuilder.and(filterKueryNodeArray); + const res = await getAllSavedNote(frameworkRequest, options); const body: GetNotesResponse = res ?? {}; return response.ok({ body }); diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx index bb55cea5cd50f..f586f0d7f035e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_content_tab.test.tsx @@ -19,12 +19,13 @@ import { import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceContentTab } from './edit_space_content_tab'; -import { EditSpaceProvider } from './provider'; +import { EditSpaceProviderRoot } from './provider'; import type { Space } from '../../../common'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import type { SpaceContentTypeSummaryItem } from '../../types'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); @@ -42,7 +43,7 @@ const logger = loggingSystemMock.createLogger(); const TestComponent: React.FC = ({ children }) => { return ( - = ({ children }) => { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx index 1c32b97f777c0..9a35572254340 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_general_tab.test.tsx @@ -23,12 +23,13 @@ import { KibanaFeature } from '@kbn/features-plugin/common'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceSettingsTab } from './edit_space_general_tab'; -import { EditSpaceProvider } from './provider/edit_space_provider'; +import { EditSpaceProviderRoot } from './provider/edit_space_provider'; import type { SolutionView } from '../../../common'; import { SOLUTION_VIEW_CLASSIC } from '../../../common/constants'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const space = { id: 'default', name: 'Default', disabledFeatures: [], _reserved: true }; const history = scopedHistoryMock.create(); @@ -64,7 +65,7 @@ describe('EditSpaceSettings', () => { const TestComponent: React.FC = ({ children }) => { return ( - { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx index 882301d36459a..bf59f00b5490d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_page.tsx @@ -9,9 +9,9 @@ import React from 'react'; import type { ComponentProps, PropsWithChildren } from 'react'; import { EditSpace } from './edit_space'; -import { EditSpaceProvider, type EditSpaceProviderProps } from './provider'; +import { EditSpaceProviderRoot, type EditSpaceProviderRootProps } from './provider'; -type EditSpacePageProps = ComponentProps & EditSpaceProviderProps; +type EditSpacePageProps = ComponentProps & EditSpaceProviderRootProps; export function EditSpacePage({ spaceId, @@ -25,7 +25,7 @@ export function EditSpacePage({ ...editSpaceServicesProps }: PropsWithChildren) { return ( - + - + ); } diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx index fccd999eb7941..1959d1d8465ac 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.test.tsx @@ -19,10 +19,11 @@ import { import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { EditSpaceAssignedRolesTab } from './edit_space_roles_tab'; -import { EditSpaceProvider } from './provider'; +import { EditSpaceProviderRoot } from './provider'; import { spacesManagerMock } from '../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../security_license.mock'; const getUrlForApp = (appId: string) => appId; const navigateToUrl = jest.fn(); @@ -51,7 +52,7 @@ describe('EditSpaceAssignedRolesTab', () => { const TestComponent: React.FC = ({ children }) => { return ( - { notifications={notifications} overlays={overlays} getPrivilegesAPIClient={getPrivilegeAPIClient} + getSecurityLicense={getSecurityLicenseMock} theme={theme} i18n={i18n} logger={logger} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx index 2733790d8de8b..2e3d40527dbd7 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/edit_space_roles_tab.tsx @@ -62,7 +62,7 @@ export const EditSpaceAssignedRolesTab: FC = ({ space, features, isReadOn (defaultSelected?: Role[]) => { const overlayRef = overlays.openFlyout( toMountPoint( - + = ({ space, features, isReadOn [ overlays, services, + dispatch, + state, space, features, - dispatch, invokeClient, getUrlForApp, theme, diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx index bfd7d7b6059e8..a236b9bc05e1d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.test.tsx @@ -20,10 +20,15 @@ import { import type { ApplicationStart } from '@kbn/core-application-browser'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider'; +import { + EditSpaceProviderRoot, + useEditSpaceServices, + useEditSpaceStore, +} from './edit_space_provider'; import { spacesManagerMock } from '../../../spaces_manager/spaces_manager.mock'; import { getPrivilegeAPIClientMock } from '../../privilege_api_client.mock'; import { getRolesAPIClientMock } from '../../roles_api_client.mock'; +import { getSecurityLicenseMock } from '../../security_license.mock'; const http = httpServiceMock.createStartContract(); const notifications = notificationServiceMock.createStartContract(); @@ -45,7 +50,7 @@ const SUTProvider = ({ }: PropsWithChildren>>) => { return ( - _, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: getPrivilegeAPIClientMock, + getSecurityLicense: getSecurityLicenseMock, navigateToUrl: jest.fn(), capabilities, }} > {children} - + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx index 75af2beea2108..374d90d19ace1 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/edit_space_provider.tsx @@ -23,6 +23,7 @@ import type { Logger } from '@kbn/logging'; import type { PrivilegesAPIClientPublicContract, RolesAPIClient, + SecurityLicense, } from '@kbn/security-plugin-types-public'; import { @@ -32,7 +33,7 @@ import { } from './reducers'; import type { SpacesManager } from '../../../spaces_manager'; -export interface EditSpaceProviderProps +export interface EditSpaceProviderRootProps extends Pick { logger: Logger; capabilities: ApplicationStart['capabilities']; @@ -42,10 +43,7 @@ export interface EditSpaceProviderProps spacesManager: SpacesManager; getRolesAPIClient: () => Promise; getPrivilegesAPIClient: () => Promise; -} - -export interface EditSpaceServices extends EditSpaceProviderProps { - invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; + getSecurityLicense: () => Promise; } interface EditSpaceClients { @@ -54,6 +52,15 @@ interface EditSpaceClients { privilegesClient: PrivilegesAPIClientPublicContract; } +export interface EditSpaceServices + extends Omit< + EditSpaceProviderRootProps, + 'getRolesAPIClient' | 'getPrivilegesAPIClient' | 'getSecurityLicense' + > { + invokeClient(arg: (clients: EditSpaceClients) => Promise): Promise; + license?: SecurityLicense; +} + export interface EditSpaceStore { state: IEditSpaceStoreState; dispatch: Dispatch; @@ -63,16 +70,43 @@ const createSpaceRolesContext = once(() => createContext( const createEditSpaceServicesContext = once(() => createContext(null)); +/** + * + * @description EditSpaceProvider is a provider component that wraps the children components with the necessary context providers for the Edit Space feature. It provides the necessary services and state management for the feature, + * this is provided as an export for use with out of band renders within the spaces app + */ export const EditSpaceProvider = ({ children, + state, + dispatch, ...services -}: PropsWithChildren) => { +}: PropsWithChildren) => { const EditSpaceStoreContext = createSpaceRolesContext(); const EditSpaceServicesContext = createEditSpaceServicesContext(); - const clients = useRef( - Promise.all([services.getRolesAPIClient(), services.getPrivilegesAPIClient()]) + return ( + + + {children} + + ); +}; + +/** + * @description EditSpaceProviderRoot is the root provider for the Edit Space feature. It instantiates the necessary services and state management for the feature. It ideally + * should only be rendered once + */ +export const EditSpaceProviderRoot = ({ + children, + ...services +}: PropsWithChildren) => { + const { logger, getRolesAPIClient, getPrivilegesAPIClient, getSecurityLicense } = services; + + const clients = useRef(Promise.all([getRolesAPIClient(), getPrivilegesAPIClient()])); + const license = useRef(getSecurityLicense); + + const licenseRef = useRef(); const rolesAPIClientRef = useRef(); const privilegesClientRef = useRef(); @@ -81,7 +115,14 @@ export const EditSpaceProvider = ({ fetchRolesError: false, }); - const { logger } = services; + const resolveSecurityLicense = useCallback(async () => { + try { + licenseRef.current = await license.current(); + } catch (err) { + logger.error('Could not resolve Security License!', err); + } + }, [logger]); + const resolveAPIClients = useCallback(async () => { try { [rolesAPIClientRef.current, privilegesClientRef.current] = await clients.current; @@ -94,6 +135,10 @@ export const EditSpaceProvider = ({ resolveAPIClients(); }, [resolveAPIClients]); + useEffect(() => { + resolveSecurityLicense(); + }, [resolveSecurityLicense]); + const createInitialState = useCallback((state: IEditSpaceStoreState) => { return state; }, []); @@ -118,11 +163,11 @@ export const EditSpaceProvider = ({ ); return ( - - - {children} - - + + {children} + ); }; diff --git a/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts index 7ae7301cd2c60..405f59c44a6f8 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts +++ b/x-pack/plugins/spaces/public/management/edit_space/provider/index.ts @@ -5,9 +5,14 @@ * 2.0. */ -export { EditSpaceProvider, useEditSpaceServices, useEditSpaceStore } from './edit_space_provider'; +export { + EditSpaceProviderRoot, + EditSpaceProvider, + useEditSpaceServices, + useEditSpaceStore, +} from './edit_space_provider'; export type { - EditSpaceProviderProps, + EditSpaceProviderRootProps, EditSpaceServices, EditSpaceStore, } from './edit_space_provider'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index 3595cefd1220c..7f99202e23791 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import crypto from 'crypto'; import React from 'react'; @@ -19,7 +19,7 @@ import { themeServiceMock, } from '@kbn/core/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import type { Role } from '@kbn/security-plugin-types-common'; +import type { Role, SecurityLicense } from '@kbn/security-plugin-types-common'; import { createRawKibanaPrivileges, kibanaFeatures, @@ -33,11 +33,8 @@ import { FEATURE_PRIVILEGES_READ, } from '../../../../../common/constants'; import { spacesManagerMock } from '../../../../spaces_manager/spaces_manager.mock'; -import { - createPrivilegeAPIClientMock, - getPrivilegeAPIClientMock, -} from '../../../privilege_api_client.mock'; -import { createRolesAPIClientMock, getRolesAPIClientMock } from '../../../roles_api_client.mock'; +import { createPrivilegeAPIClientMock } from '../../../privilege_api_client.mock'; +import { createRolesAPIClientMock } from '../../../roles_api_client.mock'; import { EditSpaceProvider } from '../../provider'; const rolesAPIClient = createRolesAPIClientMock(); @@ -74,6 +71,9 @@ const spacesClientsInvocatorMock = jest.fn((fn) => const dispatchMock = jest.fn(); const onSaveCompleted = jest.fn(); const closeFlyout = jest.fn(); +const licenseMock = { + getFeatures: jest.fn(() => ({})), +} as unknown as SecurityLicense; const renderPrivilegeRolesForm = ({ preSelectedRoles, @@ -93,15 +93,20 @@ const renderPrivilegeRolesForm = ({ spacesManager, serverBasePath: '', getUrlForApp: jest.fn((_) => _), - getRolesAPIClient: getRolesAPIClientMock, - getPrivilegesAPIClient: getPrivilegeAPIClientMock, navigateToUrl: jest.fn(), + license: licenseMock, capabilities: { navLinks: {}, management: {}, catalogue: {}, spaces: { manage: true }, }, + dispatch: dispatchMock, + state: { + roles: new Map(), + fetchRolesError: false, + }, + invokeClient: spacesClientsInvocatorMock, }} > _), }} /> @@ -358,11 +360,11 @@ describe('PrivilegesRolesForm', () => { preSelectedRoles: roles, }); - await waitFor(() => null); - - expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( - 'aria-pressed', - String(true) + await waitFor(() => + expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( + 'aria-pressed', + String(true) + ) ); await user.click(screen.getByTestId('custom-privilege-button')); @@ -408,5 +410,116 @@ describe('PrivilegesRolesForm', () => { String(true) ); }); + + it('prevents customization up to sub privilege level by default', async () => { + const user = userEvent.setup(); + + const roles: Role[] = [ + createRole('test_role_1', [ + { base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] }, + ]), + ]; + + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) => + Boolean(kibanaFeature.subFeatures.length) + ); + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await user.click(screen.getByTestId('custom-privilege-button')); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + const featureUT = featuresWithSubFeatures[0]; + + // change a single feature with sub features to read from default privilege "none" + await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`)); + + // click on the accordion toggle to show sub features + await user.click( + screen.getByTestId( + `featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle` + ) + ); + + // sub feature table renders + expect( + screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`) + ).toBeInTheDocument(); + + // assert switch to customize sub feature can toggled + expect( + within( + screen.getByTestId( + `${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer` + ) + ).getByTestId('customizeSubFeaturePrivileges') + ).toBeDisabled(); + }); + + it('supports customization up to sub privilege level only when security license allows', async () => { + const user = userEvent.setup(); + + const roles: Role[] = [ + createRole('test_role_1', [ + { base: [FEATURE_PRIVILEGES_READ], feature: {}, spaces: [space.id] }, + ]), + ]; + + // enable sub feature privileges + (licenseMock.getFeatures as jest.Mock).mockReturnValue({ + allowSubFeaturePrivileges: true, + }); + + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const featuresWithSubFeatures = kibanaFeatures.filter((kibanaFeature) => + Boolean(kibanaFeature.subFeatures.length) + ); + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await user.click(screen.getByTestId('custom-privilege-button')); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + const featureUT = featuresWithSubFeatures[0]; + + // change a single feature with sub features to read from default privilege "none" + await user.click(screen.getByTestId(`${featureUT.id}_${FEATURE_PRIVILEGES_READ}`)); + + // click on the accordion toggle to show sub features + await user.click( + screen.getByTestId( + `featurePrivilegeControls_${featureUT.category.id}_${featureUT.id}_accordionToggle` + ) + ); + + // sub feature table renders + expect( + screen.getByTestId(`${featureUT.category.id}_${featureUT.id}_subFeaturesTable`) + ).toBeInTheDocument(); + + // assert switch to customize sub feature can toggled + expect( + within( + screen.getByTestId( + `${featureUT.category.id}_${featureUT.id}_customizeSubFeaturesSwitchContainer` + ) + ).getByTestId('customizeSubFeaturePrivileges') + ).not.toBeDisabled(); + }); }); }); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index f33c2cba25268..e0f3e8f3714c6 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -46,7 +46,7 @@ import { FEATURE_PRIVILEGES_CUSTOM, FEATURE_PRIVILEGES_READ, } from '../../../../../common/constants'; -import { type EditSpaceServices, type EditSpaceStore, useEditSpaceServices } from '../../provider'; +import { useEditSpaceServices, useEditSpaceStore } from '../../provider'; type KibanaRolePrivilege = | keyof NonNullable @@ -62,9 +62,6 @@ interface PrivilegesRolesFormProps { * this is useful when the form is opened in edit mode */ defaultSelected?: Role[]; - storeDispatch: EditSpaceStore['dispatch']; - spacesClientsInvocator: EditSpaceServices['invokeClient']; - getUrlForApp: EditSpaceServices['getUrlForApp']; } const createRolesComboBoxOptions = (roles: Role[]): Array> => @@ -74,17 +71,9 @@ const createRolesComboBoxOptions = (roles: Role[]): Array = (props) => { - const { - space, - onSaveCompleted, - closeFlyout, - features, - defaultSelected = [], - spacesClientsInvocator, - storeDispatch, - getUrlForApp, - } = props; - const { logger, notifications } = useEditSpaceServices(); + const { space, onSaveCompleted, closeFlyout, features, defaultSelected = [] } = props; + const { logger, notifications, license, invokeClient, getUrlForApp } = useEditSpaceServices(); + const { dispatch: storeDispatch } = useEditSpaceStore(); const [assigningToRole, setAssigningToRole] = useState(false); const [fetchingDataDeps, setFetchingDataDeps] = useState(false); const [kibanaPrivileges, setKibanaPrivileges] = useState(null); @@ -98,7 +87,7 @@ export const PrivilegesRolesForm: FC = (props) => { async function fetchRequiredData(spaceId: string) { setFetchingDataDeps(true); - const [systemRoles, _kibanaPrivileges] = await spacesClientsInvocator((clients) => + const [systemRoles, _kibanaPrivileges] = await invokeClient((clients) => Promise.all([ clients.rolesClient.getRoles(), clients.privilegesClient.getAll({ includeActions: true, respectLicenseLevel: false }), @@ -123,7 +112,7 @@ export const PrivilegesRolesForm: FC = (props) => { } fetchRequiredData(space.id!).finally(() => setFetchingDataDeps(false)); - }, [space.id, spacesClientsInvocator]); + }, [invokeClient, space.id]); const selectedRolesCombinedPrivileges = useMemo(() => { const combinedPrivilege = new Set( @@ -315,7 +304,7 @@ export const PrivilegesRolesForm: FC = (props) => { return selectedRole.value!; }); - await spacesClientsInvocator((clients) => + await invokeClient((clients) => clients.rolesClient.bulkUpdateRoles({ rolesUpdate: updatedRoles }).then((response) => { setAssigningToRole(false); onSaveCompleted(response); @@ -338,13 +327,14 @@ export const PrivilegesRolesForm: FC = (props) => { }); } }, [ + roleSpacePrivilege, + roleCustomizationAnchor.value?.kibana, + roleCustomizationAnchor.privilegeIndex, selectedRoles, - spacesClientsInvocator, + invokeClient, storeDispatch, - onSaveCompleted, space.id, - roleSpacePrivilege, - roleCustomizationAnchor, + onSaveCompleted, logger, notifications.toasts, ]); @@ -571,7 +561,9 @@ export const PrivilegesRolesForm: FC = (props) => { ) } allSpacesSelected={false} - canCustomizeSubFeaturePrivileges={false} + canCustomizeSubFeaturePrivileges={ + license?.getFeatures().allowSubFeaturePrivileges ?? false + } /> )} diff --git a/x-pack/plugins/spaces/public/management/management_service.test.ts b/x-pack/plugins/spaces/public/management/management_service.test.ts index d1d7fe8d160a9..8a08334b0a76d 100644 --- a/x-pack/plugins/spaces/public/management/management_service.test.ts +++ b/x-pack/plugins/spaces/public/management/management_service.test.ts @@ -13,6 +13,7 @@ import { managementPluginMock } from '@kbn/management-plugin/public/mocks'; import { ManagementService } from './management_service'; import { getRolesAPIClientMock } from './roles_api_client.mock'; +import { getSecurityLicenseMock } from './security_license.mock'; import { EventTracker } from '../analytics'; import type { ConfigType } from '../config'; import type { PluginsStart } from '../plugin'; @@ -49,6 +50,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, isServerless: false, }); @@ -73,6 +75,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: getRolesAPIClientMock, getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, isServerless: false, }); @@ -98,6 +101,7 @@ describe('ManagementService', () => { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: getSecurityLicenseMock, eventTracker, isServerless: false, }); diff --git a/x-pack/plugins/spaces/public/management/management_service.tsx b/x-pack/plugins/spaces/public/management/management_service.tsx index ba66229323bc8..317e091839534 100644 --- a/x-pack/plugins/spaces/public/management/management_service.tsx +++ b/x-pack/plugins/spaces/public/management/management_service.tsx @@ -5,30 +5,15 @@ * 2.0. */ -import type { StartServicesAccessor } from '@kbn/core/public'; -import type { Logger } from '@kbn/logging'; import type { ManagementApp, ManagementSetup } from '@kbn/management-plugin/public'; -import type { - PrivilegesAPIClientPublicContract, - RolesAPIClient, -} from '@kbn/security-plugin-types-public'; -import { spacesManagementApp } from './spaces_management_app'; -import type { EventTracker } from '../analytics'; -import type { ConfigType } from '../config'; -import type { PluginsStart } from '../plugin'; -import type { SpacesManager } from '../spaces_manager'; +import { + spacesManagementApp, + type CreateParams as SpacesManagementAppCreateParams, +} from './spaces_management_app'; -interface SetupDeps { +interface SetupDeps extends SpacesManagementAppCreateParams { management: ManagementSetup; - getStartServices: StartServicesAccessor; - spacesManager: SpacesManager; - config: ConfigType; - getRolesAPIClient: () => Promise; - eventTracker: EventTracker; - getPrivilegesAPIClient: () => Promise; - logger: Logger; - isServerless: boolean; } export class ManagementService { @@ -44,6 +29,7 @@ export class ManagementService { eventTracker, getPrivilegesAPIClient, isServerless, + getSecurityLicense, }: SetupDeps) { this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp( spacesManagementApp.create({ @@ -55,6 +41,7 @@ export class ManagementService { eventTracker, getPrivilegesAPIClient, isServerless, + getSecurityLicense, }) ); } diff --git a/x-pack/plugins/spaces/public/management/security_license.mock.ts b/x-pack/plugins/spaces/public/management/security_license.mock.ts new file mode 100644 index 0000000000000..d5d6e73d03db4 --- /dev/null +++ b/x-pack/plugins/spaces/public/management/security_license.mock.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BehaviorSubject, type Observable } from 'rxjs'; + +import type { SecurityLicense } from '@kbn/security-plugin-types-public'; + +type SecurityLicenseFeatures = SecurityLicense['features$'] extends Observable ? P : never; + +export const createSecurityLicenseMock = ({ + securityFeaturesConfig, +}: { + securityFeaturesConfig: SecurityLicenseFeatures; +}): SecurityLicense => { + return { + isLicenseAvailable: jest.fn(), + isEnabled: jest.fn(), + getFeatures: jest.fn(), + getUnavailableReason: jest.fn(), + hasAtLeast: jest.fn(), + getLicenseType: jest.fn(), + features$: new BehaviorSubject(securityFeaturesConfig), + }; +}; + +export const getSecurityLicenseMock = jest.fn().mockResolvedValue( + createSecurityLicenseMock({ + securityFeaturesConfig: { + showLinks: true, + showLogin: true, + allowLogin: true, + allowRbac: true, + allowFips: true, + showRoleMappingsManagement: true, + allowAccessAgreement: true, + allowAuditLogging: true, + allowSubFeaturePrivileges: true, + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + allowRoleRemoteIndexPrivileges: true, + allowRemoteClusterPrivileges: true, + allowUserProfileCollaboration: true, + }, + }) +); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index ffafe432a5a3b..9c7056214d8fd 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -77,6 +77,7 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: jest.fn(), eventTracker, isServerless: false, }) @@ -102,6 +103,7 @@ describe('spacesManagementApp', () => { logger, getRolesAPIClient: jest.fn(), getPrivilegesAPIClient: jest.fn(), + getSecurityLicense: jest.fn(), eventTracker, isServerless: false, }) diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 7ad85d0ef7c52..a7111b72e563e 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -18,6 +18,7 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import type { PrivilegesAPIClientPublicContract, RolesAPIClient, + SecurityLicense, } from '@kbn/security-plugin-types-public'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; @@ -28,7 +29,7 @@ import type { ConfigType } from '../config'; import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; -interface CreateParams { +export interface CreateParams { getStartServices: StartServicesAccessor; spacesManager: SpacesManager; config: ConfigType; @@ -37,6 +38,7 @@ interface CreateParams { eventTracker: EventTracker; getPrivilegesAPIClient: () => Promise; isServerless: boolean; + getSecurityLicense: () => Promise; } export const spacesManagementApp = Object.freeze({ @@ -50,6 +52,7 @@ export const spacesManagementApp = Object.freeze({ getRolesAPIClient, getPrivilegesAPIClient, isServerless, + getSecurityLicense, }: CreateParams) { const title = i18n.translate('xpack.spaces.displayName', { defaultMessage: 'Spaces', @@ -149,6 +152,7 @@ export const spacesManagementApp = Object.freeze({ capabilities={application.capabilities} getUrlForApp={application.getUrlForApp} navigateToUrl={application.navigateToUrl} + getSecurityLicense={getSecurityLicense} serverBasePath={http.basePath.serverBasePath} getFeatures={features.getFeatures} http={http} diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 8450aaff32657..107d3fab57652 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -10,7 +10,7 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb import type { FeaturesPluginStart } from '@kbn/features-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; -import type { SecurityPluginStart } from '@kbn/security-plugin-types-public'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin-types-public'; import { EventTracker, registerAnalyticsContext, registerSpacesEventTypes } from './analytics'; import type { ConfigType } from './config'; @@ -114,6 +114,18 @@ export class SpacesPlugin implements Plugin { + const { security } = await core.plugins.onSetup<{ security: SecurityPluginSetup }>( + 'security' + ); + + if (!security.found) { + throw new Error('Security plugin is not available as runtime dependency.'); + } + + return security.contract.license; + }; + if (plugins.home) { plugins.home.featureCatalogue.register(createSpacesFeatureCatalogueEntry()); } @@ -130,6 +142,7 @@ export class SpacesPlugin implements Plugin { }); it('correctly defines route.', () => { - expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.options).toEqual({ excludeFromOAS: true }); expect(routeConfig.validate).toBe(false); }); diff --git a/x-pack/plugins/spaces/server/routes/views/index.ts b/x-pack/plugins/spaces/server/routes/views/index.ts index ab06b17374f13..f21a665e35525 100644 --- a/x-pack/plugins/spaces/server/routes/views/index.ts +++ b/x-pack/plugins/spaces/server/routes/views/index.ts @@ -20,7 +20,7 @@ export interface ViewRouteDeps { export function initSpacesViewsRoutes(deps: ViewRouteDeps) { deps.httpResources.register( - { path: '/spaces/space_selector', validate: false }, + { path: '/spaces/space_selector', validate: false, options: { excludeFromOAS: true } }, (context, request, response) => response.renderCoreApp() ); @@ -32,6 +32,7 @@ export function initSpacesViewsRoutes(deps: ViewRouteDeps) { schema.object({ next: schema.maybe(schema.string()) }, { unknowns: 'ignore' }) ), }, + options: { excludeFromOAS: true }, }, async (context, request, response) => { try { diff --git a/x-pack/test/functional/es_archives/security_solution/entity_store/data.json b/x-pack/test/functional/es_archives/security_solution/entity_store/data.json index 28498e7cb0917..a7804bd132d20 100644 --- a/x-pack/test/functional/es_archives/security_solution/entity_store/data.json +++ b/x-pack/test/functional/es_archives/security_solution/entity_store/data.json @@ -2,7 +2,7 @@ "type": "doc", "value": { "id": "a4cf452c1e0375c3d4412cb550ad1783358468a3b3b777da4829d72c7d6fb74f", - "index": ".entities.v1.latest.ea_default_user_entity_store", + "index": ".entities.v1.latest.security_user_default", "source": { "event": { "ingested": "2024-09-11T11:26:49.706875Z" @@ -27,7 +27,7 @@ "id": "LBQAgKHGmpup0Kg9nlKmeQ==", "type": "node", "firstSeenTimestamp": "2024-09-11T10:46:00.000Z", - "definitionId": "ea_default_user_entity_store" + "definitionId": "security_user_default" } } } @@ -37,7 +37,7 @@ "type": "doc", "value": { "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", - "index": ".entities.v1.latest.ea_default_host_entity_store", + "index": ".entities.v1.latest.security_host_default", "source": { "event": { "ingested": "2024-09-11T11:26:49.641707Z" @@ -78,7 +78,7 @@ "id": "ZXKm6GEcUJY6NHkMgPPmGQ==", "type": "node", "firstSeenTimestamp": "2024-09-11T10:46:00.000Z", - "definitionId": "ea_default_host_entity_store" + "definitionId": "security_host_default" } } } diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts index 3e3eb594cc0bc..0fb8644f5e93c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/rules_management/rule_bulk_actions/trial_license_complete_tier/perform_bulk_action_ess.ts @@ -47,7 +47,9 @@ export default ({ getService }: FtrProviderContext): void => { const createWebHookConnector = () => createConnector(getWebHookAction()); // Failing: See https://github.com/elastic/kibana/issues/173804 - describe('@ess perform_bulk_action - ESS specific logic', () => { + // Failing: See https://github.com/elastic/kibana/issues/196470 + // Failing: See https://github.com/elastic/kibana/issues/196462 + describe.skip('@ess perform_bulk_action - ESS specific logic', () => { beforeEach(async () => { await deleteAllRules(supertest, log); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts index 6c41f4f916141..a7d32767f50ce 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine.ts @@ -7,26 +7,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { EntityStoreUtils, elasticAssetCheckerFactory } from '../../utils'; +import { EntityStoreUtils } from '../../utils'; import { dataViewRouteHelpersFactory } from '../../utils/data_view'; export default ({ getService }: FtrProviderContext) => { const api = getService('securitySolutionApi'); const supertest = getService('supertest'); - const { - expectTransformExists, - expectTransformNotFound, - expectEnrichPolicyExists, - expectEnrichPolicyNotFound, - expectComponentTemplateExists, - expectComponentTemplateNotFound, - expectIngestPipelineExists, - expectIngestPipelineNotFound, - } = elasticAssetCheckerFactory(getService); const utils = EntityStoreUtils(getService); - - // TODO: unskip once permissions issue is resolved - describe.skip('@ess Entity Store Engine APIs', () => { + // Failing: See https://github.com/elastic/kibana/issues/196526 + describe.skip('@ess @skipInServerlessMKI Entity Store Engine APIs', () => { const dataView = dataViewRouteHelpersFactory(supertest); before(async () => { @@ -45,20 +34,12 @@ export default ({ getService }: FtrProviderContext) => { it('should have installed the expected user resources', async () => { await utils.initEntityEngineForEntityType('user'); - - await expectTransformExists('entities-v1-latest-ea_default_user_entity_store'); - await expectEnrichPolicyExists('entity_store_field_retention_user_default_v1'); - await expectComponentTemplateExists(`ea_default_user_entity_store-latest@platform`); - await expectIngestPipelineExists(`ea_default_user_entity_store-latest@platform`); + await utils.expectEngineAssetsExist('user'); }); it('should have installed the expected host resources', async () => { await utils.initEntityEngineForEntityType('host'); - - await expectTransformExists('entities-v1-latest-ea_default_host_entity_store'); - await expectEnrichPolicyExists('entity_store_field_retention_host_default_v1'); - await expectComponentTemplateExists(`ea_default_host_entity_store-latest@platform`); - await expectIngestPipelineExists(`ea_default_host_entity_store-latest@platform`); + await utils.expectEngineAssetsExist('host'); }); }); @@ -188,10 +169,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-latest-ea_default_host_entity_store'); - await expectEnrichPolicyNotFound('entity_store_field_retention_host_default_v1'); - await expectComponentTemplateNotFound(`ea_default_host_entity_store-latest@platform`); - await expectIngestPipelineNotFound(`ea_default_host_entity_store-latest@platform`); + await utils.expectEngineAssetsDoNotExist('host'); }); it('should delete the user entity engine', async () => { @@ -204,10 +182,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - await expectTransformNotFound('entities-v1-latest-ea_default_user_entity_store'); - await expectEnrichPolicyNotFound('entity_store_field_retention_user_default_v1'); - await expectComponentTemplateNotFound(`ea_default_user_entity_store-latest@platform`); - await expectIngestPipelineNotFound(`ea_default_user_entity_store-latest@platform`); + await utils.expectEngineAssetsDoNotExist('user'); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts index ee86231fe23d4..481f7aa4056f6 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/engine_nondefault_spaces.ts @@ -9,15 +9,19 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; import { EntityStoreUtils } from '../../utils'; +import { dataViewRouteHelpersFactory } from '../../utils/data_view'; + export default ({ getService }: FtrProviderContextWithSpaces) => { const api = getService('securitySolutionApi'); const spaces = getService('spaces'); const namespace = uuidv4().substring(0, 8); - + const supertest = getService('supertest'); const utils = EntityStoreUtils(getService, namespace); - // TODO: unskip once kibana system user has entity index privileges + // Failing: See https://github.com/elastic/kibana/issues/196546 describe.skip('@ess Entity Store Engine APIs in non-default space', () => { + const dataView = dataViewRouteHelpersFactory(supertest, namespace); + before(async () => { await utils.cleanEngines(); await spaces.create({ @@ -25,9 +29,11 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { name: namespace, disabledFeatures: [], }); + await dataView.create('security-solution'); }); after(async () => { + await dataView.delete('security-solution'); await spaces.delete(namespace); }); @@ -38,18 +44,12 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { it('should have installed the expected user resources', async () => { await utils.initEntityEngineForEntityType('user'); - - const expectedTransforms = [`entities-v1-latest-ea_${namespace}_user_entity_store`]; - - await utils.expectTransformsExist(expectedTransforms); + await utils.expectEngineAssetsExist('user'); }); it('should have installed the expected host resources', async () => { await utils.initEntityEngineForEntityType('host'); - - const expectedTransforms = [`entities-v1-latest-ea_${namespace}_host_entity_store`]; - - await utils.expectTransformsExist(expectedTransforms); + await utils.expectEngineAssetsExist('host'); }); }); @@ -79,9 +79,9 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { expect(getResponse.body).to.eql({ status: 'started', type: 'host', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }); }); @@ -98,9 +98,9 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { expect(getResponse.body).to.eql({ status: 'started', type: 'user', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }); }); }); @@ -116,16 +116,16 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { { status: 'started', type: 'host', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }, { status: 'started', type: 'user', - indexPattern: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*', filter: '', + fieldHistoryLength: 10, + indexPattern: '', }, ]); }); @@ -200,10 +200,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { ) .expect(200); - await utils.expectTransformNotFound( - `entities-v1-history-ea_${namespace}_host_entity_store` - ); - await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_host_entity_store`); + await utils.expectEngineAssetsDoNotExist('host'); }); it('should delete the user entity engine', async () => { @@ -219,10 +216,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { ) .expect(200); - await utils.expectTransformNotFound( - `entities-v1-history-ea_${namespace}_user_entity_store` - ); - await utils.expectTransformNotFound(`entities-v1-latest-ea_${namespace}_user_entity_store`); + await utils.expectEngineAssetsDoNotExist('user'); }); }); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts index 69f9c14d06086..9d7af16c79441 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts @@ -11,8 +11,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { const securitySolutionApi = getService('securitySolutionApi'); - // TODO: unskip once permissions issue is resolved - describe.skip('@ess Entity store - Entities list API', () => { + describe('@ess @skipInServerlessMKI Entity store - Entities list API', () => { describe('when the entity store is disable', () => { it("should return response with success status when the index doesn't exist", async () => { const { body } = await securitySolutionApi.listEntities({ diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts index 4eba56d3a757b..e94f7b7119ddf 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/data_view.ts @@ -12,7 +12,7 @@ export const dataViewRouteHelpersFactory = ( ) => ({ create: (name: string) => { return supertest - .post(`/api/data_views/data_view`) + .post(`/s/${namespace}/api/data_views/data_view`) .set('kbn-xsrf', 'foo') .send({ data_view: { @@ -26,13 +26,13 @@ export const dataViewRouteHelpersFactory = ( }, delete: (name: string) => { return supertest - .delete(`/api/data_views/data_view/${name}-${namespace}`) + .delete(`/s/${namespace}/api/data_views/data_view/${name}-${namespace}`) .set('kbn-xsrf', 'foo') .expect(200); }, updateIndexPattern: (name: string, indexPattern: string) => { return supertest - .post(`/api/data_views/data_view/${name}-${namespace}`) + .post(`/s/${namespace}/api/data_views/data_view/${name}-${namespace}`) .set('kbn-xsrf', 'foo') .send({ data_view: { diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts index 3ac171de1d4fd..24c1434b5e4a5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/utils/entity_store.ts @@ -6,16 +6,27 @@ */ import { EntityType } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/common.gen'; - +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; +import { elasticAssetCheckerFactory } from './elastic_asset_checker'; export const EntityStoreUtils = ( getService: FtrProviderContext['getService'], - namespace?: string + namespace: string = 'default' ) => { const api = getService('securitySolutionApi'); const es = getService('es'); const log = getService('log'); + const { + expectTransformExists, + expectTransformNotFound, + expectEnrichPolicyExists, + expectEnrichPolicyNotFound, + expectComponentTemplateExists, + expectComponentTemplateNotFound, + expectIngestPipelineExists, + expectIngestPipelineNotFound, + } = elasticAssetCheckerFactory(getService); log.debug(`EntityStoreUtils namespace: ${namespace}`); @@ -37,17 +48,24 @@ export const EntityStoreUtils = ( } }; - const initEntityEngineForEntityType = (entityType: EntityType) => { - log.info(`Initializing engine for entity type ${entityType} in namespace ${namespace}`); - return api - .initEntityEngine( - { - params: { entityType }, - body: {}, - }, - namespace - ) - .expect(200); + const initEntityEngineForEntityType = async (entityType: EntityType) => { + log.info( + `Initializing engine for entity type ${entityType} in namespace ${namespace || 'default'}` + ); + const res = await api.initEntityEngine( + { + params: { entityType }, + body: {}, + }, + namespace + ); + + if (res.status !== 200) { + log.error(`Failed to initialize engine for entity type ${entityType}`); + log.error(JSON.stringify(res.body)); + } + + expect(res.status).to.eql(200); }; const expectTransformStatus = async ( @@ -78,22 +96,25 @@ export const EntityStoreUtils = ( } }; - const expectTransformNotFound = async (transformId: string, attempts: number = 5) => { - return expectTransformStatus(transformId, false); - }; - const expectTransformExists = async (transformId: string) => { - return expectTransformStatus(transformId, true); + const expectEngineAssetsExist = async (entityType: EntityType) => { + await expectTransformExists(`entities-v1-latest-security_${entityType}_${namespace}`); + await expectEnrichPolicyExists(`entity_store_field_retention_${entityType}_${namespace}_v1`); + await expectComponentTemplateExists(`security_${entityType}_${namespace}-latest@platform`); + await expectIngestPipelineExists(`security_${entityType}_${namespace}-latest@platform`); }; - const expectTransformsExist = async (transformIds: string[]) => - Promise.all(transformIds.map((id) => expectTransformExists(id))); + const expectEngineAssetsDoNotExist = async (entityType: EntityType) => { + await expectTransformNotFound(`entities-v1-latest-security_${entityType}_${namespace}`); + await expectEnrichPolicyNotFound(`entity_store_field_retention_${entityType}_${namespace}_v1`); + await expectComponentTemplateNotFound(`security_${entityType}_${namespace}-latest@platform`); + await expectIngestPipelineNotFound(`security_${entityType}_${namespace}-latest@platform`); + }; return { cleanEngines, initEntityEngineForEntityType, expectTransformStatus, - expectTransformNotFound, - expectTransformExists, - expectTransformsExist, + expectEngineAssetsExist, + expectEngineAssetsDoNotExist, }; }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts index 8a636358c2649..027c0a20262a8 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/trial_license_complete_tier/notes.ts @@ -15,7 +15,8 @@ export default function ({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const supertest = getService('supertest'); - describe('Note - Saved Objects', () => { + // Failing: See https://github.com/elastic/kibana/issues/196492 + describe.skip('Note - Saved Objects', () => { const es = getService('es'); before(() => kibanaServer.savedObjects.cleanStandardList()); @@ -407,11 +408,7 @@ export default function ({ getService }: FtrProviderContext) { expect(notes[2].eventId).to.be('1'); }); - // TODO should add more tests for the filter query parameter (I don't know how it's supposed to work) - // TODO should add more tests for the MAX_UNASSOCIATED_NOTES advanced settings values - // TODO figure out why this test is failing on CI but not locally - // we can't really test for other users because the persistNote endpoint forces overrideOwner to be true then all the notes created here are owned by the elastic user it.skip('should retrieve all notes that have been created by a specific user', async () => { await Promise.all([ createNote(supertest, { text: 'first note' }), @@ -443,6 +440,116 @@ export default function ({ getService }: FtrProviderContext) { expect(totalCount).to.be(0); }); + + it('should retrieve all notes that have an association with a document only', async () => { + await Promise.all([ + createNote(supertest, { documentId: eventId1, text: 'associated with event-1 only' }), + createNote(supertest, { + savedObjectId: timelineId1, + text: 'associated with timeline-1 only', + }), + createNote(supertest, { + documentId: eventId1, + savedObjectId: timelineId1, + text: 'associated with event-1 and timeline-1', + }), + createNote(supertest, { text: 'associated with nothing' }), + ]); + + const response = await supertest + .get('/api/note?associatedFilter=document_only') + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + + const { totalCount, notes } = response.body; + + expect(totalCount).to.be(1); + expect(notes[0].eventId).to.be(eventId1); + }); + + it('should retrieve all notes that have an association with a saved object only', async () => { + await Promise.all([ + createNote(supertest, { documentId: eventId1, text: 'associated with event-1 only' }), + createNote(supertest, { + savedObjectId: timelineId1, + text: 'associated with timeline-1 only', + }), + createNote(supertest, { + documentId: eventId1, + savedObjectId: timelineId1, + text: 'associated with event-1 and timeline-1', + }), + createNote(supertest, { text: 'associated with nothing' }), + ]); + + const response = await supertest + .get('/api/note?associatedFilter=saved_object_only') + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + + const { totalCount, notes } = response.body; + + expect(totalCount).to.be(1); + expect(notes[0].timelineId).to.be(timelineId1); + }); + + it('should retrieve all notes that have an association with a document AND a saved object', async () => { + await Promise.all([ + createNote(supertest, { documentId: eventId1, text: 'associated with event-1 only' }), + createNote(supertest, { + savedObjectId: timelineId1, + text: 'associated with timeline-1 only', + }), + createNote(supertest, { + documentId: eventId1, + savedObjectId: timelineId1, + text: 'associated with event-1 and timeline-1', + }), + createNote(supertest, { text: 'associated with nothing' }), + ]); + + const response = await supertest + .get('/api/note?associatedFilter=document_and_saved_object') + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + + const { totalCount, notes } = response.body; + + expect(totalCount).to.be(1); + expect(notes[0].eventId).to.be(eventId1); + expect(notes[0].timelineId).to.be(timelineId1); + }); + + it('should retrieve all notes that have an association with no document AND no saved object', async () => { + await Promise.all([ + createNote(supertest, { documentId: eventId1, text: 'associated with event-1 only' }), + createNote(supertest, { + savedObjectId: timelineId1, + text: 'associated with timeline-1 only', + }), + createNote(supertest, { + documentId: eventId1, + savedObjectId: timelineId1, + text: 'associated with event-1 and timeline-1', + }), + createNote(supertest, { text: 'associated with nothing' }), + ]); + + const response = await supertest + .get('/api/note?associatedFilter=orphan') + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + + const { totalCount, notes } = response.body; + + expect(totalCount).to.be(1); + expect(notes[0].eventId).to.be(''); + expect(notes[0].timelineId).to.be(''); + }); + + // TODO should add more tests for the filter query parameter (I don't know how it's supposed to work) + // TODO should add more tests for the MAX_UNASSOCIATED_NOTES advanced settings values + // TODO add more tests to check the combination of filters (user, association and filter) }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts index c76ccb81f8ce2..c102f502f9489 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/sessions.ts @@ -10,7 +10,6 @@ import { SupertestWithRoleScopeType } from '@kbn/test-suites-xpack/api_integrati import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - const svlCommonApi = getService('svlCommonApi'); const samlAuth = getService('samlAuth'); const roleScopedSupertest = getService('roleScopedSupertest'); let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; @@ -26,16 +25,6 @@ export default function ({ getService }: FtrProviderContext) { }); describe('route access', () => { - describe('disabled', () => { - it('invalidate', async () => { - const { body, status } = await supertestViewerWithCookieCredentials - .post('/api/security/session/_invalidate') - .set(samlAuth.getInternalRequestHeader()) - .send({ match: 'all' }); - svlCommonApi.assertApiNotFound(body, status); - }); - }); - describe('internal', () => { it('get session info', async () => { let body: any; @@ -84,6 +73,45 @@ export default function ({ getService }: FtrProviderContext) { // expect redirect expect(status).toBe(302); }); + + it('invalidate', async () => { + const supertestAdmin = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + useCookieHeader: true, + }); + + let body: any; + let status: number; + + ({ body, status } = await supertestViewerWithCookieCredentials + .post('/api/security/session/_invalidate') + .set(samlAuth.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [post] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertestViewerWithCookieCredentials + .post('/api/security/session/_invalidate') + .set(samlAuth.getInternalRequestHeader())); + // expect forbidden because the viewer does not have privilege to invalidate a session + expect(status).toBe(403); + + ({ body, status } = await supertestAdmin + .post('/api/security/session/_invalidate') + .set(samlAuth.getInternalRequestHeader())); + // expect 400 due to no body, admin has privilege, but the request body is missing + expect(status).toBe(400); + expect(body).toEqual({ + error: 'Bad Request', + message: '[request body]: expected a plain object value, but found [null] instead.', + statusCode: 400, + }); + }); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts index 1bedd0acd0cc4..89edce106f64e 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover esql view', function () { + // Failing: See https://github.com/elastic/kibana/issues/194305 + describe.skip('discover esql view', function () { // see details: https://github.com/elastic/kibana/issues/188816 this.tags(['failsOnMKI']); diff --git a/yarn.lock b/yarn.lock index ec4c8f0e0837f..ed8af28c675f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1844,9 +1844,9 @@ "@elastic/search-ui" "1.20.2" "@elastic/request-converter@^8.15.4": - version "8.15.4" - resolved "https://registry.yarnpkg.com/@elastic/request-converter/-/request-converter-8.15.4.tgz#332dc6266841b5a578c92e1655eee46b82c47ed2" - integrity sha512-iZDQpZpygV+AVOweaDzTsMJBfa2hwwduPXNNzk/yTXgC9qtjmns/AjehtLStKXs274+u3fg+BFxVt6NcMwUAAg== + version "8.16.0" + resolved "https://registry.yarnpkg.com/@elastic/request-converter/-/request-converter-8.16.0.tgz#e607d06d898ec290c7a9412104d7fa67d5fb9c8c" + integrity sha512-tSwCJMoX3/hme1HXi7ewfP5E+BLFV2LcItt3EB4eucWPKEtG3SqJ3iuK3ygWm1PodPz8Mww/DMaxTJFERb/usg== dependencies: child-process-promise "^2.2.1" commander "^12.1.0" @@ -21081,7 +21081,7 @@ isbinaryfile@4.0.2: isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isexe@^3.1.1: version "3.1.1" @@ -26411,7 +26411,7 @@ prr@~1.0.1: pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== psl@^1.1.33: version "1.4.0" @@ -29690,7 +29690,7 @@ string-replace-loader@^2.2.0: loader-utils "^1.2.3" schema-utils "^1.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -29708,6 +29708,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -29818,7 +29827,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -29832,6 +29841,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -31140,9 +31156,9 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== uglify-js@^3.1.4: - version "3.17.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" - integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + version "3.19.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== unbox-primitive@^1.0.2: version "1.0.2" @@ -32733,7 +32749,7 @@ word-wrap@~1.2.3: wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== worker-farm@^1.7.0: version "1.7.0" @@ -32754,7 +32770,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -32780,6 +32796,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -32892,7 +32917,7 @@ xpath@^0.0.33: resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07" integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA== -"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1: +"xstate5@npm:xstate@^5.18.1": version "5.18.1" resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4" integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g== @@ -32902,6 +32927,11 @@ xstate@^4.38.2: resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804" integrity sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg== +xstate@^5.18.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4" + integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g== + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -32937,7 +32967,7 @@ y18n@^5.0.5: yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== yallist@^3.0.2: version "3.1.1"