diff --git a/x-pack/platform/plugins/shared/cases/common/constants/owners.ts b/x-pack/platform/plugins/shared/cases/common/constants/owners.ts index 4ed1139667a39..384c0b1832a74 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/owners.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/owners.ts @@ -7,7 +7,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { APP_ID } from './application'; -import type { Owner } from './types'; +import type { ServerlessProjectType, Owner } from './types'; /** * Owner @@ -16,7 +16,14 @@ export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const; export const OBSERVABILITY_OWNER = 'observability' as const; export const GENERAL_CASES_OWNER = APP_ID; +export const SECURITY_PROJECT_TYPE_ID = 'security'; +export const OBSERVABILITY_PROJECT_TYPE_ID = 'observability'; + export const OWNERS = [GENERAL_CASES_OWNER, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER] as const; +export const SERVERLESS_PROJECT_TYPES = [ + SECURITY_PROJECT_TYPE_ID, + OBSERVABILITY_PROJECT_TYPE_ID, +] as const; interface RouteInfo { id: Owner; @@ -25,6 +32,7 @@ interface RouteInfo { iconType: string; appRoute: string; validRuleConsumers?: readonly AlertConsumers[]; + serverlessProjectType?: ServerlessProjectType; } export const OWNER_INFO: Record = { @@ -35,6 +43,7 @@ export const OWNER_INFO: Record = { iconType: 'logoSecurity', appRoute: '/app/security', validRuleConsumers: [AlertConsumers.SIEM], + serverlessProjectType: SECURITY_PROJECT_TYPE_ID, }, [OBSERVABILITY_OWNER]: { id: OBSERVABILITY_OWNER, @@ -53,6 +62,7 @@ export const OWNER_INFO: Record = { AlertConsumers.MONITORING, AlertConsumers.STREAMS, ], + serverlessProjectType: OBSERVABILITY_PROJECT_TYPE_ID, }, [GENERAL_CASES_OWNER]: { id: GENERAL_CASES_OWNER, diff --git a/x-pack/platform/plugins/shared/cases/common/constants/types.ts b/x-pack/platform/plugins/shared/cases/common/constants/types.ts index 0c2767adfa63a..ec48033d57796 100644 --- a/x-pack/platform/plugins/shared/cases/common/constants/types.ts +++ b/x-pack/platform/plugins/shared/cases/common/constants/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { OWNERS } from './owners'; +import type { SERVERLESS_PROJECT_TYPES, OWNERS } from './owners'; export enum HttpApiPrivilegeOperation { Read = 'Read', @@ -14,3 +14,4 @@ export enum HttpApiPrivilegeOperation { } export type Owner = (typeof OWNERS)[number]; +export type ServerlessProjectType = (typeof SERVERLESS_PROJECT_TYPES)[number]; diff --git a/x-pack/platform/plugins/shared/cases/common/utils/owner.test.ts b/x-pack/platform/plugins/shared/cases/common/utils/owner.test.ts index 4e005750ad36f..7a44726dc61fc 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/owner.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/utils/owner.test.ts @@ -8,6 +8,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { OWNER_INFO } from '../constants'; import { getCaseOwnerByAppId, getOwnerFromRuleConsumerProducer, isValidOwner } from './owner'; +import type { ServerlessProjectType } from '../constants/types'; describe('owner utils', () => { describe('isValidOwner', () => { @@ -71,16 +72,6 @@ describe('owner utils', () => { expect(owner).toBe(OWNER_INFO.securitySolution.id); }); - it('returns securitySolution owner if project isServerlessSecurity', () => { - const owner = getOwnerFromRuleConsumerProducer({ - consumer: AlertConsumers.OBSERVABILITY, - producer: AlertConsumers.OBSERVABILITY, - isServerlessSecurity: true, - }); - - expect(owner).toBe(OWNER_INFO.securitySolution.id); - }); - it('fallbacks to producer when the consumer is alerts', () => { const owner = getOwnerFromRuleConsumerProducer({ consumer: AlertConsumers.ALERTS, @@ -89,5 +80,27 @@ describe('owner utils', () => { expect(owner).toBe(OWNER_INFO.observability.id); }); + + describe('serverless projects', () => { + const cloudProjects: Array<[ServerlessProjectType, string]> = [ + [OWNER_INFO.observability.serverlessProjectType!, OWNER_INFO.observability.id], + [OWNER_INFO.securitySolution.serverlessProjectType!, OWNER_INFO.securitySolution.id], + // @ts-expect-error - we need to test the unknown project type + ['unknown-by-us', OWNER_INFO.cases.id], + ]; + + it.each(cloudProjects)( + 'when the project type is %j, the owner should be %j', + (cloudProjectType, expectedOwner) => { + const owner = getOwnerFromRuleConsumerProducer({ + consumer: 'should be ignored', + producer: 'should be ignored', + serverlessProjectType: cloudProjectType, + }); + + expect(owner).toBe(expectedOwner); + } + ); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/utils/owner.ts b/x-pack/platform/plugins/shared/cases/common/utils/owner.ts index d159a3d55ee7a..5e97bcffc470f 100644 --- a/x-pack/platform/plugins/shared/cases/common/utils/owner.ts +++ b/x-pack/platform/plugins/shared/cases/common/utils/owner.ts @@ -7,7 +7,7 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { OWNER_INFO } from '../constants'; -import type { Owner } from '../constants/types'; +import type { ServerlessProjectType, Owner } from '../constants/types'; export const isValidOwner = (owner: string): owner is keyof typeof OWNER_INFO => Object.keys(OWNER_INFO).includes(owner); @@ -18,16 +18,21 @@ export const getCaseOwnerByAppId = (currentAppId?: string) => export const getOwnerFromRuleConsumerProducer = ({ consumer, producer, - isServerlessSecurity, + serverlessProjectType, }: { consumer?: string; producer?: string; - isServerlessSecurity?: boolean; + serverlessProjectType?: ServerlessProjectType; }): Owner => { // This is a workaround for a very specific bug with the cases action in serverless security + // This same bug was later encountered in o11y as well // More info here: https://github.com/elastic/kibana/issues/186270 - if (isServerlessSecurity) { - return OWNER_INFO.securitySolution.id; + if (serverlessProjectType) { + const foundOwner = Object.entries(OWNER_INFO).find(([, info]) => { + return info.serverlessProjectType === serverlessProjectType; + }); + + return foundOwner ? foundOwner[1].id : OWNER_INFO.cases.id; } // Fallback to producer if the consumer is alerts diff --git a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx index fce9d872823c8..9bdbc6fb8743a 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.test.tsx @@ -318,6 +318,54 @@ describe('CasesParamsFields renders', () => { getConfigurationByOwnerSpy.mockRestore(); }); + it('renders observability templates if the project is serverless observability', async () => { + useKibanaMock.mockReturnValue({ + services: { + ...createStartServicesMock(), + // simulate a observability security project + cloud: { isServerlessEnabled: true, serverless: { projectType: 'observability' } }, + data: { dataViews: {} }, + }, + } as unknown as ReturnType); + + const configuration = { + ...useGetAllCaseConfigurationsResponse.data[0], + templates: templatesConfigurationMock, + }; + useGetAllCaseConfigurationsMock.mockImplementation(() => ({ + ...useGetAllCaseConfigurationsResponse, + data: [configuration], + })); + const getConfigurationByOwnerSpy = jest + .spyOn(utils, 'getConfigurationByOwner') + .mockImplementation(() => configuration); + + const securityOwnedRule = { + ...defaultProps, + // these two would normally produce a security owner + producerId: 'securitySolution', + featureId: 'securitySolution', + actionParams: { + subAction: 'run', + subActionParams: { + ...actionParams.subActionParams, + templateId: templatesConfigurationMock[1].key, + }, + }, + }; + + render(); + + expect(getConfigurationByOwnerSpy).toHaveBeenCalledWith( + expect.objectContaining({ + // the observability owner was forced + owner: 'observability', + }) + ); + + getConfigurationByOwnerSpy.mockRestore(); + }); + it('updates template correctly', async () => { useGetAllCaseConfigurationsMock.mockReturnValueOnce({ ...useGetAllCaseConfigurationsResponse, diff --git a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx index 04b3fb3690bcf..ae3a762f1916b 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/system_actions/cases/cases_params.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; +import type { ServerlessProjectType } from '../../../../common/constants/types'; import * as i18n from './translations'; import type { CasesActionParams } from './types'; import { CASES_CONNECTOR_SUB_ACTION } from '../../../../common/constants'; @@ -48,13 +49,16 @@ export const CasesParamsFieldsComponent: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; + const serverlessProjectType = cloud?.isServerlessEnabled + ? (cloud.serverless.projectType as ServerlessProjectType) + : undefined; + const owner = getOwnerFromRuleConsumerProducer({ consumer: featureId, producer: producerId, // This is a workaround for a very specific bug with the cases action in serverless security // More info here: https://github.com/elastic/kibana/issues/195599 - isServerlessSecurity: - cloud?.isServerlessEnabled && cloud?.serverless.projectType === 'security', + serverlessProjectType, }); const { dataView, isLoading: loadingAlertDataViews } = useAlertsDataView({ diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts index 1a62c54d20c62..d1ce0e24383d1 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.test.ts @@ -9,6 +9,7 @@ import type { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_acti import type { CasesConnectorConfig, CasesConnectorSecrets } from './types'; import { getCasesConnectorAdapter, getCasesConnectorType } from '.'; import { AlertConsumers } from '@kbn/rule-data-utils'; +import { OBSERVABILITY_PROJECT_TYPE_ID, SECURITY_PROJECT_TYPE_ID } from '../../../common/constants'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { Logger } from '@kbn/core/server'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; @@ -340,7 +341,7 @@ describe('getCasesConnectorType', () => { it('correctly fallsback to security owner if the project is serverless security', () => { const adapter = getCasesConnectorAdapter({ - isServerlessSecurity: true, + serverlessProjectType: SECURITY_PROJECT_TYPE_ID, logger: mockLogger, }); @@ -623,7 +624,7 @@ describe('getCasesConnectorType', () => { it('correctly overrides the consumer and producer if the project is serverless security', () => { const adapter = getCasesConnectorAdapter({ - isServerlessSecurity: true, + serverlessProjectType: SECURITY_PROJECT_TYPE_ID, logger: mockLogger, }); @@ -645,6 +646,31 @@ describe('getCasesConnectorType', () => { 'cases:securitySolution/assignCase', ]); }); + + it('correctly overrides the consumer and producer if the project is serverless observability', () => { + const adapter = getCasesConnectorAdapter({ + serverlessProjectType: OBSERVABILITY_PROJECT_TYPE_ID, + logger: mockLogger, + }); + + expect( + adapter.getKibanaPrivileges?.({ + consumer: 'alerts', + producer: AlertConsumers.SIEM, + }) + ).toEqual([ + 'cases:observability/createCase', + 'cases:observability/updateCase', + 'cases:observability/deleteCase', + 'cases:observability/pushCase', + 'cases:observability/createComment', + 'cases:observability/updateComment', + 'cases:observability/deleteComment', + 'cases:observability/findConfigurations', + 'cases:observability/reopenCase', + 'cases:observability/assignCase', + ]); + }); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts index 31f35cd1bcfb2..0b1e8122d5087 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/cases/index.ts @@ -15,13 +15,10 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; import type { ConnectorAdapter } from '@kbn/alerting-plugin/server'; import { ATTACK_DISCOVERY_SCHEDULES_ALERT_TYPE_ID } from '@kbn/elastic-assistant-common'; +import type { ServerlessProjectType } from '../../../common/constants/types'; import { CasesConnector } from './cases_connector'; import { DEFAULT_MAX_OPEN_CASES } from './constants'; -import { - CASES_CONNECTOR_ID, - CASES_CONNECTOR_TITLE, - SECURITY_SOLUTION_OWNER, -} from '../../../common/constants'; +import { CASES_CONNECTOR_ID, CASES_CONNECTOR_TITLE, OWNER_INFO } from '../../../common/constants'; import { getOwnerFromRuleConsumerProducer } from '../../../common/utils/owner'; import type { @@ -47,14 +44,14 @@ interface GetCasesConnectorTypeArgs { savedObjectTypes: string[] ) => Promise; getSpaceId: (request?: KibanaRequest) => string; - isServerlessSecurity?: boolean; + serverlessProjectType?: string; } export const getCasesConnectorType = ({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient, - isServerlessSecurity, + serverlessProjectType, }: GetCasesConnectorTypeArgs): SubActionConnectorType< CasesConnectorConfig, CasesConnectorSecrets @@ -82,18 +79,26 @@ export const getCasesConnectorType = ({ throw new Error('Cannot authorize cases. Owner is not defined in the subActionParams.'); } - const owner = isServerlessSecurity - ? SECURITY_SOLUTION_OWNER - : (params?.subActionParams?.owner as string); + let owner: string; + if (serverlessProjectType) { + const foundOwner = Object.entries(OWNER_INFO).find(([, info]) => { + return info.serverlessProjectType === serverlessProjectType; + }); + + owner = foundOwner ? foundOwner[1].id : OWNER_INFO.cases.id; + } else { + owner = params?.subActionParams?.owner as string; + } return constructRequiredKibanaPrivileges(owner); }, }); export const getCasesConnectorAdapter = ({ - isServerlessSecurity, + serverlessProjectType, logger, }: { + serverlessProjectType?: ServerlessProjectType; isServerlessSecurity?: boolean; logger: Logger; }): ConnectorAdapter => { @@ -125,7 +130,7 @@ export const getCasesConnectorAdapter = ({ const owner = getOwnerFromRuleConsumerProducer({ consumer: rule.consumer, producer: rule.producer, - isServerlessSecurity, + serverlessProjectType, }); const subActionParams = { @@ -144,7 +149,7 @@ export const getCasesConnectorAdapter = ({ return { subAction: 'run', subActionParams }; }, getKibanaPrivileges: ({ consumer, producer }) => { - const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, isServerlessSecurity }); + const owner = getOwnerFromRuleConsumerProducer({ consumer, producer, serverlessProjectType }); return constructRequiredKibanaPrivileges(owner); }, }; diff --git a/x-pack/platform/plugins/shared/cases/server/connectors/index.ts b/x-pack/platform/plugins/shared/cases/server/connectors/index.ts index 6b6820329d30a..aeca83581f2b5 100644 --- a/x-pack/platform/plugins/shared/cases/server/connectors/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/connectors/index.ts @@ -10,6 +10,7 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { CoreSetup, Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { SECURITY_EXTENSION_ID } from '@kbn/core/server'; import type { AlertingServerSetup } from '@kbn/alerting-plugin/server'; +import type { ServerlessProjectType } from '../../common/constants/types'; import type { CasesClient } from '../client'; import { getCasesConnectorAdapter, getCasesConnectorType } from './cases'; @@ -23,7 +24,7 @@ export function registerConnectorTypes({ logger, getCasesClient, getSpaceId, - isServerlessSecurity, + serverlessProjectType, }: { actions: ActionsPluginSetupContract; alerting: AlertingServerSetup; @@ -31,7 +32,7 @@ export function registerConnectorTypes({ logger: Logger; getCasesClient: (request: KibanaRequest) => Promise; getSpaceId: (request?: KibanaRequest) => string; - isServerlessSecurity?: boolean; + serverlessProjectType?: ServerlessProjectType; }) { const getUnsecuredSavedObjectsClient = async ( request: KibanaRequest, @@ -61,9 +62,9 @@ export function registerConnectorTypes({ getCasesClient, getSpaceId, getUnsecuredSavedObjectsClient, - isServerlessSecurity, + serverlessProjectType, }) ); - alerting.registerConnectorAdapter(getCasesConnectorAdapter({ isServerlessSecurity, logger })); + alerting.registerConnectorAdapter(getCasesConnectorAdapter({ serverlessProjectType, logger })); } diff --git a/x-pack/platform/plugins/shared/cases/server/plugin.ts b/x-pack/platform/plugins/shared/cases/server/plugin.ts index e69f55cca2099..524af9599650c 100644 --- a/x-pack/platform/plugins/shared/cases/server/plugin.ts +++ b/x-pack/platform/plugins/shared/cases/server/plugin.ts @@ -47,6 +47,7 @@ import { registerCaseFileKinds } from './files'; import type { ConfigType } from './config'; import { registerConnectorTypes } from './connectors'; import { registerSavedObjects } from './saved_object_types'; +import type { ServerlessProjectType } from '../common/constants/types'; import { IncrementalIdTaskManager } from './tasks/incremental_id/incremental_id_task_manager'; import { createCasesAnalyticsIndexes, @@ -162,12 +163,10 @@ export class CasePlugin const router = core.http.createRouter(); const telemetryUsageCounter = plugins.usageCollection?.createUsageCounter(APP_ID); - const isServerless = plugins.cloud?.isServerlessEnabled; - registerRoutes({ router, routes: [ - ...getExternalRoutes({ isServerless, docLinks: core.docLinks }), + ...getExternalRoutes({ isServerless: this.isServerless, docLinks: core.docLinks }), ...getInternalRoutes(this.userProfileService), ], logger: this.logger, @@ -191,8 +190,9 @@ export class CasePlugin return plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; }; - const isServerlessSecurity = - plugins.cloud?.isServerlessEnabled && plugins.cloud?.serverless.projectType === 'security'; + const serverlessProjectType = this.isServerless + ? (plugins.cloud?.serverless.projectType as ServerlessProjectType) + : undefined; registerConnectorTypes({ actions: plugins.actions, @@ -201,7 +201,7 @@ export class CasePlugin logger: this.logger, getCasesClient, getSpaceId, - isServerlessSecurity, + serverlessProjectType, }); return {