From b1a75ae83105d5668b22b2256e7de93591f3408a Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Wed, 14 Dec 2022 16:24:12 +0100 Subject: [PATCH] [Fleet][Endpoint][RBAC V2] Update fleet router and config to allow API access via RBAC controls (#145361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary > **Note** > This PR is adding changes only to some of `api/fleet/package_policies` API routes, there will be subsequent PRs after this to update `api/fleet/epm/packages`, `api/fleet/agent_policeis` and, `api/fleet/agent_status`. This PR introduces the framework needed in fleet in order to be able to support Package level Privileges - meaning: if a user does not have authorization granted via Fleet and/or Integration privileges, then package level privileges are check and API access granted. When access is granted based on Package Privileges, the data is also validated to ensure that it is limited to the integration package names that were given authorization to the API. The following APIs were updated to leverage this new framework: - Integration Package Policy list API - Integration Package Policy get one API - Integration Package Policy update one API - Integration Package Policy bulk get API > ℹ️ these API were updated in support of Endpoint use cases needed for v8.7. Example of API error for Package policies api: ```json5 { "statusCode": 403, "error": "Forbidden", "message": "Authorization denied to [package.name=fleet_server]. Allowed package.name's: endpoint" } ``` ___________ To test: 1. Log in as `elastic`/superuser and create some agent policies. 1. Under `Stack Management`, create a role `policy_role` with the following RBAC settings. **DO NOT** select `Fleet -> All` or toggle `Integrations`. Leave those RBAC toggles set to `None` Screenshot 2022-11-16 at 14 45 15 2. Create a user e.g. `policy_user` and assign them _only_ the above role. **NOT** `superuser`. 3. Login with this user and navigate to `app/security/administration/policy` or curl/postman. 4. Expect to see the following: - GET `api/fleet/epm/packages?category=security` should return a `403` status. - GET `api/fleet/package_policies?page=1&perPage=10&kuery=ingest-package-policies.package.name%3A%20endpoint` should return a list of policies. - GET `/api/fleet/package_policies/` should return a `200` and a signle item that has the policie's details. Note that the package name of this item is `endpoint`. - there should be a POST API request matching `api/fleet/agent_policies/_bulk_get`, and should return a `403`. 5. With `Policy Management` RBAC set to `All` - PUT `http://localhost:5601/api/fleet/package_policies/` should return a `200` with the updated policy details as response ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: Paul Tavares --- .../plugins/fleet/common/services/routes.ts | 2 +- x-pack/plugins/fleet/server/mocks/index.ts | 5 +- x-pack/plugins/fleet/server/plugin.ts | 46 +- .../fleet/server/routes/agent/index.ts | 3 +- .../server/routes/agent_policy/handlers.ts | 10 +- .../fleet/server/routes/agent_policy/index.ts | 3 +- .../plugins/fleet/server/routes/app/index.ts | 4 +- .../fleet/server/routes/data_streams/index.ts | 3 +- .../server/routes/download_source/index.tsx | 3 +- .../server/routes/enrollment_api_key/index.ts | 3 +- .../fleet/server/routes/epm/handlers.ts | 20 +- .../plugins/fleet/server/routes/epm/index.ts | 3 +- .../server/routes/fleet_proxies/index.ts | 4 +- .../fleet_server_policy_config/index.ts | 4 +- .../fleet/server/routes/health_check/index.ts | 3 +- x-pack/plugins/fleet/server/routes/index.ts | 3 +- .../fleet/server/routes/output/index.ts | 3 +- .../routes/package_policy/handlers.test.ts | 3 +- .../server/routes/package_policy/handlers.ts | 115 +++- .../server/routes/package_policy/index.ts | 42 +- .../server/routes/preconfiguration/index.ts | 3 +- .../plugins/fleet/server/routes/security.ts | 297 ---------- .../fleet/server/routes/settings/index.ts | 7 +- .../server/routes/setup/handlers.test.ts | 5 +- .../fleet/server/routes/setup/handlers.ts | 2 +- .../fleet/server/routes/setup/index.ts | 4 +- .../services/agents/agent_service.test.ts | 4 +- .../server/services/agents/agent_service.ts | 2 +- .../services/epm/package_service.test.ts | 2 +- .../server/services/epm/package_service.ts | 2 +- .../fleet/server/services/package_policy.ts | 11 +- .../security/fleet_router.test.ts} | 106 +--- .../server/services/security/fleet_router.ts | 95 +++ .../fleet/server/services/security/index.ts | 17 + .../services/security/route_required_authz.ts | 175 ++++++ .../server/services/security/security.test.ts | 540 ++++++++++++++++++ .../server/services/security/security.ts | 252 ++++++++ .../fleet/server/services/security/types.ts | 62 ++ .../fleet/server/types/request_context.ts | 18 +- .../apis/epm/bulk_upgrade.ts | 2 + 40 files changed, 1405 insertions(+), 483 deletions(-) delete mode 100644 x-pack/plugins/fleet/server/routes/security.ts rename x-pack/plugins/fleet/server/{routes/security.test.ts => services/security/fleet_router.test.ts} (66%) create mode 100644 x-pack/plugins/fleet/server/services/security/fleet_router.ts create mode 100644 x-pack/plugins/fleet/server/services/security/index.ts create mode 100644 x-pack/plugins/fleet/server/services/security/route_required_authz.ts create mode 100644 x-pack/plugins/fleet/server/services/security/security.test.ts create mode 100644 x-pack/plugins/fleet/server/services/security/security.ts create mode 100644 x-pack/plugins/fleet/server/services/security/types.ts diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 59c158fe4dee..17b193fd8624 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -257,7 +257,7 @@ export const settingsRoutesService = { }; export const appRoutesService = { - getCheckPermissionsPath: (fleetServerSetup?: boolean) => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN, + getCheckPermissionsPath: () => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN, getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, postHealthCheckPath: () => APP_API_ROUTES.HEALTH_CHECK_PATTERN, }; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index eb98d0e68eca..01d92b8c8f28 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -90,10 +90,9 @@ export const createFleetRequestHandlerContextMock = (): jest.Mocked< asCurrentUser: createPackagePolicyServiceMock(), asInternalUser: createPackagePolicyServiceMock(), }, - epm: { - internalSoClient: savedObjectsClientMock.create(), - }, + internalSoClient: savedObjectsClientMock.create(), spaceId: 'default', + limitedToPackages: undefined, }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index cadb7859cc2e..7bcf9502d59b 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -57,6 +57,13 @@ import type { ExperimentalFeatures } from '../common/experimental_features'; import { INTEGRATIONS_PLUGIN_ID } from '../common'; import { parseExperimentalConfigValue } from '../common/experimental_features'; +import { + getRouteRequiredAuthz, + makeRouterWithFleetAuthz, + calculateRouteAuthz, + getAuthzFromRequest, +} from './services/security'; + import { PLUGIN_ID, OUTPUT_SAVED_OBJECT_TYPE, @@ -93,7 +100,6 @@ import { fetchAgentsUsage, fetchFleetUsage, } from './collectors/register'; -import { getAuthzFromRequest, makeRouterWithFleetAuthz } from './routes/security'; import { FleetArtifactsClient } from './services/artifacts'; import type { FleetRouter } from './types/request_context'; import { TelemetryEventsSender } from './telemetry/sender'; @@ -337,7 +343,19 @@ export class FleetPlugin PLUGIN_ID, async (context, request) => { const plugin = this; - const esClient = (await context.core).elasticsearch.client; + const coreContext = await context.core; + const authz = await getAuthzFromRequest(request); + const esClient = coreContext.elasticsearch.client; + + const routeRequiredAuthz = getRouteRequiredAuthz(request.route.method, request.route.path); + const routeAuthz = routeRequiredAuthz + ? calculateRouteAuthz(authz, routeRequiredAuthz) + : undefined; + + const getInternalSoClient = (): SavedObjectsClientContract => + appContextService + .getSavedObjects() + .getScopedClient(request, { excludedExtensions: [SECURITY_EXTENSION_ID] }); return { get agentClient() { @@ -356,18 +374,21 @@ export class FleetPlugin asInternalUser: service.asInternalUser, }; }, - authz: await getAuthzFromRequest(request), - epm: { + authz, + + get internalSoClient() { // Use a lazy getter to avoid constructing this client when not used by a request handler - get internalSoClient() { - return appContextService - .getSavedObjects() - .getScopedClient(request, { excludedExtensions: [SECURITY_EXTENSION_ID] }); - }, + return getInternalSoClient(); }, get spaceId() { return deps.spaces.spacesService.getSpaceId(request); }, + + get limitedToPackages() { + if (routeAuthz && routeAuthz.granted) { + return routeAuthz.scopeDataToPackages; + } + }, }; } ); @@ -384,10 +405,11 @@ export class FleetPlugin // Only some endpoints require superuser so we pass a raw IRouter here // For all the routes we enforce the user to have role superuser - const { router: fleetAuthzRouter, onPostAuthHandler: fleetAuthzOnPostAuthHandler } = - makeRouterWithFleetAuthz(router); + const fleetAuthzRouter = makeRouterWithFleetAuthz( + router, + this.initializerContext.logger.get('fleet_authz_router') + ); - core.http.registerOnPostAuth(fleetAuthzOnPostAuthHandler); registerRoutes(fleetAuthzRouter, config); this.telemetryEventsSender.setup(deps.telemetry); diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index e8ac9a2de674..fd75c7a7020a 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { AGENT_API_ROUTES } from '../../constants'; import { GetAgentsRequestSchema, @@ -30,7 +32,6 @@ import { } from '../../types'; import * as AgentService from '../../services/agents'; import type { FleetConfigType } from '../..'; -import type { FleetAuthzRouter } from '../security'; import { PostBulkUpdateAgentTagsRequestSchema } from '../../types/rest_spec/agent'; diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 42f4560f736b..9d0fe813a03a 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -67,7 +67,7 @@ export const getAgentPoliciesHandler: FleetRequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const fleetContext = await context.fleet; - const soClient = fleetContext.epm.internalSoClient; + const soClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const { full: withPackagePolicies = false, ...restOfQuery } = request.query; try { @@ -98,7 +98,7 @@ export const bulkGetAgentPoliciesHandler: FleetRequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const fleetContext = await context.fleet; - const soClient = fleetContext.epm.internalSoClient; + const soClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const { full: withPackagePolicies = false, ignoreMissing = false, ids } = request.body; try { @@ -158,7 +158,7 @@ export const createAgentPolicyHandler: FleetRequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const fleetContext = await context.fleet; - const soClient = fleetContext.epm.internalSoClient; + const soClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; @@ -276,7 +276,7 @@ export const getFullAgentPolicy: FleetRequestHandler< TypeOf > = async (context, request, response) => { const fleetContext = await context.fleet; - const soClient = fleetContext.epm.internalSoClient; + const soClient = fleetContext.internalSoClient; if (request.query.kubernetes === true) { try { @@ -332,7 +332,7 @@ export const downloadFullAgentPolicy: FleetRequestHandler< TypeOf > = async (context, request, response) => { const fleetContext = await context.fleet; - const soClient = fleetContext.epm.internalSoClient; + const soClient = fleetContext.internalSoClient; const { params: { agentPolicyId }, } = request; diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts index 938d8b3332b7..7c9726227f43 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { AGENT_POLICY_API_ROUTES } from '../../constants'; import { GetAgentPoliciesRequestSchema, @@ -17,7 +19,6 @@ import { GetK8sManifestRequestSchema, BulkGetAgentPoliciesRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { K8S_API_ROUTES } from '../../../common/constants'; diff --git a/x-pack/plugins/fleet/server/routes/app/index.ts b/x-pack/plugins/fleet/server/routes/app/index.ts index dd16c34ede80..cb0c12f9211a 100644 --- a/x-pack/plugins/fleet/server/routes/app/index.ts +++ b/x-pack/plugins/fleet/server/routes/app/index.ts @@ -8,11 +8,12 @@ import type { RequestHandler } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import type { FleetAuthzRouter } from '../../services/security'; + import { APP_API_ROUTES } from '../../constants'; import { appContextService } from '../../services'; import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../../common/types'; import { defaultFleetErrorHandler, GenerateServiceTokenError } from '../../errors'; -import type { FleetAuthzRouter } from '../security'; import type { FleetRequestHandler } from '../../types'; import { CheckPermissionsRequestSchema } from '../../types'; @@ -90,7 +91,6 @@ export const registerRoutes = (router: FleetAuthzRouter) => { { path: APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN, validate: CheckPermissionsRequestSchema, - options: { tags: [] }, }, getCheckPermissionsHandler ); diff --git a/x-pack/plugins/fleet/server/routes/data_streams/index.ts b/x-pack/plugins/fleet/server/routes/data_streams/index.ts index ddefc537ba20..861ade877192 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/index.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/index.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { DATA_STREAM_API_ROUTES } from '../../constants'; -import type { FleetAuthzRouter } from '../security'; import { getListHandler } from './handlers'; diff --git a/x-pack/plugins/fleet/server/routes/download_source/index.tsx b/x-pack/plugins/fleet/server/routes/download_source/index.tsx index 1c670bdf2b01..135cbf570078 100644 --- a/x-pack/plugins/fleet/server/routes/download_source/index.tsx +++ b/x-pack/plugins/fleet/server/routes/download_source/index.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { DOWNLOAD_SOURCE_API_ROUTES } from '../../constants'; import { getDownloadSourcesRequestSchema, @@ -13,7 +15,6 @@ import { PostDownloadSourcesRequestSchema, DeleteDownloadSourcesRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { getDownloadSourcesHandler, diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts index d904ab60e19e..496ec7ae705c 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { ENROLLMENT_API_KEY_ROUTES } from '../../constants'; import { GetEnrollmentAPIKeysRequestSchema, @@ -12,7 +14,6 @@ import { DeleteEnrollmentAPIKeyRequestSchema, PostEnrollmentAPIKeyRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { getEnrollmentApiKeysHandler, diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index e66bfa473edb..b4b189d3ad2b 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -86,7 +86,7 @@ export const getListHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { try { - const savedObjectsClient = (await context.fleet).epm.internalSoClient; + const savedObjectsClient = (await context.fleet).internalSoClient; const res = await getPackages({ savedObjectsClient, ...request.query, @@ -112,7 +112,7 @@ export const getLimitedListHandler: FleetRequestHandler< undefined > = async (context, request, response) => { try { - const savedObjectsClient = (await context.fleet).epm.internalSoClient; + const savedObjectsClient = (await context.fleet).internalSoClient; const res = await getLimitedPackages({ savedObjectsClient, prerelease: request.query.prerelease, @@ -134,7 +134,7 @@ export const getFileHandler: FleetRequestHandler< > = async (context, request, response) => { try { const { pkgName, pkgVersion, filePath } = request.params; - const savedObjectsClient = (await context.fleet).epm.internalSoClient; + const savedObjectsClient = (await context.fleet).internalSoClient; const installation = await getInstallation({ savedObjectsClient, pkgName }); const useLocalFile = pkgVersion === installation?.version; @@ -208,7 +208,7 @@ export const getInfoHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { try { - const savedObjectsClient = (await context.fleet).epm.internalSoClient; + const savedObjectsClient = (await context.fleet).internalSoClient; const { pkgName, pkgVersion } = request.params; const { ignoreUnverified = false, full = false, prerelease } = request.query; if (pkgVersion && !semverValid(pkgVersion)) { @@ -237,7 +237,7 @@ export const updatePackageHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { try { - const savedObjectsClient = (await context.fleet).epm.internalSoClient; + const savedObjectsClient = (await context.fleet).internalSoClient; const { pkgName } = request.params; const res = await updatePackage({ savedObjectsClient, pkgName, ...request.body }); @@ -256,7 +256,7 @@ export const getStatsHandler: FleetRequestHandler< > = async (context, request, response) => { try { const { pkgName } = request.params; - const savedObjectsClient = (await context.fleet).epm.internalSoClient; + const savedObjectsClient = (await context.fleet).internalSoClient; const body: GetStatsResponse = { response: await getPackageUsageStats({ savedObjectsClient, pkgName }), }; @@ -273,7 +273,7 @@ export const installPackageFromRegistryHandler: FleetRequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const fleetContext = await context.fleet; - const savedObjectsClient = fleetContext.epm.internalSoClient; + const savedObjectsClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const { pkgName, pkgVersion } = request.params; @@ -323,7 +323,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const fleetContext = await context.fleet; - const savedObjectsClient = fleetContext.epm.internalSoClient; + const savedObjectsClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const spaceId = fleetContext.spaceId; const bulkInstalledResponses = await bulkInstallPackages({ @@ -354,7 +354,7 @@ export const installPackageByUploadHandler: FleetRequestHandler< } const coreContext = await context.core; const fleetContext = await context.fleet; - const savedObjectsClient = fleetContext.epm.internalSoClient; + const savedObjectsClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later const archiveBuffer = Buffer.from(request.body); @@ -390,7 +390,7 @@ export const deletePackageHandler: FleetRequestHandler< const { pkgName, pkgVersion } = request.params; const coreContext = await context.core; const fleetContext = await context.fleet; - const savedObjectsClient = fleetContext.epm.internalSoClient; + const savedObjectsClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const res = await removeInstallation({ savedObjectsClient, diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index cab94b37f398..383f9c3116d8 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -7,6 +7,8 @@ import type { IKibanaResponse } from '@kbn/core/server'; +import type { FleetAuthzRouter } from '../../services/security'; + import type { DeletePackageResponse, GetInfoResponse, @@ -32,7 +34,6 @@ import { UpdatePackageRequestSchema, UpdatePackageRequestSchemaDeprecated, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { getCategoriesHandler, diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts index bca9d465a2fd..079b475e4f53 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { FLEET_PROXY_API_ROUTES } from '../../../common/constants'; import { GetOneFleetProxyRequestSchema, @@ -11,8 +13,6 @@ import { PutFleetProxyRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; - import { getAllFleetProxyHandler, postFleetProxyHandler, diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts b/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts index 48607c5df7a7..f4f4837554d8 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { FLEET_SERVER_HOST_API_ROUTES } from '../../../common/constants'; import { GetAllFleetServerHostRequestSchema, @@ -13,8 +15,6 @@ import { PutFleetServerHostRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; - import { deleteFleetServerPolicyHandler, getAllFleetServerPolicyHandler, diff --git a/x-pack/plugins/fleet/server/routes/health_check/index.ts b/x-pack/plugins/fleet/server/routes/health_check/index.ts index 61b42b3bc2aa..55bd4bf7029e 100644 --- a/x-pack/plugins/fleet/server/routes/health_check/index.ts +++ b/x-pack/plugins/fleet/server/routes/health_check/index.ts @@ -9,9 +9,10 @@ import https from 'https'; import type { TypeOf } from '@kbn/config-schema'; import fetch from 'node-fetch'; +import type { FleetAuthzRouter } from '../../services/security'; + import { APP_API_ROUTES } from '../../constants'; import type { FleetRequestHandler } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { defaultFleetErrorHandler } from '../../errors'; import { PostHealthCheckRequestSchema } from '../../types'; diff --git a/x-pack/plugins/fleet/server/routes/index.ts b/x-pack/plugins/fleet/server/routes/index.ts index 5ecbb3eaad80..b5f82f992353 100644 --- a/x-pack/plugins/fleet/server/routes/index.ts +++ b/x-pack/plugins/fleet/server/routes/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../services/security'; + import type { FleetConfigType } from '../config'; import { registerRoutes as registerAgentPolicyRoutes } from './agent_policy'; @@ -22,7 +24,6 @@ import { registerRoutes as registerDownloadSourcesRoutes } from './download_sour import { registerRoutes as registerHealthCheckRoutes } from './health_check'; import { registerRoutes as registerFleetServerHostRoutes } from './fleet_server_policy_config'; import { registerRoutes as registerFleetProxiesRoutes } from './fleet_proxies'; -import type { FleetAuthzRouter } from './security'; export async function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: FleetConfigType) { // Always register app routes for permissions checking diff --git a/x-pack/plugins/fleet/server/routes/output/index.ts b/x-pack/plugins/fleet/server/routes/output/index.ts index f74c1bb88aeb..827fca7c9444 100644 --- a/x-pack/plugins/fleet/server/routes/output/index.ts +++ b/x-pack/plugins/fleet/server/routes/output/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { OUTPUT_API_ROUTES } from '../../constants'; import { DeleteOutputRequestSchema, @@ -13,7 +15,6 @@ import { PostOutputRequestSchema, PutOutputRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { deleteOutputHandler, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 13beda40e885..5ade3dd8e169 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -9,6 +9,8 @@ import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; import type { KibanaRequest } from '@kbn/core/server'; import type { RouteConfig } from '@kbn/core/server'; +import type { FleetAuthzRouter } from '../../services/security'; + import { PACKAGE_POLICY_API_ROUTES } from '../../../common/constants'; import { appContextService, packagePolicyService } from '../../services'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; @@ -22,7 +24,6 @@ import type { CreatePackagePolicyRequestSchema, UpdatePackagePolicyRequestSchema, } from '../../types/rest_spec'; -import type { FleetAuthzRouter } from '../security'; import type { FleetRequestHandler } from '../../types'; import type { PackagePolicy } from '../../types'; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index ee9caa4def67..ab3af0cb0a16 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -43,16 +43,72 @@ import { simplifiedPackagePolicytoNewPackagePolicy } from '../../../common/servi import type { SimplifiedPackagePolicy } from '../../../common/services/simplified_package_policy_helper'; -export const getPackagePoliciesHandler: RequestHandler< +const getAllowedPackageNamesMessage = (allowedPackageNames: string[]): string => { + return `Allowed package.name's: ${allowedPackageNames.join(', ')}`; +}; + +/** + * Validates that Package Policy data only includes `package.name`'s that are in the list of + * `allowedPackageNames`. If an error is encountered, then a message is return, otherwise, undefined. + * + * @param data + * @param allowedPackageNames + */ +const validatePackagePolicyDataIsScopedToAllowedPackageNames = ( + data: PackagePolicy[], + allowedPackageNames: string[] | undefined +): string | undefined => { + if (!data.length || typeof allowedPackageNames === 'undefined') { + return; + } + + if (!allowedPackageNames.length) { + return 'Authorization denied due to lack of integration package privileges'; + } + + // Because List type of APIs have an un-bounded `perPage` query param, we only validate the + // data up to the first package.name that we find is not authorized. + for (const packagePolicy of data) { + if (!packagePolicy.package) { + return `Authorization denied. ${getAllowedPackageNamesMessage(allowedPackageNames)}`; + } + + if (!allowedPackageNames.includes(packagePolicy.package.name)) { + return `Authorization denied to [package.name=${ + packagePolicy.package.name + }]. ${getAllowedPackageNamesMessage(allowedPackageNames)}`; + } + } +}; + +export const getPackagePoliciesHandler: FleetRequestHandler< undefined, TypeOf > = async (context, request, response) => { - const soClient = (await context.core).savedObjects.client; + const fleetContext = await context.fleet; + const soClient = fleetContext.internalSoClient; + const limitedToPackages = fleetContext.limitedToPackages; + try { const { items, total, page, perPage } = await packagePolicyService.list( soClient, request.query ); + + // specific to package-level RBAC + const validationResult = validatePackagePolicyDataIsScopedToAllowedPackageNames( + items, + limitedToPackages + ); + if (validationResult) { + return response.forbidden({ + body: { + message: validationResult, + }, + }); + } + + // agnostic to package-level RBAC return response.ok({ body: { items, @@ -66,13 +122,16 @@ export const getPackagePoliciesHandler: RequestHandler< } }; -export const bulkGetPackagePoliciesHandler: RequestHandler< +export const bulkGetPackagePoliciesHandler: FleetRequestHandler< undefined, undefined, TypeOf > = async (context, request, response) => { - const soClient = (await context.core).savedObjects.client; + const fleetContext = await context.fleet; + const soClient = fleetContext.internalSoClient; + const limitedToPackages = fleetContext.limitedToPackages; const { ids, ignoreMissing } = request.body; + try { const items = await packagePolicyService.getByIDs(soClient, ids, { ignoreMissing, @@ -80,6 +139,18 @@ export const bulkGetPackagePoliciesHandler: RequestHandler< const body: BulkGetPackagePoliciesResponse = { items: items ?? [] }; + const validationResult = validatePackagePolicyDataIsScopedToAllowedPackageNames( + body.items, + limitedToPackages + ); + if (validationResult) { + return response.forbidden({ + body: { + message: validationResult, + }, + }); + } + return response.ok({ body, }); @@ -94,17 +165,32 @@ export const bulkGetPackagePoliciesHandler: RequestHandler< } }; -export const getOnePackagePolicyHandler: RequestHandler< +export const getOnePackagePolicyHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { - const soClient = (await context.core).savedObjects.client; + const fleetContext = await context.fleet; + const soClient = fleetContext.internalSoClient; + const limitedToPackages = fleetContext.limitedToPackages; const { packagePolicyId } = request.params; const notFoundResponse = () => response.notFound({ body: { message: `Package policy ${packagePolicyId} not found` } }); try { const packagePolicy = await packagePolicyService.get(soClient, packagePolicyId); + if (packagePolicy) { + const validationResult = validatePackagePolicyDataIsScopedToAllowedPackageNames( + [packagePolicy], + limitedToPackages + ); + if (validationResult) { + return response.forbidden({ + body: { + message: validationResult, + }, + }); + } + return response.ok({ body: { item: packagePolicy, @@ -184,7 +270,7 @@ export const createPackagePolicyHandler: FleetRequestHandler< > = async (context, request, response) => { const coreContext = await context.core; const fleetContext = await context.fleet; - const soClient = fleetContext.epm.internalSoClient; + const soClient = fleetContext.internalSoClient; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; const { force, package: pkg, ...newPolicy } = request.body; @@ -258,13 +344,15 @@ export const createPackagePolicyHandler: FleetRequestHandler< } }; -export const updatePackagePolicyHandler: RequestHandler< +export const updatePackagePolicyHandler: FleetRequestHandler< TypeOf, unknown, TypeOf > = async (context, request, response) => { const coreContext = await context.core; - const soClient = coreContext.savedObjects.client; + const fleetContext = await context.fleet; + const soClient = fleetContext.internalSoClient; + const limitedToPackages = fleetContext.limitedToPackages; const esClient = coreContext.elasticsearch.client.asInternalUser; const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; const packagePolicy = await packagePolicyService.get(soClient, request.params.packagePolicyId); @@ -273,6 +361,15 @@ export const updatePackagePolicyHandler: RequestHandler< throw Boom.notFound('Package policy not found'); } + if (limitedToPackages && limitedToPackages.length) { + const packageName = packagePolicy?.package?.name; + if (packageName && !limitedToPackages.includes(packageName)) { + return response.forbidden({ + body: { message: `Update for package name ${packageName} is not authorized.` }, + }); + } + } + try { const { force, package: pkg, ...body } = request.body; // TODO Remove deprecated APIs https://github.com/elastic/kibana/issues/121485 diff --git a/x-pack/plugins/fleet/server/routes/package_policy/index.ts b/x-pack/plugins/fleet/server/routes/package_policy/index.ts index 00d06363968f..393bbb2fd982 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/index.ts @@ -5,6 +5,11 @@ * 2.0. */ +import { getRouteRequiredAuthz } from '../../services/security'; + +import type { FleetAuthzRouter } from '../../services/security'; + +import type { FleetAuthz } from '../../../common'; import { PACKAGE_POLICY_API_ROUTES } from '../../constants'; import { GetPackagePoliciesRequestSchema, @@ -17,7 +22,7 @@ import { DeleteOnePackagePolicyRequestSchema, BulkGetPackagePoliciesRequestSchema, } from '../../types'; -import type { FleetAuthzRouter } from '../security'; +import { calculateRouteAuthz } from '../../services/security/security'; import { getPackagePoliciesHandler, @@ -38,20 +43,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { { path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, validate: GetPackagePoliciesRequestSchema, - fleetAuthz: { - integrations: { readIntegrationPolicies: true }, - }, + fleetAuthz: (fleetAuthz: FleetAuthz): boolean => + calculateRouteAuthz( + fleetAuthz, + getRouteRequiredAuthz('get', PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) + ).granted, }, getPackagePoliciesHandler ); + // Get bulk router.post( { path: PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN, validate: BulkGetPackagePoliciesRequestSchema, - fleetAuthz: { - integrations: { readIntegrationPolicies: true }, - }, + fleetAuthz: (fleetAuthz: FleetAuthz): boolean => + calculateRouteAuthz( + fleetAuthz, + getRouteRequiredAuthz('post', PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN) + ).granted, }, bulkGetPackagePoliciesHandler ); @@ -61,9 +71,11 @@ export const registerRoutes = (router: FleetAuthzRouter) => { { path: PACKAGE_POLICY_API_ROUTES.INFO_PATTERN, validate: GetOnePackagePolicyRequestSchema, - fleetAuthz: { - integrations: { readIntegrationPolicies: true }, - }, + fleetAuthz: (fleetAuthz: FleetAuthz): boolean => + calculateRouteAuthz( + fleetAuthz, + getRouteRequiredAuthz('get', PACKAGE_POLICY_API_ROUTES.INFO_PATTERN) + ).granted, }, getOnePackagePolicyHandler ); @@ -93,14 +105,16 @@ export const registerRoutes = (router: FleetAuthzRouter) => { { path: PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN, validate: UpdatePackagePolicyRequestSchema, - fleetAuthz: { - integrations: { writeIntegrationPolicies: true }, - }, + fleetAuthz: (fleetAuthz: FleetAuthz): boolean => + calculateRouteAuthz( + fleetAuthz, + getRouteRequiredAuthz('put', PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN) + ).granted, }, updatePackagePolicyHandler ); - // Delete + // Delete (bulk) router.post( { path: PACKAGE_POLICY_API_ROUTES.DELETE_PATTERN, diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts index 9f5f37229853..5d427b49bed6 100644 --- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts +++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts @@ -5,9 +5,10 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { PRECONFIGURATION_API_ROUTES } from '../../constants'; import { PostResetOnePreconfiguredAgentPoliciesSchema } from '../../types'; -import type { FleetAuthzRouter } from '../security'; import { resetPreconfigurationHandler, resetOnePreconfigurationHandler } from './handler'; diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts deleted file mode 100644 index 8914be58294b..000000000000 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; -import type { - IRouter, - RouteConfig, - RouteMethod, - KibanaRequest, - RequestHandler, - RequestHandlerContext, - OnPostAuthHandler, -} from '@kbn/core/server'; - -import type { FleetAuthz } from '../../common'; -import { INTEGRATIONS_PLUGIN_ID } from '../../common'; -import { calculateAuthz, calculatePackagePrivilegesFromKibanaPrivileges } from '../../common/authz'; - -import { appContextService } from '../services'; -import type { FleetRequestHandlerContext } from '../types'; -import { PLUGIN_ID, ENDPOINT_PRIVILEGES } from '../constants'; - -function checkSecurityEnabled() { - return appContextService.getSecurityLicense().isEnabled(); -} - -export function checkSuperuser(req: KibanaRequest) { - if (!checkSecurityEnabled()) { - return false; - } - - const security = appContextService.getSecurity(); - const user = security.authc.getCurrentUser(req); - if (!user) { - return false; - } - - const userRoles = user.roles || []; - if (!userRoles.includes('superuser')) { - return false; - } - - return true; -} - -function getAuthorizationFromPrivileges( - kibanaPrivileges: Array<{ - resource?: string; - privilege: string; - authorized: boolean; - }>, - searchPrivilege: string -) { - const privilege = kibanaPrivileges.find((p) => p.privilege.includes(searchPrivilege)); - return privilege ? privilege.authorized : false; -} - -export async function getAuthzFromRequest(req: KibanaRequest): Promise { - const security = appContextService.getSecurity(); - - if (security.authz.mode.useRbacForRequest(req)) { - const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req); - const endpointPrivileges = ENDPOINT_PRIVILEGES.map((privilege) => - security.authz.actions.api.get(`${DEFAULT_APP_CATEGORIES.security.id}-${privilege}`) - ); - const { privileges } = await checkPrivileges({ - kibana: [ - security.authz.actions.api.get(`${PLUGIN_ID}-all`), - security.authz.actions.api.get(`${PLUGIN_ID}-setup`), - security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-all`), - security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-read`), - ...endpointPrivileges, - ], - }); - const fleetAllAuth = getAuthorizationFromPrivileges(privileges.kibana, `${PLUGIN_ID}-all`); - const intAllAuth = getAuthorizationFromPrivileges( - privileges.kibana, - `${INTEGRATIONS_PLUGIN_ID}-all` - ); - const intReadAuth = getAuthorizationFromPrivileges( - privileges.kibana, - `${INTEGRATIONS_PLUGIN_ID}-read` - ); - const fleetSetupAuth = getAuthorizationFromPrivileges(privileges.kibana, 'fleet-setup'); - - return { - ...calculateAuthz({ - fleet: { all: fleetAllAuth, setup: fleetSetupAuth }, - integrations: { - all: intAllAuth, - read: intReadAuth, - }, - isSuperuser: checkSuperuser(req), - }), - packagePrivileges: calculatePackagePrivilegesFromKibanaPrivileges(privileges.kibana), - }; - } - - return calculateAuthz({ - fleet: { all: false, setup: false }, - integrations: { - all: false, - read: false, - }, - isSuperuser: false, - }); -} - -interface Authz { - [k: string]: Authz | boolean; -} - -function containsRequirement(authz: Authz, requirements: DeepPartialTruthy) { - if (!authz) { - return false; - } - for (const key of Object.keys(requirements)) { - if (typeof requirements[key] !== 'undefined' && typeof requirements[key] === 'boolean') { - if (!authz[key]) { - return false; - } - } else if ( - !containsRequirement(authz[key] as Authz, requirements[key] as DeepPartialTruthy) - ) { - return false; - } - } - return true; -} - -export function hasRequiredFleetAuthzPrivilege( - authz: FleetAuthz, - { fleetAuthz }: { fleetAuthz?: FleetAuthzRequirements } -): boolean { - if (!checkSecurityEnabled()) { - return false; - } - if (fleetAuthz && !containsRequirement(authz as unknown as Authz, fleetAuthz)) { - return false; - } - - return true; -} - -type DeepPartialTruthy = { - [P in keyof T]?: T[P] extends boolean ? true : DeepPartialTruthy; -}; - -type FleetAuthzRequirements = DeepPartialTruthy; - -type FleetAuthzRouteRegistrar< - Method extends RouteMethod, - Context extends RequestHandlerContext = RequestHandlerContext -> = ( - route: FleetRouteConfig, - handler: RequestHandler -) => void; - -export interface FleetAuthzRouteConfig { - fleetAuthz?: FleetAuthzRequirements; -} - -type FleetRouteConfig = RouteConfig & - FleetAuthzRouteConfig; - -// Fleet router that allow to add required access when registering route -export interface FleetAuthzRouter< - TContext extends FleetRequestHandlerContext = FleetRequestHandlerContext -> extends IRouter { - get: FleetAuthzRouteRegistrar<'get', TContext>; - delete: FleetAuthzRouteRegistrar<'delete', TContext>; - post: FleetAuthzRouteRegistrar<'post', TContext>; - put: FleetAuthzRouteRegistrar<'put', TContext>; - patch: FleetAuthzRouteRegistrar<'patch', TContext>; -} - -function shouldHandlePostAuthRequest(req: KibanaRequest) { - if (req?.route?.options?.tags) { - return req.route.options.tags.some((tag) => tag.match(/^fleet:authz/)); - } - return false; -} -// Exported for test only -export function deserializeAuthzConfig(tags: readonly string[]): FleetAuthzRouteConfig { - let fleetAuthz: FleetAuthzRequirements | undefined; - for (const tag of tags) { - if (!tag.match(/^fleet:authz/)) { - continue; - } - - if (!fleetAuthz) { - fleetAuthz = {}; - } - - tag - .replace(/^fleet:authz:/, '') - .split(':') - .reduce((acc: any, key, idx, keys) => { - if (idx === keys.length - 1) { - acc[key] = true; - - return acc; - } - - if (!acc[key]) { - acc[key] = {}; - } - - return acc[key]; - }, fleetAuthz); - } - - return { fleetAuthz }; -} - -// Exported for test only -export function serializeAuthzConfig(config: FleetAuthzRouteConfig): string[] { - const tags: string[] = []; - - if (config.fleetAuthz) { - function fleetAuthzToTags(requirements: DeepPartialTruthy, prefix: string = '') { - for (const key of Object.keys(requirements)) { - if (typeof requirements[key] === 'boolean') { - tags.push(`fleet:authz:${prefix}${key}`); - } else if (typeof requirements[key] !== 'undefined') { - fleetAuthzToTags(requirements[key] as DeepPartialTruthy, `${prefix}${key}:`); - } - } - } - - fleetAuthzToTags(config.fleetAuthz); - } - - return tags; -} - -export function makeRouterWithFleetAuthz( - router: IRouter -): { router: FleetAuthzRouter; onPostAuthHandler: OnPostAuthHandler } { - function buildFleetAuthzRouteConfig({ - fleetAuthz, - ...routeConfig - }: FleetRouteConfig) { - return { - ...routeConfig, - options: { - ...routeConfig.options, - tags: [ - ...(routeConfig?.options?.tags ?? []), - ...serializeAuthzConfig({ - fleetAuthz, - }), - ], - }, - }; - } - - const fleetAuthzOnPostAuthHandler: OnPostAuthHandler = async (req, res, toolkit) => { - if (!shouldHandlePostAuthRequest(req)) { - return toolkit.next(); - } - - if (!checkSecurityEnabled()) { - return res.forbidden(); - } - - const fleetAuthzConfig = deserializeAuthzConfig(req.route.options.tags); - - if (!fleetAuthzConfig) { - return toolkit.next(); - } - const authz = await getAuthzFromRequest(req); - if (!hasRequiredFleetAuthzPrivilege(authz, fleetAuthzConfig)) { - return res.forbidden(); - } - - return toolkit.next(); - }; - - const fleetAuthzRouter: FleetAuthzRouter = { - get: (routeConfig, handler) => router.get(buildFleetAuthzRouteConfig(routeConfig), handler), - delete: (routeConfig, handler) => - router.delete(buildFleetAuthzRouteConfig(routeConfig), handler), - post: (routeConfig, handler) => router.post(buildFleetAuthzRouteConfig(routeConfig), handler), - put: (routeConfig, handler) => router.put(buildFleetAuthzRouteConfig(routeConfig), handler), - patch: (routeConfig, handler) => router.patch(buildFleetAuthzRouteConfig(routeConfig), handler), - handleLegacyErrors: (handler) => router.handleLegacyErrors(handler), - getRoutes: () => router.getRoutes(), - routerPath: router.routerPath, - }; - - return { router: fleetAuthzRouter, onPostAuthHandler: fleetAuthzOnPostAuthHandler }; -} diff --git a/x-pack/plugins/fleet/server/routes/settings/index.ts b/x-pack/plugins/fleet/server/routes/settings/index.ts index f11244d7b59f..881541b56980 100644 --- a/x-pack/plugins/fleet/server/routes/settings/index.ts +++ b/x-pack/plugins/fleet/server/routes/settings/index.ts @@ -7,15 +7,16 @@ import type { TypeOf } from '@kbn/config-schema'; +import type { FleetAuthzRouter } from '../../services/security'; + import { SETTINGS_API_ROUTES } from '../../constants'; import type { FleetRequestHandler } from '../../types'; import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types'; import { defaultFleetErrorHandler } from '../../errors'; import { settingsService, agentPolicyService, appContextService } from '../../services'; -import type { FleetAuthzRouter } from '../security'; export const getSettingsHandler: FleetRequestHandler = async (context, request, response) => { - const soClient = (await context.fleet).epm.internalSoClient; + const soClient = (await context.fleet).internalSoClient; try { const settings = await settingsService.getSettings(soClient); @@ -39,7 +40,7 @@ export const putSettingsHandler: FleetRequestHandler< undefined, TypeOf > = async (context, request, response) => { - const soClient = (await context.fleet).epm.internalSoClient; + const soClient = (await context.fleet).internalSoClient; const esClient = (await context.core).elasticsearch.client.asInternalUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 0ecbca40132c..f4b35508e52c 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -51,10 +51,9 @@ describe('FleetSetupHandler', () => { asCurrentUser: createPackagePolicyServiceMock(), asInternalUser: createPackagePolicyServiceMock(), }, - epm: { - internalSoClient: savedObjectsClientMock.create(), - }, + internalSoClient: savedObjectsClientMock.create(), spaceId: 'default', + limitedToPackages: undefined, }, }; response = httpServerMock.createResponseFactory(); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index cf2ff46cd111..78daddf837ac 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -60,7 +60,7 @@ export const getFleetStatusHandler: FleetRequestHandler = async (context, reques export const fleetSetupHandler: FleetRequestHandler = async (context, request, response) => { try { - const soClient = (await context.fleet).epm.internalSoClient; + const soClient = (await context.fleet).internalSoClient; const esClient = (await context.core).elasticsearch.client.asInternalUser; const setupStatus = await setupFleet(soClient, esClient); const body: PostFleetSetupResponse = { diff --git a/x-pack/plugins/fleet/server/routes/setup/index.ts b/x-pack/plugins/fleet/server/routes/setup/index.ts index 8b2aa2bf8f57..b4470e648dca 100644 --- a/x-pack/plugins/fleet/server/routes/setup/index.ts +++ b/x-pack/plugins/fleet/server/routes/setup/index.ts @@ -5,11 +5,11 @@ * 2.0. */ +import type { FleetAuthzRouter } from '../../services/security'; + import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; import type { FleetConfigType } from '../../../common/types'; -import type { FleetAuthzRouter } from '../security'; - import { getFleetStatusHandler, fleetSetupHandler } from './handlers'; export const registerFleetSetupRoute = (router: FleetAuthzRouter) => { diff --git a/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts b/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts index be4ca37260b8..fe43fa50b431 100644 --- a/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -jest.mock('../../routes/security'); +jest.mock('../security'); jest.mock('./crud'); jest.mock('./status'); @@ -14,7 +14,7 @@ import { elasticsearchServiceMock, httpServerMock } from '@kbn/core/server/mocks import { FleetUnauthorizedError } from '../../errors'; -import { getAuthzFromRequest } from '../../routes/security'; +import { getAuthzFromRequest } from '../security'; import type { FleetAuthz } from '../../../common'; import type { AgentClient } from './agent_service'; diff --git a/x-pack/plugins/fleet/server/services/agents/agent_service.ts b/x-pack/plugins/fleet/server/services/agents/agent_service.ts index 576505e46bd2..8f732809adf5 100644 --- a/x-pack/plugins/fleet/server/services/agents/agent_service.ts +++ b/x-pack/plugins/fleet/server/services/agents/agent_service.ts @@ -12,7 +12,7 @@ import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; import type { AgentStatus, ListWithKuery } from '../../types'; import type { Agent, GetAgentStatusResponse } from '../../../common/types'; -import { getAuthzFromRequest } from '../../routes/security'; +import { getAuthzFromRequest } from '../security'; import { FleetUnauthorizedError } from '../../errors'; diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts index 44d35f3e4c33..779f0dad02c8 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -jest.mock('../../routes/security'); +jest.mock('../security'); import type { MockedLogger } from '@kbn/logging-mocks'; diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts index f3d82f13d96e..dfc02c4f68c5 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts @@ -22,7 +22,7 @@ import type { ArchivePackage, BundledPackage, } from '../../types'; -import { checkSuperuser } from '../../routes/security'; +import { checkSuperuser } from '../security'; import { FleetUnauthorizedError } from '../../errors'; import { installTransforms, isTransform } from './elasticsearch/transform/install'; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 7acd555380a6..a95cba87b39b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -82,8 +82,9 @@ import type { } from '../types'; import type { ExternalCallback } from '..'; -import type { FleetAuthzRouteConfig } from '../routes/security'; -import { getAuthzFromRequest, hasRequiredFleetAuthzPrivilege } from '../routes/security'; +import type { FleetAuthzRouteConfig } from './security'; + +import { getAuthzFromRequest, doesNotHaveRequiredFleetAuthz } from './security'; import { storedPackagePolicyToAgentInputs } from './agent_policies'; import { agentPolicyService } from './agent_policy'; @@ -1294,12 +1295,14 @@ export class PackagePolicyServiceImpl implements PackagePolicyService { public asScoped(request: KibanaRequest): PackagePolicyClient { - const preflightCheck = async (fleetAuthzConfig: FleetAuthzRouteConfig) => { + const preflightCheck = async ({ fleetAuthz: fleetRequiredAuthz }: FleetAuthzRouteConfig) => { const authz = await getAuthzFromRequest(request); - if (!hasRequiredFleetAuthzPrivilege(authz, fleetAuthzConfig)) { + + if (doesNotHaveRequiredFleetAuthz(authz, fleetRequiredAuthz)) { throw new FleetUnauthorizedError('Not authorized to this action on integration policies'); } }; + return new PackagePolicyClientWithAuthz(preflightCheck); } diff --git a/x-pack/plugins/fleet/server/routes/security.test.ts b/x-pack/plugins/fleet/server/services/security/fleet_router.test.ts similarity index 66% rename from x-pack/plugins/fleet/server/routes/security.test.ts rename to x-pack/plugins/fleet/server/services/security/fleet_router.test.ts index 15e46529c869..1f2b6c3fab22 100644 --- a/x-pack/plugins/fleet/server/routes/security.test.ts +++ b/x-pack/plugins/fleet/server/services/security/fleet_router.test.ts @@ -5,17 +5,25 @@ * 2.0. */ +import type { CheckPrivilegesDynamically } from '@kbn/security-plugin/server/authorization/check_privileges_dynamically'; import type { IRouter, RequestHandler, RouteConfig } from '@kbn/core/server'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; + +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; + import { coreMock } from '@kbn/core/server/mocks'; -import type { AuthenticatedUser, CheckPrivilegesPayload } from '@kbn/security-plugin/server'; + +import type { CheckPrivilegesPayload } from '@kbn/security-plugin/server'; + import type { CheckPrivilegesResponse } from '@kbn/security-plugin/server/authorization/types'; -import type { CheckPrivilegesDynamically } from '@kbn/security-plugin/server/authorization/check_privileges_dynamically'; -import { createAppContextStartContractMock } from '../mocks'; -import { appContextService } from '../services'; -import type { FleetRequestHandlerContext } from '../types'; +import type { FleetRequestHandlerContext } from '../..'; +import { createAppContextStartContractMock } from '../../mocks'; +import { appContextService } from '..'; + +import { makeRouterWithFleetAuthz } from './fleet_router'; -import { deserializeAuthzConfig, makeRouterWithFleetAuthz, serializeAuthzConfig } from './security'; +const mockLogger = loggingSystemMock.createLogger(); function getCheckPrivilegesMockedImplementation(kibanaRoles: string[]) { return (checkPrivileges: CheckPrivilegesPayload) => { @@ -82,12 +90,11 @@ describe('FleetAuthzRouter', () => { appContextService.start(mockContext); - const { router: wrappedRouter, onPostAuthHandler } = makeRouterWithFleetAuthz(fakeRouter); - wrappedRouter.get({ ...routeConfig } as RouteConfig, fakeHandler); + const fleetAuthzRouter = makeRouterWithFleetAuthz(fakeRouter, mockLogger); + fleetAuthzRouter.get({ ...routeConfig } as RouteConfig, fakeHandler); const wrappedHandler = fakeRouter.get.mock.calls[0][1]; const wrappedRouteConfig = fakeRouter.get.mock.calls[0][0]; const resFactory = { forbidden: jest.fn(() => 'forbidden'), ok: jest.fn(() => 'ok') }; - const fakeToolkit = { next: jest.fn(() => 'next') }; const fakeReq = { route: { @@ -96,11 +103,6 @@ describe('FleetAuthzRouter', () => { options: wrappedRouteConfig.options, }, } as any; - const onPostRes = await onPostAuthHandler(fakeReq, resFactory as any, fakeToolkit as any); - - if ((onPostRes as unknown) !== 'next') { - return onPostRes; - } const res = await wrappedHandler( { @@ -198,79 +200,3 @@ describe('FleetAuthzRouter', () => { }); }); }); - -describe('serializeAuthzConfig', () => { - it('should serialize authz to tags', () => { - const res = serializeAuthzConfig({ - fleetAuthz: { - fleet: { - readEnrollmentTokens: true, - setup: true, - }, - integrations: { - readPackageInfo: true, - removePackages: true, - }, - packagePrivileges: { - endpoint: { - actions: { - readPolicyManagement: { - executePackageAction: true, - }, - readBlocklist: { - executePackageAction: true, - }, - }, - }, - }, - }, - }); - - expect(res).toEqual([ - 'fleet:authz:fleet:readEnrollmentTokens', - 'fleet:authz:fleet:setup', - 'fleet:authz:integrations:readPackageInfo', - 'fleet:authz:integrations:removePackages', - 'fleet:authz:packagePrivileges:endpoint:actions:readPolicyManagement:executePackageAction', - 'fleet:authz:packagePrivileges:endpoint:actions:readBlocklist:executePackageAction', - ]); - }); -}); - -describe('deserializeAuthzConfig', () => { - it('should deserialize tags to fleet authz', () => { - const res = deserializeAuthzConfig([ - 'fleet:authz:fleet:readEnrollmentTokens', - 'fleet:authz:fleet:setup', - 'fleet:authz:integrations:readPackageInfo', - 'fleet:authz:integrations:removePackages', - 'fleet:authz:packagePrivileges:endpoint:actions:readPolicyManagement:executePackageAction', - 'fleet:authz:packagePrivileges:endpoint:actions:readBlocklist:executePackageAction', - ]); - - expect(res).toEqual({ - fleetAuthz: { - fleet: { - readEnrollmentTokens: true, - setup: true, - }, - integrations: { - readPackageInfo: true, - removePackages: true, - }, - packagePrivileges: { - endpoint: { - actions: { - readPolicyManagement: { - executePackageAction: true, - }, - readBlocklist: { - executePackageAction: true, - }, - }, - }, - }, - }, - }); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/security/fleet_router.ts b/x-pack/plugins/fleet/server/services/security/fleet_router.ts new file mode 100644 index 000000000000..1b1d84d3aca4 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/fleet_router.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + IKibanaResponse, + IRouter, + KibanaRequest, + KibanaResponseFactory, + Logger, + RequestHandler, + RouteMethod, +} from '@kbn/core/server'; + +import type { FleetRequestHandlerContext } from '../..'; + +import type { FleetAuthzRouteConfig, FleetAuthzRouter } from './types'; +import { + checkSecurityEnabled, + getAuthzFromRequest, + doesNotHaveRequiredFleetAuthz, +} from './security'; + +export function makeRouterWithFleetAuthz( + router: IRouter, + logger: Logger +): FleetAuthzRouter { + const routerAuthzWrapper = async ({ + context, + request, + response, + handler, + hasRequiredAuthz, + }: { + context: TContext; + request: KibanaRequest; + response: KibanaResponseFactory; + handler: RequestHandler; + hasRequiredAuthz?: FleetAuthzRouteConfig['fleetAuthz']; + }): Promise> => { + if (!checkSecurityEnabled()) { + const securityEnabledInfo = 'Kibana security must be enabled to use Fleet'; + logger.info(securityEnabledInfo); + return response.forbidden({ + body: { + message: securityEnabledInfo, + }, + }); + } + + const requestedAuthz = await getAuthzFromRequest(request); + + if (doesNotHaveRequiredFleetAuthz(requestedAuthz, hasRequiredAuthz)) { + logger.info(`User does not have required fleet authz to access path: ${request.route.path}`); + return response.forbidden(); + } + return handler(context, request, response); + }; + + const fleetAuthzRouter: FleetAuthzRouter = { + get: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => { + router.get(options, async (context, request, response) => + routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz }) + ); + }, + delete: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => { + router.delete(options, async (context, request, response) => + routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz }) + ); + }, + post: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => { + router.post(options, async (context, request, response) => + routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz }) + ); + }, + put: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => { + router.put(options, async (context, request, response) => + routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz }) + ); + }, + patch: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => { + router.patch(options, async (context, request, response) => + routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz }) + ); + }, + handleLegacyErrors: (handler) => router.handleLegacyErrors(handler), + getRoutes: () => router.getRoutes(), + routerPath: router.routerPath, + }; + + return fleetAuthzRouter; +} diff --git a/x-pack/plugins/fleet/server/services/security/index.ts b/x-pack/plugins/fleet/server/services/security/index.ts new file mode 100644 index 000000000000..c41c769c58d8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './types'; +export { makeRouterWithFleetAuthz } from './fleet_router'; +export { getRouteRequiredAuthz } from './route_required_authz'; +export { + checkSecurityEnabled, + checkSuperuser, + calculateRouteAuthz, + getAuthzFromRequest, + doesNotHaveRequiredFleetAuthz, +} from './security'; diff --git a/x-pack/plugins/fleet/server/services/security/route_required_authz.ts b/x-pack/plugins/fleet/server/services/security/route_required_authz.ts new file mode 100644 index 000000000000..db0ea31eff7a --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/route_required_authz.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deepFreeze } from '@kbn/std'; + +import type { RouteMethod } from '@kbn/core-http-server'; + +import { PACKAGE_POLICY_API_ROUTES } from '../../../common'; + +import type { FleetRouteRequiredAuthz } from './types'; + +/** + * The authorization requirements needed for an API route. Route authorization requirements are + * defined either via an `all` object, where all values must be `true` in order for access to be granted, + * or, by an `any` object, where any value defined that is set to `true` will grant access to the API. + * + * The `all` conditions are checked first and if those evaluate to `false`, then `any` conditions are evaluated. + */ +const ROUTE_AUTHZ_REQUIREMENTS = deepFreeze>({ + // Package Policy Update API + [`put:${PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN}`]: { + any: { + integrations: { writeIntegrationPolicies: true }, + packagePrivileges: { + endpoint: { + actions: { + writePolicyManagement: { + executePackageAction: true, + }, + }, + }, + }, + }, + }, + + // Package Policy GET one API + [`get:${PACKAGE_POLICY_API_ROUTES.INFO_PATTERN}`]: { + any: { + integrations: { + readIntegrationPolicies: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readTrustedApplications: { + executePackageAction: true, + }, + readEventFilters: { + executePackageAction: true, + }, + readHostIsolationExceptions: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + }, + + // Package Policy Bulk GET API + [`post:${PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN}`]: { + any: { + integrations: { + readIntegrationPolicies: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readTrustedApplications: { + executePackageAction: true, + }, + readEventFilters: { + executePackageAction: true, + }, + readHostIsolationExceptions: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + }, + + // Package Policy List API + [`get:${PACKAGE_POLICY_API_ROUTES.LIST_PATTERN}`]: { + any: { + integrations: { + readIntegrationPolicies: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readTrustedApplications: { + executePackageAction: true, + }, + readEventFilters: { + executePackageAction: true, + }, + readHostIsolationExceptions: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + }, +}); + +/** + * Retrieves the required fleet route authz + * in order to grant access to the given api route + * @param routeMethod + * @param routePath + */ +export const getRouteRequiredAuthz = ( + routeMethod: RouteMethod, + routePath: string +): FleetRouteRequiredAuthz | undefined => { + const key = `${routeMethod}:${routePath}`; + + if (typeof ROUTE_AUTHZ_REQUIREMENTS[key] !== 'undefined') { + return ROUTE_AUTHZ_REQUIREMENTS[key]; + } + + for (const k of Object.keys(ROUTE_AUTHZ_REQUIREMENTS)) { + if (pathMatchesPattern(k, key)) { + return ROUTE_AUTHZ_REQUIREMENTS[k]; + } + } +}; + +const pathMatchesPattern = (pathPattern: string, path: string): boolean => { + // No path params - pattern is single path + if (pathPattern === path) { + return true; + } + + // If pathPattern has params (`{value}`), then see if `path` matches it + if (/{.*?}/.test(pathPattern)) { + const pathParts = path.split(/\//); + const patternParts = pathPattern.split(/\//); + + if (pathParts.length !== patternParts.length) { + return false; + } + + return pathParts.every((part, index) => { + return part === patternParts[index] || /{.*?}/.test(patternParts[index]); + }); + } + + return false; +}; diff --git a/x-pack/plugins/fleet/server/services/security/security.test.ts b/x-pack/plugins/fleet/server/services/security/security.test.ts new file mode 100644 index 000000000000..f99a708504d6 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/security.test.ts @@ -0,0 +1,540 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { deepFreeze } from '@kbn/std'; + +import type { FleetAuthz } from '../../../common'; + +import { calculateRouteAuthz } from './security'; + +describe('When using calculateRouteAuthz()', () => { + const fleetAuthz = deepFreeze({ + fleet: { + all: false, + setup: false, + readEnrollmentTokens: false, + readAgentPolicies: false, + }, + integrations: { + readPackageInfo: false, + readInstalledPackages: false, + installPackages: false, + upgradePackages: false, + removePackages: false, + uploadPackages: false, + readPackageSettings: false, + writePackageSettings: false, + readIntegrationPolicies: false, + writeIntegrationPolicies: false, + }, + packagePrivileges: { + endpoint: { + actions: { + writeEndpointList: { + executePackageAction: false, + }, + readEndpointList: { + executePackageAction: false, + }, + writeTrustedApplications: { + executePackageAction: false, + }, + readTrustedApplications: { + executePackageAction: false, + }, + writeHostIsolationExceptions: { + executePackageAction: false, + }, + readHostIsolationExceptions: { + executePackageAction: false, + }, + writeBlocklist: { + executePackageAction: false, + }, + readBlocklist: { + executePackageAction: false, + }, + writeEventFilters: { + executePackageAction: false, + }, + readEventFilters: { + executePackageAction: false, + }, + writePolicyManagement: { + executePackageAction: false, + }, + readPolicyManagement: { + executePackageAction: false, + }, + writeActionsLogManagement: { + executePackageAction: false, + }, + readActionsLogManagement: { + executePackageAction: false, + }, + writeHostIsolation: { + executePackageAction: false, + }, + writeProcessOperations: { + executePackageAction: false, + }, + writeFileOperations: { + executePackageAction: false, + }, + }, + }, + + someOtherPackage: { + actions: { + readSomeThing: { + executePackageAction: false, + }, + }, + }, + }, + }); + + const getFleetAuthzMock = (authz: FleetAuthz = fleetAuthz) => authz; + + describe('with ANY object defined', () => { + it('should grant access if `any` are true', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + any: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ + granted: true, + grantedByFleetPrivileges: false, + scopeDataToPackages: ['endpoint'], + }); + }); + + it('should deny access if `any` are false', () => { + expect( + calculateRouteAuthz(getFleetAuthzMock(), { + any: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + }) + ).toEqual({ + granted: false, + grantedByFleetPrivileges: false, + scopeDataToPackages: undefined, + }); + }); + }); + + describe('with ALL object defined', () => { + it('should grant access if `all` are true', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + integrations: { + ...fleetAuthz.integrations, + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined }); + }); + + it('should deny access if not `all` are true', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ + granted: false, + grantedByFleetPrivileges: false, + scopeDataToPackages: undefined, + }); + }); + }); + + describe('with ALL and ANY', () => { + it('should grant access if `all` are true', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + integrations: { + ...fleetAuthz.integrations, + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined }); + }); + + it('should grant access if all OR any are true', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + integrations: { + ...fleetAuthz.integrations, + readPackageInfo: true, + removePackages: true, + }, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + }, + any: { + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined }); + }); + + it('should grant access if `all` are not true but `any` are true ', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + integrations: { + ...fleetAuthz.integrations, + readPackageInfo: true, + }, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + }, + }, + + someOtherPackage: { + actions: { + readSomeThing: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + }, + any: { + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + someOtherPackage: { + actions: { + readSomeThing: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ + granted: true, + grantedByFleetPrivileges: false, + scopeDataToPackages: ['endpoint', 'someOtherPackage'], + }); + }); + + it('should grant access if `all` are true but `any` are not true ', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + integrations: { + ...fleetAuthz.integrations, + readPackageInfo: true, + removePackages: true, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + }, + any: { + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined }); + }); + }); + + describe('and access is granted based on package privileges', () => { + it('should exclude package names for which there is no access allowed', () => { + expect( + calculateRouteAuthz( + getFleetAuthzMock({ + ...fleetAuthz, + packagePrivileges: { + ...fleetAuthz.packagePrivileges, + endpoint: { + ...fleetAuthz.packagePrivileges.endpoint, + actions: { + ...fleetAuthz.packagePrivileges.endpoint.actions, + readPolicyManagement: { + executePackageAction: true, + }, + }, + }, + }, + }), + { + all: { + integrations: { + readPackageInfo: true, + removePackages: true, + }, + }, + any: { + packagePrivileges: { + endpoint: { + actions: { + readPolicyManagement: { + executePackageAction: true, + }, + readBlocklist: { + executePackageAction: true, + }, + }, + }, + // This package Authz is not allowed, so it should not be listed in `scopeDataToPackages` + someOtherPackage: { + actions: { + readSomeThing: { + executePackageAction: true, + }, + }, + }, + }, + }, + } + ) + ).toEqual({ + granted: true, + grantedByFleetPrivileges: false, + scopeDataToPackages: ['endpoint'], + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/security/security.ts b/x-pack/plugins/fleet/server/services/security/security.ts new file mode 100644 index 000000000000..c5dabecc8090 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/security.ts @@ -0,0 +1,252 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; + +import type { KibanaRequest } from '@kbn/core/server'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; + +import type { FleetAuthz } from '../../../common'; +import { INTEGRATIONS_PLUGIN_ID } from '../../../common'; +import { + calculateAuthz, + calculatePackagePrivilegesFromKibanaPrivileges, +} from '../../../common/authz'; + +import { appContextService } from '..'; +import { ENDPOINT_PRIVILEGES, PLUGIN_ID } from '../../constants'; + +import type { + FleetAuthzRequirements, + FleetRouteRequiredAuthz, + FleetAuthzRouteConfig, +} from './types'; + +export function checkSecurityEnabled() { + return appContextService.getSecurityLicense().isEnabled(); +} + +export function checkSuperuser(req: KibanaRequest) { + if (!checkSecurityEnabled()) { + return false; + } + + const security = appContextService.getSecurity(); + const user = security.authc.getCurrentUser(req); + if (!user) { + return false; + } + + const userRoles = user.roles || []; + if (!userRoles.includes('superuser')) { + return false; + } + + return true; +} + +function getAuthorizationFromPrivileges( + kibanaPrivileges: Array<{ + resource?: string; + privilege: string; + authorized: boolean; + }>, + searchPrivilege: string +) { + const privilege = kibanaPrivileges.find((p) => p.privilege.includes(searchPrivilege)); + return privilege ? privilege.authorized : false; +} + +export async function getAuthzFromRequest(req: KibanaRequest): Promise { + const security = appContextService.getSecurity(); + + if (security.authz.mode.useRbacForRequest(req)) { + const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req); + const endpointPrivileges = ENDPOINT_PRIVILEGES.map((privilege) => + security.authz.actions.api.get(`${DEFAULT_APP_CATEGORIES.security.id}-${privilege}`) + ); + const { privileges } = await checkPrivileges({ + kibana: [ + security.authz.actions.api.get(`${PLUGIN_ID}-all`), + security.authz.actions.api.get(`${PLUGIN_ID}-setup`), + security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-all`), + security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-read`), + ...endpointPrivileges, + ], + }); + const fleetAllAuth = getAuthorizationFromPrivileges(privileges.kibana, `${PLUGIN_ID}-all`); + const intAllAuth = getAuthorizationFromPrivileges( + privileges.kibana, + `${INTEGRATIONS_PLUGIN_ID}-all` + ); + const intReadAuth = getAuthorizationFromPrivileges( + privileges.kibana, + `${INTEGRATIONS_PLUGIN_ID}-read` + ); + const fleetSetupAuth = getAuthorizationFromPrivileges(privileges.kibana, 'fleet-setup'); + + return { + ...calculateAuthz({ + fleet: { all: fleetAllAuth, setup: fleetSetupAuth }, + integrations: { + all: intAllAuth, + read: intReadAuth, + }, + isSuperuser: checkSuperuser(req), + }), + packagePrivileges: calculatePackagePrivilegesFromKibanaPrivileges(privileges.kibana), + }; + } + + return calculateAuthz({ + fleet: { all: false, setup: false }, + integrations: { + all: false, + read: false, + }, + isSuperuser: false, + }); +} + +interface RouteAuthz { + /** Is route access granted (based on authz) */ + granted: boolean; + + /** Was authorization to the api a result of Fleet (and Integrations) Privileges (as oposed to Package privileges) */ + grantedByFleetPrivileges: boolean; + + /** + * Set when `grantedByFleetPrivileges` is `false` and `granted` is true, which indicate access was granted + * via a Package Privileges. Array will hold the list of Package names that are allowed + */ + scopeDataToPackages: string[] | undefined; +} + +/** + * Calculates Authz information for a Route, including: + * 1. Is access granted + * 2. was access granted based on Fleet and/or Integration privileges, and + * 3. a list of package names for which access was granted (only set if access was granted by package privileges) + * + * @param fleetAuthz + * @param requiredAuthz + */ +export const calculateRouteAuthz = ( + fleetAuthz: FleetAuthz, + requiredAuthz: FleetRouteRequiredAuthz | undefined +): RouteAuthz => { + const response: RouteAuthz = { + granted: false, + grantedByFleetPrivileges: false, + scopeDataToPackages: undefined, + }; + const fleetAuthzFlatten = flatten(fleetAuthz); + + const isPrivilegeGranted = (flattenPrivilegeKey: string): boolean => + fleetAuthzFlatten[flattenPrivilegeKey] === true; + + if (typeof requiredAuthz === 'undefined') { + return response; + } + + if (requiredAuthz.all) { + response.granted = Object.keys(flatten(requiredAuthz.all)).every(isPrivilegeGranted); + + if (response.granted) { + if (requiredAuthz.all.fleet || requiredAuthz.all.integrations) { + response.grantedByFleetPrivileges = true; + } + + return response; + } + } + + if (requiredAuthz.any) { + response.granted = Object.keys(flatten(requiredAuthz.any)).some(isPrivilegeGranted); + + if (response.granted) { + // Figure out if authz was granted via Fleet privileges + if (requiredAuthz.any.fleet || requiredAuthz.any.integrations) { + const fleetAnyPrivileges = pick(requiredAuthz.any, ['fleet', 'integrations']); + + response.grantedByFleetPrivileges = Object.keys(flatten(fleetAnyPrivileges)).some( + isPrivilegeGranted + ); + } + + // If access was NOT granted via Fleet Authz, then retrieve a list of Package names that were + // granted access to their respective data. + if (!response.grantedByFleetPrivileges && requiredAuthz.any.packagePrivileges) { + for (const [packageName, packageRequiredAuthz] of Object.entries( + requiredAuthz.any.packagePrivileges + )) { + const packageRequiredAuthzKeys = Object.keys( + flatten({ packagePrivileges: { [packageName]: packageRequiredAuthz } }) + ); + + if (packageRequiredAuthzKeys.some(isPrivilegeGranted)) { + if (!response.scopeDataToPackages) { + response.scopeDataToPackages = []; + } + + response.scopeDataToPackages.push(packageName); + } + } + } + + return response; + } + } + + return response; +}; + +/** + * Utility to flatten an object's key all the way down to the last value. + * @param source + */ +function flatten(source: FleetAuthzRequirements | FleetAuthz): Record { + const response: Record = {}; + const processKeys = (prefix: string, value: unknown) => { + if (typeof value === 'object' && value !== null) { + const objectKeys = Object.keys(value); + + for (const key of objectKeys) { + processKeys(`${prefix}${prefix ? '.' : ''}${key}`, (value as Record)[key]); + } + } else if (Array.isArray(value)) { + value.forEach((subValue, key) => { + processKeys(`${prefix}${prefix ? '.' : ''}${key}`, subValue); + }); + } else { + response[prefix] = value as boolean; + } + }; + + processKeys('', source); + + return response; +} + +/** + * Utility to determine if a user has the required Fleet Authz based on user privileges + * and route required authz structure. + * @param authz + * @param fleetRequiredAuthz + * @returns boolean + */ +export const doesNotHaveRequiredFleetAuthz = ( + authz: FleetAuthz, + fleetRequiredAuthz: FleetAuthzRouteConfig['fleetAuthz'] +): boolean => { + return ( + !!fleetRequiredAuthz && + ((typeof fleetRequiredAuthz === 'function' && !fleetRequiredAuthz(authz)) || + (typeof fleetRequiredAuthz !== 'function' && + !calculateRouteAuthz(authz, { all: fleetRequiredAuthz }).granted)) + ); +}; diff --git a/x-pack/plugins/fleet/server/services/security/types.ts b/x-pack/plugins/fleet/server/services/security/types.ts new file mode 100644 index 000000000000..8559ee57b35e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/security/types.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RouteConfig, RouteMethod } from '@kbn/core-http-server'; +import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; +import type { IRouter, RequestHandler } from '@kbn/core/server'; + +import type { FleetRequestHandlerContext } from '../..'; + +import type { FleetAuthz } from '../../../common'; + +/** The values allowed for the `fleetAuthz` property of the Fleet Router registration interface. */ +type FleetAuthzRouterConfigParam = FleetAuthzRequirements | ((userAuthz: FleetAuthz) => boolean); + +type FleetAuthzRouteRegistrar< + Method extends RouteMethod, + Context extends RequestHandlerContext = RequestHandlerContext +> = ( + route: FleetRouteConfig, + handler: RequestHandler +) => void; + +export interface FleetAuthzRouteConfig< + T extends FleetAuthzRouterConfigParam = FleetAuthzRouterConfigParam +> { + fleetAuthz?: T; +} + +export type FleetRouteConfig = RouteConfig & + FleetAuthzRouteConfig; + +// Fleet router that allow to add required access when registering route +export interface FleetAuthzRouter< + TContext extends FleetRequestHandlerContext = FleetRequestHandlerContext +> extends IRouter { + get: FleetAuthzRouteRegistrar<'get', TContext>; + delete: FleetAuthzRouteRegistrar<'delete', TContext>; + post: FleetAuthzRouteRegistrar<'post', TContext>; + put: FleetAuthzRouteRegistrar<'put', TContext>; + patch: FleetAuthzRouteRegistrar<'patch', TContext>; +} + +type DeepPartialTruthy = { + [P in keyof T]?: T[P] extends boolean ? true : DeepPartialTruthy; +}; + +/** + * The set of authz properties required to be granted access to an API route + */ +export type FleetAuthzRequirements = DeepPartialTruthy; + +/** + * Interface used for registering and calculating authorization for a Fleet API routes + */ +export type FleetRouteRequiredAuthz = Partial<{ + any: FleetAuthzRequirements; + all: FleetAuthzRequirements; +}>; diff --git a/x-pack/plugins/fleet/server/types/request_context.ts b/x-pack/plugins/fleet/server/types/request_context.ts index f803d1c0cd6b..bc0e1c8886bf 100644 --- a/x-pack/plugins/fleet/server/types/request_context.ts +++ b/x-pack/plugins/fleet/server/types/request_context.ts @@ -32,14 +32,18 @@ export type FleetRequestHandlerContext = CustomRequestHandlerContext<{ asCurrentUser: PackagePolicyClient; asInternalUser: PackagePolicyClient; }; - epm: { - /** - * Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be - * used by routes that have additional privilege checks for authorization (such as requiring superuser). - */ - readonly internalSoClient: SavedObjectsClientContract; - }; + /** + * Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be + * used by routes that have additional privilege checks for authorization (such as requiring superuser). + */ + readonly internalSoClient: SavedObjectsClientContract; + spaceId: string; + /** + * If data is to be limited to the list of integration package names. This will be set when + * authz to the API was granted only based on Package Privileges. + */ + limitedToPackages: string[] | undefined; }; }>; diff --git a/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts index fe875fe4e256..ad9c1eec519c 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts @@ -51,6 +51,7 @@ export default function (providerContext: FtrProviderContext) { .post(`/api/fleet/epm/packages/_bulk`) .auth(testUsers.fleet_all_int_read.username, testUsers.fleet_all_int_read.password) .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'overrides'] }) .expect(403); }); it('should return 403 if user without fleet access requests upgrade', async function () { @@ -58,6 +59,7 @@ export default function (providerContext: FtrProviderContext) { .post(`/api/fleet/epm/packages/_bulk`) .auth(testUsers.integr_all_only.username, testUsers.integr_all_only.password) .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'overrides'] }) .expect(403); }); it('should return 200 and an array for upgrading a package', async function () {