;
-
-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 f11244d7b59ff..881541b569805 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 0ecbca40132cb..f4b35508e52c8 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 cf2ff46cd1110..78daddf837ac1 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 8b2aa2bf8f573..b4470e648dcab 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 be4ca37260b8a..fe43fa50b4317 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 576505e46bd2d..8f732809adf5f 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 44d35f3e4c33c..779f0dad02c8c 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 f3d82f13d96ee..dfc02c4f68c57 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 7acd555380a68..a95cba87b39be 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 15e46529c8697..1f2b6c3fab22b 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 0000000000000..1b1d84d3aca40
--- /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 0000000000000..c41c769c58d8d
--- /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 0000000000000..db0ea31eff7ae
--- /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 0000000000000..f99a708504d6c
--- /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 0000000000000..c5dabecc8090e
--- /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 0000000000000..8559ee57b35e8
--- /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 f803d1c0cd6b5..bc0e1c8886bff 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 fe875fe4e2565..ad9c1eec519c4 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 () {
From 2ad6b60a4e35c5ef40c4869fcc5348573d52487a Mon Sep 17 00:00:00 2001
From: gchaps <33642766+gchaps@users.noreply.github.com>
Date: Wed, 14 Dec 2022 07:36:12 -0800
Subject: [PATCH 02/37] [DOCS] Updates what's new pages (#147483)
## Summary
This PR updates the links in the What's New page and landing page.
---
docs/index-custom-title-page.html | 2 --
docs/user/whats-new.asciidoc | 2 +-
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/docs/index-custom-title-page.html b/docs/index-custom-title-page.html
index 7af50716913b4..baaa155a8913b 100644
--- a/docs/index-custom-title-page.html
+++ b/docs/index-custom-title-page.html
@@ -63,8 +63,6 @@ Bring your data to life
- What's new
- Release notes
How-to videos
diff --git a/docs/user/whats-new.asciidoc b/docs/user/whats-new.asciidoc
index 399de14d5f18c..e9d1faf845c6f 100644
--- a/docs/user/whats-new.asciidoc
+++ b/docs/user/whats-new.asciidoc
@@ -5,7 +5,7 @@ Here are the highlights of what's new and improved in {minor-version}.
For detailed information about this release,
check the <>.
-Previous versions: {kibana-ref-all}/8.4/whats-new.html[8.4] | {kibana-ref-all}/8.3/whats-new.html[8.3] | {kibana-ref-all}/8.2/whats-new.html[8.2]
+Previous versions: {kibana-ref-all}/8.6/whats-new.html[8.6] | {kibana-ref-all}/8.5/whats-new.html[8.5] | {kibana-ref-all}/8.4/whats-new.html[8.4] | {kibana-ref-all}/8.3/whats-new.html[8.3] | {kibana-ref-all}/8.2/whats-new.html[8.2]
| {kibana-ref-all}/8.1/whats-new.html[8.1] | {kibana-ref-all}/8.0/whats-new.html[8.0]
//NOTE: The notable-highlights tagged regions are re-used in the
From 39d8c7eaccad8f4ecc7486655092da4f92d88416 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?=
Date: Wed, 14 Dec 2022 16:57:17 +0100
Subject: [PATCH 03/37] [Synthetics UI] Add missing configuration options to
the add/edit monitor forms (#147265)
## Summary
Part of #147182.
## Implemented
- Parameters (this is not under advanced, but the main configuration, in
Uptime)
- Ignore HTTPS errors (under advanced)
- Synthetics args (under advanced)
## Not implemented
- Throttling. This is pending clarification from @drewpost
---
.../monitor_add_edit/fields/code_editor.tsx | 13 ++-
.../monitor_add_edit/form/field_config.tsx | 80 +++++++++++++++++++
.../monitor_add_edit/form/form_config.tsx | 8 +-
.../monitor_add_edit/form/validation.tsx | 2 +
4 files changed, 99 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx
index e5d8c8152d68f..d4d82f2efdbdd 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/code_editor.tsx
@@ -25,9 +25,18 @@ interface Props {
onChange: (value: string) => void;
value: string;
placeholder?: string;
+ height?: string;
}
-export const CodeEditor = ({ ariaLabel, id, languageId, onChange, value, placeholder }: Props) => {
+export const CodeEditor = ({
+ ariaLabel,
+ id,
+ languageId,
+ onChange,
+ value,
+ placeholder,
+ height = '250px',
+}: Props) => {
return (
= {
defaultMessage: 'Monitor script is required',
}),
},
+ [ConfigKey.PARAMS]: {
+ fieldKey: ConfigKey.PARAMS,
+ label: i18n.translate('xpack.synthetics.monitorConfig.params.label', {
+ defaultMessage: 'Parameters',
+ }),
+ component: JSONEditor,
+ props: ({ setValue }) => ({
+ id: 'syntheticsMonitorConfigParams',
+ height: '100px',
+ onChange: (json: string) => {
+ setValue(ConfigKey.PARAMS, json);
+ },
+ }),
+ error: i18n.translate('xpack.synthetics.monitorConfig.params.error', {
+ defaultMessage: 'Invalid JSON format',
+ }),
+ helpText: (
+ params.value,
+ }}
+ />
+ ),
+ validation: () => ({
+ validate: (value) => {
+ const validateFn = validate[DataStream.BROWSER][ConfigKey.PARAMS];
+ if (validateFn) {
+ return !validateFn({
+ [ConfigKey.PARAMS]: value,
+ });
+ }
+ },
+ }),
+ },
isTLSEnabled: {
fieldKey: 'isTLSEnabled',
component: EuiSwitch,
@@ -1058,4 +1094,48 @@ export const FIELD: Record = {
},
}),
},
+ [ConfigKey.IGNORE_HTTPS_ERRORS]: {
+ fieldKey: ConfigKey.IGNORE_HTTPS_ERRORS,
+ component: EuiSwitch,
+ controlled: true,
+ helpText: (
+
+ {i18n.translate('xpack.synthetics.monitorConfig.ignoreHttpsErrors.helpText', {
+ defaultMessage:
+ 'Turns off TLS/SSL validation in the synthetics browser. This is useful for testing sites that use self-signed certificates.',
+ })}
+
+ ),
+ props: ({ setValue }) => ({
+ id: 'syntheticsMontiorConfigIgnoreHttpsErrors',
+ label: i18n.translate('xpack.synthetics.monitorConfig.ignoreHttpsErrors.label', {
+ defaultMessage: 'Ignore HTTPS errors',
+ }),
+ onChange: (event: React.ChangeEvent) => {
+ setValue(ConfigKey.IGNORE_HTTPS_ERRORS, !!event.target.checked);
+ },
+ }),
+ },
+ [ConfigKey.SYNTHETICS_ARGS]: {
+ fieldKey: ConfigKey.SYNTHETICS_ARGS,
+ component: EuiFieldText,
+ controlled: true,
+ label: i18n.translate('xpack.synthetics.monitorConfig.syntheticsArgs.label', {
+ defaultMessage: 'Synthetics args',
+ }),
+ helpText: (
+
+ {i18n.translate('xpack.synthetics.monitorConfig.syntheticsArgs.helpText', {
+ defaultMessage:
+ 'Extra arguments to pass to the synthetics agent package. Takes a list of strings. This is useful in rare scenarios, and should not ordinarily need to be set.',
+ })}
+
+ ),
+ props: ({ setValue }) => ({
+ id: 'syntheticsMontiorConfigSyntheticsArgs',
+ onChange: (event: React.ChangeEvent) => {
+ setValue(ConfigKey.SYNTHETICS_ARGS, event.target.value);
+ },
+ }),
+ },
};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx
index 132e3ab0343e1..5a4bad3e2f561 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx
@@ -114,7 +114,11 @@ export const BROWSER_ADVANCED = [
defaultMessage: 'Provide fine-tuned configuration for the synthetics agent.',
}
),
- components: [FIELD[`${ConfigKey.PLAYWRIGHT_OPTIONS}`]],
+ components: [
+ FIELD[ConfigKey.IGNORE_HTTPS_ERRORS],
+ FIELD[ConfigKey.SYNTHETICS_ARGS],
+ FIELD[ConfigKey.PLAYWRIGHT_OPTIONS],
+ ],
},
];
@@ -200,7 +204,7 @@ export const FORM_CONFIG: FieldConfig = {
FIELD[ConfigKey.THROTTLING_CONFIG],
FIELD[ConfigKey.ENABLED],
],
- step3: [FIELD[ConfigKey.SOURCE_INLINE]],
+ step3: [FIELD[ConfigKey.SOURCE_INLINE], FIELD[ConfigKey.PARAMS]],
scriptEdit: [FIELD[ConfigKey.SOURCE_INLINE]],
advanced: [
{
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx
index 00330134344c4..9f06395bf1a19 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/validation.tsx
@@ -162,6 +162,8 @@ const validateBrowser: ValidationLibrary = {
[ConfigKey.LATENCY]: ({ [ConfigKey.LATENCY]: latency }) => validateThrottleValue(latency, true),
[ConfigKey.PLAYWRIGHT_OPTIONS]: ({ [ConfigKey.PLAYWRIGHT_OPTIONS]: playwrightOptions }) =>
playwrightOptions ? !validJSONFormat(playwrightOptions) : false,
+ [ConfigKey.PARAMS]: ({ [ConfigKey.PARAMS]: params }) =>
+ params ? !validJSONFormat(params) : false,
};
export type ValidateDictionary = Record;
From f2fcb1c5afc19f8439dfff4c4137e1b2253478dc Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Wed, 14 Dec 2022 16:57:32 +0100
Subject: [PATCH 04/37] [Profiling] Remove link to 'Other' bucket (#147523)
Closes https://github.com/elastic/prodfiler/issues/2739
---
x-pack/plugins/profiling/public/components/subchart.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/x-pack/plugins/profiling/public/components/subchart.tsx b/x-pack/plugins/profiling/public/components/subchart.tsx
index 0dc017bbdf5f3..106f0f53ad8e8 100644
--- a/x-pack/plugins/profiling/public/components/subchart.tsx
+++ b/x-pack/plugins/profiling/public/components/subchart.tsx
@@ -187,6 +187,8 @@ export const SubChart: React.FC = ({
onShowMoreClick?.()}>
{label}
+ ) : category === OTHER_BUCKET_LABEL ? (
+ {label}
) : (
{label}
From 62b26e020c407ce392b621ff63c31c6bea525212 Mon Sep 17 00:00:00 2001
From: Lola
Date: Wed, 14 Dec 2022 11:31:59 -0500
Subject: [PATCH 05/37] [Kubernetes-Security-Dashboard] [8.7] Add k8 response
action button to kubernetes dashboard (#147217)
## Summary
Summarize your PR. If it involves visual changes include a screenshot or
gif.
This PR uses the `useResponderActionData` hook to get the
EndpointDetails metadata and trigger a click callback to show the
response console. When selecting a cluster level in the tree nav, we
will retrieve an agent and update a local state within the
`KubernetesContainer` component. When clicking on the response action
button, we call `handleResponseActionClick` handler which contains a
hook to show will show the response console.
### Checklist
Delete any items that are not applicable to this PR.
- [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
### For maintainers
- [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)
---
.../kubernetes_security/common/constants.ts | 2 +
.../kubernetes_security_routes/index.test.tsx | 8 +
.../kubernetes_security_routes/index.tsx | 6 +
.../__snapshots__/index.test.tsx.snap | 1594 ++++++++++-------
.../breadcrumb/index.test.tsx | 52 +-
.../tree_view_container/breadcrumb/index.tsx | 101 +-
.../tree_view_container/breadcrumb/styles.ts | 3 +
.../breadcrumb/translations.ts | 14 +
.../components/tree_view_container/hooks.tsx | 33 +-
.../components/tree_view_container/index.tsx | 53 +-
.../tree_view_container/tree_nav/types.ts | 4 +
.../kubernetes_security/public/types.ts | 8 +
.../server/routes/agent_id.ts | 49 +
.../server/routes/index.ts | 2 +
.../public/kubernetes/pages/index.tsx | 29 +-
15 files changed, 1233 insertions(+), 725 deletions(-)
create mode 100644 x-pack/plugins/kubernetes_security/public/components/tree_view_container/breadcrumb/translations.ts
create mode 100644 x-pack/plugins/kubernetes_security/server/routes/agent_id.ts
diff --git a/x-pack/plugins/kubernetes_security/common/constants.ts b/x-pack/plugins/kubernetes_security/common/constants.ts
index 3de3ec6be69e3..cde3413d7e136 100644
--- a/x-pack/plugins/kubernetes_security/common/constants.ts
+++ b/x-pack/plugins/kubernetes_security/common/constants.ts
@@ -12,6 +12,7 @@ export const LOCAL_STORAGE_HIDE_WIDGETS_KEY = 'kubernetesSecurity:shouldHideWidg
export const AGGREGATE_ROUTE = '/internal/kubernetes_security/aggregate';
export const COUNT_ROUTE = '/internal/kubernetes_security/count';
export const MULTI_TERMS_AGGREGATE_ROUTE = '/internal/kubernetes_security/multi_terms_aggregate';
+export const AGENT_ID_ROUTE = '/internal/kubernetes_security/agent_id';
export const AGGREGATE_PAGE_SIZE = 10;
// so, bucket sort can only page through what we request at the top level agg, which means there is a ceiling to how many aggs we can page through.
@@ -23,6 +24,7 @@ export const QUERY_KEY_PERCENT_WIDGET = 'kubernetesSecurityPercentWidget';
export const QUERY_KEY_COUNT_WIDGET = 'kubernetesSecurityCountWidget';
export const QUERY_KEY_CONTAINER_NAME_WIDGET = 'kubernetesSecurityContainerNameWidget';
export const QUERY_KEY_PROCESS_EVENTS = 'kubernetesSecurityProcessEvents';
+export const QUERY_KEY_AGENT_ID = 'kubernetesSecurityAgentId';
// ECS fields
export const ENTRY_LEADER_INTERACTIVE = 'process.entry_leader.interactive';
diff --git a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx
index ea9b8b11da891..7f939d4cf4164 100644
--- a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx
+++ b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx
@@ -53,6 +53,11 @@ const renderWithRouter = (
},
};
});
+ const responseActionButtonProps = {
+ tooltip: { content: 'test' },
+ isDisabled: false,
+ canAccessResponseConsole: true,
+ };
const mockedContext = createAppRootMockRenderer();
return mockedContext.render(
@@ -63,7 +68,10 @@ const renderWithRouter = (
startDate: '2022-03-08T18:52:15.532Z',
endDate: '2022-06-09T17:52:15.532Z',
}}
+ responseActionButtonProps={responseActionButtonProps}
+ responseActionClick={jest.fn()}
renderSessionsView={jest.fn()}
+ handleTreeNavSelection={jest.fn()}
/>
);
diff --git a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx
index 629d200eae348..d51e9609c62b2 100644
--- a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx
+++ b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx
@@ -60,6 +60,9 @@ const KubernetesSecurityRoutesComponent = ({
indexPattern,
globalFilter,
renderSessionsView,
+ responseActionClick,
+ handleTreeNavSelection,
+ responseActionButtonProps,
}: KubernetesSecurityDeps) => {
const [shouldHideCharts, setShouldHideCharts] = useLocalStorage(
LOCAL_STORAGE_HIDE_WIDGETS_KEY,
@@ -296,6 +299,9 @@ const KubernetesSecurityRoutesComponent = ({
globalFilter={globalFilterForKubernetes}
renderSessionsView={renderSessionsView}
indexPattern={indexPattern}
+ responseActionButtonProps={responseActionButtonProps}
+ responseActionClick={responseActionClick}
+ handleTreeNavSelection={handleTreeNavSelection}
/>
diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/breadcrumb/__snapshots__/index.test.tsx.snap b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/breadcrumb/__snapshots__/index.test.tsx.snap
index 6c29585bbaad6..683ff4c14cbea 100644
--- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/breadcrumb/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/breadcrumb/__snapshots__/index.test.tsx.snap
@@ -8,104 +8,135 @@ Object {
-
-
-
-
-
-
-
+ class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
+ >
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+ selected image
+
+
+
+
+
+
-
- selected image
-
+
+
+ Respond
+
+
+
-
-
+
+