diff --git a/x-pack/plugins/fleet/common/authz.ts b/x-pack/plugins/fleet/common/authz.ts new file mode 100644 index 000000000000..2a9205ada0e0 --- /dev/null +++ b/x-pack/plugins/fleet/common/authz.ts @@ -0,0 +1,64 @@ +/* + * 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 interface FleetAuthz { + fleet: { + all: boolean; + setup: boolean; + readEnrollmentTokens: boolean; + }; + + integrations: { + readPackageInfo: boolean; + readInstalledPackages: boolean; + installPackages: boolean; + upgradePackages: boolean; + removePackages: boolean; + + readPackageSettings: boolean; + writePackageSettings: boolean; + + readIntegrationPolicies: boolean; + writeIntegrationPolicies: boolean; + }; +} + +interface CalculateParams { + fleet: { + all: boolean; + setup: boolean; + }; + + integrations: { + all: boolean; + read: boolean; + }; +} + +export const calculateAuthz = ({ fleet, integrations }: CalculateParams): FleetAuthz => ({ + fleet: { + all: fleet.all && (integrations.all || integrations.read), + + // These are currently used by Fleet Server setup + setup: fleet.all || fleet.setup, + readEnrollmentTokens: fleet.all || fleet.setup, + }, + + integrations: { + readPackageInfo: fleet.all || fleet.setup || integrations.all || integrations.read, + readInstalledPackages: integrations.all || integrations.read, + installPackages: fleet.all && integrations.all, + upgradePackages: fleet.all && integrations.all, + removePackages: fleet.all && integrations.all, + + readPackageSettings: fleet.all && integrations.all, + writePackageSettings: fleet.all && integrations.all, + + readIntegrationPolicies: fleet.all && integrations.all, + writeIntegrationPolicies: fleet.all && integrations.all, + }, +}); diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 029460c1750c..611e15032385 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -11,3 +11,5 @@ export * from './constants'; export * from './services'; export * from './types'; +export type { FleetAuthz } from './authz'; +export { calculateAuthz } from './authz'; diff --git a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx index 675eaaa1905e..367f5f488a65 100644 --- a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx +++ b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx @@ -14,6 +14,8 @@ import type { IStorage } from '../../../../../src/plugins/kibana_utils/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { setHttpClient } from '../hooks/use_request'; +import type { FleetAuthz } from '../../common'; + import { createStartDepsMock } from './plugin_dependencies'; import type { MockedFleetStartServices } from './types'; @@ -28,6 +30,25 @@ const createMockStore = (): MockedKeys => { }; }; +const fleetAuthzMock: FleetAuthz = { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, +}; + const configureStartServices = (services: MockedFleetStartServices): void => { // Store the http for use by useRequest setHttpClient(services.http); @@ -52,6 +73,7 @@ export const createStartServices = (basePath: string = '/mock'): MockedFleetStar ...coreMock.createStart({ basePath }), ...createStartDepsMock(), storage: new Storage(createMockStore()) as jest.Mocked, + authz: fleetAuthzMock, }; configureStartServices(startServices); diff --git a/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts b/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts index 134bd408d8c3..054ef958c191 100644 --- a/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts +++ b/x-pack/plugins/fleet/public/mock/plugin_interfaces.ts @@ -14,5 +14,23 @@ export const createStartMock = (extensionsStorage: UIExtensionsStorage = {}): Mo return { isInitialized: jest.fn().mockResolvedValue(true), registerExtension: createExtensionRegistrationCallback(extensionsStorage), + authz: { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, + }, }; }; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 039c1da9b934..e188b8e99b5b 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -38,8 +38,14 @@ import { Storage } from '../../../../src/plugins/kibana_utils/public'; import type { LicensingPluginSetup } from '../../licensing/public'; import type { CloudSetup } from '../../cloud/public'; import type { GlobalSearchPluginSetup } from '../../global_search/public'; -import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, setupRouteService, appRoutesService } from '../common'; -import type { CheckPermissionsResponse, PostFleetSetupResponse } from '../common'; +import { + PLUGIN_ID, + INTEGRATIONS_PLUGIN_ID, + setupRouteService, + appRoutesService, + calculateAuthz, +} from '../common'; +import type { CheckPermissionsResponse, PostFleetSetupResponse, FleetAuthz } from '../common'; import type { FleetConfigType } from '../common/types'; @@ -65,6 +71,8 @@ export interface FleetSetup {} * Describes public Fleet plugin contract returned at the `start` stage. */ export interface FleetStart { + /** Authorization for the current user */ + authz: FleetAuthz; registerExtension: UIExtensionRegistrationCallback; isInitialized: () => Promise; } @@ -90,6 +98,7 @@ export interface FleetStartServices extends CoreStart, FleetStartDeps { storage: Storage; share: SharePluginStart; cloud?: CloudSetup; + authz: FleetAuthz; } export class FleetPlugin implements Plugin { @@ -103,7 +112,7 @@ export class FleetPlugin implements Plugin, deps: FleetSetupDeps) { const config = this.config; const kibanaVersion = this.kibanaVersion; const extensions = this.extensions; @@ -129,16 +138,13 @@ export class FleetPlugin implements Plugin { - const [coreStartServices, startDepsServices] = (await core.getStartServices()) as [ - CoreStart, - FleetStartDeps, - FleetStart - ]; + const [coreStartServices, startDepsServices, fleetStart] = await core.getStartServices(); const startServices: FleetStartServices = { ...coreStartServices, ...startDepsServices, storage: this.storage, cloud: deps.cloud, + authz: fleetStart.authz, }; const { renderApp, teardownIntegrations } = await import('./applications/integrations'); @@ -169,16 +175,13 @@ export class FleetPlugin implements Plugin { - const [coreStartServices, startDepsServices] = (await core.getStartServices()) as [ - CoreStart, - FleetStartDeps, - FleetStart - ]; + const [coreStartServices, startDepsServices, fleetStart] = await core.getStartServices(); const startServices: FleetStartServices = { ...coreStartServices, ...startDepsServices, storage: this.storage, cloud: deps.cloud, + authz: fleetStart.authz, }; const { renderApp, teardownFleet } = await import('./applications/fleet'); const unmount = renderApp(startServices, params, config, kibanaVersion, extensions); @@ -243,7 +246,23 @@ export class FleetPlugin implements Plugin { if (!successPromise) { successPromise = Promise.resolve().then(async () => { diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 943f100c94f7..bd7f192dc7fd 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -20,6 +20,7 @@ import type { PackagePolicyServiceInterface } from '../services/package_policy'; import type { AgentPolicyServiceInterface, AgentService } from '../services'; import type { FleetAppContext } from '../plugin'; import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; +import type { FleetAuthz } from '../../common'; // Export all mocks from artifacts export * from '../services/artifacts/mocks'; @@ -120,3 +121,25 @@ export const createMockAgentService = (): jest.Mocked => { listAgents: jest.fn(), }; }; + +/** + * Creates mock `authz` object + */ +export const fleetAuthzMock: FleetAuthz = { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, +}; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index f7593e32c25c..3ee83a91e0df 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -15,6 +15,7 @@ import type { PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, + KibanaRequest, } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -29,7 +30,7 @@ import type { } from '../../encrypted_saved_objects/server'; import type { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import type { FleetConfigType } from '../common'; +import type { FleetConfigType, FleetAuthz } from '../common'; import { INTEGRATIONS_PLUGIN_ID } from '../common'; import type { CloudSetup } from '../../cloud/server'; @@ -79,7 +80,7 @@ import { } from './services/agents'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation, ensureInstalledPackage } from './services/epm/packages'; -import { RouterWrappers } from './routes/security'; +import { getAuthzFromRequest, RouterWrappers } from './routes/security'; import { FleetArtifactsClient } from './services/artifacts'; import type { FleetRouter } from './types/request_context'; import { TelemetryEventsSender } from './telemetry/sender'; @@ -142,6 +143,9 @@ export interface FleetStartContract { * services */ fleetSetupCompleted: () => Promise; + authz: { + fromRequest(request: KibanaRequest): Promise; + }; esIndexPatternService: ESIndexPatternService; packageService: PackageService; agentService: AgentService; @@ -205,7 +209,7 @@ export class FleetPlugin // TODO: Flesh out privileges if (deps.features) { deps.features.registerKibanaFeature({ - id: PLUGIN_ID, + id: 'fleet', name: 'Fleet and Integrations', category: DEFAULT_APP_CATEGORIES.management, app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], @@ -230,7 +234,7 @@ export class FleetPlugin }, privileges: { all: { - api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`], + api: [`fleet-read`, `fleet-all`, `integrations-all`, `integrations-read`], app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], catalogue: ['fleet'], savedObject: { @@ -240,7 +244,7 @@ export class FleetPlugin ui: ['show', 'read', 'write'], }, read: { - api: [`${PLUGIN_ID}-read`], + api: [`fleet-read`, `integrations-read`], app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], catalogue: ['fleet'], // TODO: check if this is actually available to read user savedObject: { @@ -255,7 +259,8 @@ export class FleetPlugin core.http.registerRouteHandlerContext( 'fleet', - (coreContext, request) => ({ + async (coreContext, request) => ({ + authz: await getAuthzFromRequest(request), epm: { // Use a lazy getter to avoid constructing this client when not used by a request handler get internalSoClient() { @@ -348,6 +353,9 @@ export class FleetPlugin })(); return { + authz: { + fromRequest: getAuthzFromRequest, + }, fleetSetupCompleted: () => fleetSetupPromise, esIndexPatternService: new ESIndexPatternSavedObjectService(), packageService: { diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index 8a67a7066742..0b7065edf63b 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -12,6 +12,9 @@ import type { RequestHandlerContext, } from 'src/core/server'; +import type { FleetAuthz } from '../../common'; +import { calculateAuthz } from '../../common'; + import { appContextService } from '../services'; const SUPERUSER_AUTHZ_MESSAGE = @@ -120,6 +123,48 @@ function makeRouterEnforcingFleetSetupPrivilege { + const security = appContextService.getSecurity(); + + if (security.authz.mode.useRbacForRequest(req)) { + const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req); + const { privileges } = await checkPrivileges({ + kibana: [ + security.authz.actions.api.get('fleet-all'), + security.authz.actions.api.get('fleet-setup'), + security.authz.actions.api.get('integrations-all'), + security.authz.actions.api.get('integrations-read'), + ], + }); + + const [fleetAll, fleetSetup, intAll, intRead] = privileges.kibana; + + return calculateAuthz({ + fleet: { + all: fleetAll.authorized, + setup: fleetSetup.authorized, + }, + + integrations: { + all: intAll.authorized, + read: intRead.authorized, + }, + }); + } + + return calculateAuthz({ + fleet: { + all: true, + setup: true, + }, + + integrations: { + all: true, + read: true, + }, + }); +} + export type RouterWrapper = (route: IRouter) => IRouter; interface RouterWrappersSetup { 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 ffdec9509b05..4f034a0add32 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -9,7 +9,7 @@ import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; -import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; +import { createAppContextStartContractMock, xpackMocks, fleetAuthzMock } from '../../mocks'; import { appContextService } from '../../services/app_context'; import { setupFleet } from '../../services/setup'; import type { FleetRequestHandlerContext } from '../../types'; @@ -34,6 +34,7 @@ describe('FleetSetupHandler', () => { context = { ...xpackMocks.createRequestHandlerContext(), fleet: { + authz: fleetAuthzMock, epm: { internalSoClient: savedObjectsClientMock.create(), }, diff --git a/x-pack/plugins/fleet/server/types/request_context.ts b/x-pack/plugins/fleet/server/types/request_context.ts index 0d0da9145f07..8de68c91f4ef 100644 --- a/x-pack/plugins/fleet/server/types/request_context.ts +++ b/x-pack/plugins/fleet/server/types/request_context.ts @@ -13,10 +13,12 @@ import type { SavedObjectsClientContract, IRouter, } from '../../../../../src/core/server'; +import type { FleetAuthz } from '../../common/authz'; /** @internal */ export interface FleetRequestHandlerContext extends RequestHandlerContext { fleet: { + authz: FleetAuthz; epm: { /** * Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be diff --git a/x-pack/plugins/fleet/storybook/context/index.tsx b/x-pack/plugins/fleet/storybook/context/index.tsx index ae18f393970f..8a13cd1e8580 100644 --- a/x-pack/plugins/fleet/storybook/context/index.tsx +++ b/x-pack/plugins/fleet/storybook/context/index.tsx @@ -73,6 +73,24 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ theme: { theme$: EMPTY, }, + authz: { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, + }, }), [isCloudEnabled] ); diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index c428cf49b1e1..4c722672efe4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -17,6 +17,7 @@ import { createMockAgentPolicyService, createMockAgentService, createArtifactsClientMock, + fleetAuthzMock, } from '../../../fleet/server/mocks'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { @@ -153,6 +154,9 @@ export const createMockPackageService = (): jest.Mocked => { */ export const createMockFleetStartContract = (indexPattern: string): FleetStartContract => { return { + authz: { + fromRequest: jest.fn().mockResolvedValue(fleetAuthzMock), + }, fleetSetupCompleted: jest.fn().mockResolvedValue(undefined), esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),