diff --git a/x-pack/plugins/cloud_security_posture/server/plugin.ts b/x-pack/plugins/cloud_security_posture/server/plugin.ts index f2f81ed608ba4..2709518ffbc5f 100755 --- a/x-pack/plugins/cloud_security_posture/server/plugin.ts +++ b/x-pack/plugins/cloud_security_posture/server/plugin.ts @@ -18,6 +18,7 @@ import type { CspServerPluginStart, CspServerPluginSetupDeps, CspServerPluginStartDeps, + CspRequestHandlerContext, } from './types'; import { defineRoutes } from './routes'; import { cspRuleAssetType } from './saved_objects/cis_1_4_1/csp_rule_type'; @@ -55,7 +56,7 @@ export class CspPlugin core.savedObjects.registerType(cspRuleAssetType); - const router = core.http.createRouter(); + const router = core.http.createRouter(); // Register server side APIs defineRoutes(router, cspAppContext); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts index 8d33d3db189d3..f6363794213ac 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts @@ -4,7 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { httpServiceMock, loggingSystemMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import { + httpServerMock, + httpServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; +import { + ElasticsearchClientMock, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from 'src/core/server/elasticsearch/client/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaRequest } from 'src/core/server/http/router/request'; import { defineGetBenchmarksRoute, benchmarksInputSchema, @@ -14,6 +25,7 @@ import { getAgentPolicies, createBenchmarkEntry, } from './benchmarks'; + import { SavedObjectsClientContract } from 'src/core/server'; import { createMockAgentPolicyService, @@ -25,6 +37,17 @@ import { AgentPolicy } from '../../../../fleet/common'; import { CspAppService } from '../../lib/csp_app_services'; import { CspAppContext } from '../../plugin'; +export const getMockCspContext = (mockEsClient: ElasticsearchClientMock): KibanaRequest => { + return { + core: { + elasticsearch: { + client: { asCurrentUser: mockEsClient }, + }, + }, + fleet: { authz: { fleet: { all: false } } }, + } as unknown as KibanaRequest; +}; + function createMockAgentPolicy(props: Partial = {}): AgentPolicy { return { id: 'some-uuid1', @@ -66,6 +89,54 @@ describe('benchmarks API', () => { expect(config.path).toEqual('/api/csp/benchmarks'); }); + it('should accept to a user with fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineGetBenchmarksRoute(router, cspContext); + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: true } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); + + it('should reject to a user without fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineGetBenchmarksRoute(router, cspContext); + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: false } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(1); + }); + describe('test input schema', () => { it('expect to find default values', async () => { const validatedQuery = benchmarksInputSchema.validate({}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 1e6eadb0c77f6..366fcd9e409e9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -5,7 +5,7 @@ * 2.0. */ import { uniq, map } from 'lodash'; -import type { IRouter, SavedObjectsClientContract } from 'src/core/server'; +import type { SavedObjectsClientContract } from 'src/core/server'; import { schema as rt, TypeOf } from '@kbn/config-schema'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { @@ -23,6 +23,7 @@ import { BENCHMARKS_ROUTE_PATH, CIS_KUBERNETES_PACKAGE_NAME } from '../../../com import { CspAppContext } from '../../plugin'; import type { Benchmark } from '../../../common/types'; import { isNonNullable } from '../../../common/utils/helpers'; +import { CspRouter } from '../../types'; type BenchmarksQuerySchema = TypeOf; @@ -132,13 +133,17 @@ const createBenchmarks = ( .filter(isNonNullable); }); -export const defineGetBenchmarksRoute = (router: IRouter, cspContext: CspAppContext): void => +export const defineGetBenchmarksRoute = (router: CspRouter, cspContext: CspAppContext): void => router.get( { path: BENCHMARKS_ROUTE_PATH, validate: { query: benchmarksInputSchema }, }, async (context, request, response) => { + if (!context.fleet.authz.fleet.all) { + return response.forbidden(); + } + try { const soClient = context.core.savedObjects.client; const { query } = request; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.test.ts new file mode 100644 index 0000000000000..95addd9c055de --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { httpServerMock, httpServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import // eslint-disable-next-line @kbn/eslint/no-restricted-paths +'src/core/server/elasticsearch/client/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaRequest } from 'src/core/server/http/router/request'; +import { defineGetComplianceDashboardRoute } from './compliance_dashboard'; + +import { CspAppService } from '../../lib/csp_app_services'; +import { CspAppContext } from '../../plugin'; + +describe('compliance dashboard permissions API', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + jest.clearAllMocks(); + }); + + it('should accept to a user with fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineGetComplianceDashboardRoute(router, cspContext); + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: true } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); + + it('should reject to a user without fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineGetComplianceDashboardRoute(router, cspContext); + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: true } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts index f554eb91a4a49..e414dab92606a 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/compliance_dashboard/compliance_dashboard.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, IRouter } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import { transformError } from '@kbn/securitysolution-es-utils'; import type { AggregationsMultiBucketAggregateBase as Aggregation, @@ -19,6 +19,7 @@ import { CspAppContext } from '../../plugin'; import { getResourcesTypes } from './get_resources_types'; import { getClusters } from './get_clusters'; import { getStats } from './get_stats'; +import { CspRouter } from '../../types'; export interface ClusterBucket { ordered_top_hits: AggregationsTopHitsAggregate; @@ -75,7 +76,7 @@ const getLatestCyclesIds = async (esClient: ElasticsearchClient): Promise router.get( diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts index 4e534d565d7e3..c558caea1e9d9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.test.ts @@ -6,7 +6,12 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { savedObjectsClientMock, httpServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { + savedObjectsClientMock, + httpServiceMock, + loggingSystemMock, + httpServerMock, +} from 'src/core/server/mocks'; import { convertRulesConfigToYaml, createRulesConfig, @@ -24,6 +29,7 @@ import { createPackagePolicyServiceMock } from '../../../../fleet/server/mocks'; import { cspRuleAssetSavedObjectType, CspRuleSchema } from '../../../common/schemas/csp_rule'; import { ElasticsearchClient, + KibanaRequest, SavedObjectsClientContract, SavedObjectsFindResponse, } from 'kibana/server'; @@ -55,6 +61,54 @@ describe('Update rules configuration API', () => { expect(config.path).toEqual('/api/csp/update_rules_config'); }); + it('should accept to a user with fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineUpdateRulesConfigRoute(router, cspContext); + const [_, handler] = router.post.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: true } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); + + it('should reject to a user without fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineUpdateRulesConfigRoute(router, cspContext); + const [_, handler] = router.post.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: true } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); + it('validate getCspRules input parameters', async () => { mockSoClient = savedObjectsClientMock.create(); mockSoClient.find.mockResolvedValueOnce({} as SavedObjectsFindResponse); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts index 50a4759c5ec52..a57d3902f266c 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/configuration/update_rules_configuration.ts @@ -6,7 +6,6 @@ */ import type { ElasticsearchClient, - IRouter, SavedObjectsClientContract, SavedObjectsFindResponse, } from 'src/core/server'; @@ -24,6 +23,7 @@ import { CspRuleSchema, cspRuleAssetSavedObjectType } from '../../../common/sche import { UPDATE_RULES_CONFIG_ROUTE_PATH } from '../../../common/constants'; import { CIS_KUBERNETES_PACKAGE_NAME } from '../../../common/constants'; import { PackagePolicyServiceInterface } from '../../../../fleet/server'; +import { CspRouter } from '../../types'; export const getPackagePolicy = async ( soClient: SavedObjectsClientContract, @@ -99,13 +99,17 @@ export const updatePackagePolicy = ( return packagePolicyService.update(soClient, esClient, packagePolicy.id, updatedPackagePolicy); }; -export const defineUpdateRulesConfigRoute = (router: IRouter, cspContext: CspAppContext): void => +export const defineUpdateRulesConfigRoute = (router: CspRouter, cspContext: CspAppContext): void => router.post( { path: UPDATE_RULES_CONFIG_ROUTE_PATH, validate: { query: configurationUpdateInputSchema }, }, async (context, request, response) => { + if (!context.fleet.authz.fleet.all) { + return response.forbidden(); + } + try { const esClient = context.core.elasticsearch.client.asCurrentUser; const soClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.test.ts index cfd180a86169d..c41245db04685 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.test.ts @@ -27,6 +27,7 @@ export const getMockCspContext = (mockEsClient: ElasticsearchClientMock): Kibana client: { asCurrentUser: mockEsClient }, }, }, + fleet: { authz: { fleet: { all: true } } }, } as unknown as KibanaRequest; }; @@ -56,6 +57,54 @@ describe('findings API', () => { expect(config.path).toEqual('/api/csp/findings'); }); + it('should accept to a user with fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineFindingsIndexRoute(router, cspContext); + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: true } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(0); + }); + + it('should reject to a user without fleet.all privilege', async () => { + const router = httpServiceMock.createRouter(); + const cspAppContextService = new CspAppService(); + + const cspContext: CspAppContext = { + logger, + service: cspAppContextService, + }; + defineFindingsIndexRoute(router, cspContext); + const [_, handler] = router.get.mock.calls[0]; + + const mockContext = { + fleet: { authz: { fleet: { all: false } } }, + } as unknown as KibanaRequest; + + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest(); + const [context, req, res] = [mockContext, mockRequest, mockResponse]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledTimes(1); + }); + describe('test input schema', () => { it('expect to find default values', async () => { const validatedQuery = findingsInputSchema.validate({}); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.ts b/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.ts index ca95efae3d56a..cdbbfbe5ff69d 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/findings/findings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IRouter, Logger } from 'src/core/server'; +import type { Logger } from 'src/core/server'; import { SearchRequest, QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { QueryDslBoolQuery } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -16,6 +16,7 @@ import { getLatestCycleIds } from './get_latest_cycle_ids'; import { CSP_KUBEBEAT_INDEX_PATTERN, FINDINGS_ROUTE_PATH } from '../../../common/constants'; import { CspAppContext } from '../../plugin'; +import { CspRouter } from '../../types'; type FindingsQuerySchema = TypeOf; @@ -103,13 +104,17 @@ const buildOptionsRequest = (queryParams: FindingsQuerySchema): FindingsOptions ...getSearchFields(queryParams.fields), }); -export const defineFindingsIndexRoute = (router: IRouter, cspContext: CspAppContext): void => +export const defineFindingsIndexRoute = (router: CspRouter, cspContext: CspAppContext): void => router.get( { path: FINDINGS_ROUTE_PATH, validate: { query: findingsInputSchema }, }, async (context, request, response) => { + if (!context.fleet.authz.fleet.all) { + return response.forbidden(); + } + try { const esClient = context.core.elasticsearch.client.asCurrentUser; const options = buildOptionsRequest(request.query); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/index.ts b/x-pack/plugins/cloud_security_posture/server/routes/index.ts index aa04a610aa486..a0981e2a956cd 100755 --- a/x-pack/plugins/cloud_security_posture/server/routes/index.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/index.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { IRouter } from '../../../../../src/core/server'; import { defineGetComplianceDashboardRoute } from './compliance_dashboard/compliance_dashboard'; import { defineGetBenchmarksRoute } from './benchmarks/benchmarks'; import { defineFindingsIndexRoute as defineGetFindingsIndexRoute } from './findings/findings'; import { defineUpdateRulesConfigRoute } from './configuration/update_rules_configuration'; import { CspAppContext } from '../plugin'; +import { CspRouter } from '../types'; -export function defineRoutes(router: IRouter, cspContext: CspAppContext) { +export function defineRoutes(router: CspRouter, cspContext: CspAppContext) { defineGetComplianceDashboardRoute(router, cspContext); defineGetFindingsIndexRoute(router, cspContext); defineGetBenchmarksRoute(router, cspContext); diff --git a/x-pack/plugins/cloud_security_posture/server/types.ts b/x-pack/plugins/cloud_security_posture/server/types.ts index 4e70027013df8..9fe602424321c 100644 --- a/x-pack/plugins/cloud_security_posture/server/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/types.ts @@ -10,7 +10,14 @@ import type { PluginStart as DataPluginStart, } from '../../../../src/plugins/data/server'; -import type { FleetStartContract } from '../../fleet/server'; +import type { + RouteMethod, + KibanaResponseFactory, + RequestHandler, + IRouter, +} from '../../../../src/core/server'; + +import type { FleetStartContract, FleetRequestHandlerContext } from '../../fleet/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CspServerPluginSetup {} @@ -29,3 +36,23 @@ export interface CspServerPluginStartDeps { data: DataPluginStart; fleet: FleetStartContract; } + +export type CspRequestHandlerContext = FleetRequestHandlerContext; + +/** + * Convenience type for request handlers in CSP that includes the CspRequestHandlerContext type + * @internal + */ +export type CspRequestHandler< + P = unknown, + Q = unknown, + B = unknown, + Method extends RouteMethod = any, + ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory +> = RequestHandler; + +/** + * Convenience type for routers in Csp that includes the CspRequestHandlerContext type + * @internal + */ +export type CspRouter = IRouter;