;
-
-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 () {