From 2f27ccffbfb8b66510c8b17853eaa8fbd69f21a1 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 18 Oct 2021 13:16:13 -0400 Subject: [PATCH] [Fleet] Allow package to specify cluster privileges (#114945) --- .../plugins/fleet/common/types/models/epm.ts | 5 + .../common/types/models/package_policy.ts | 5 + x-pack/plugins/fleet/server/mocks/index.ts | 2 +- .../routes/package_policy/handlers.test.ts | 2 +- .../fleet/server/saved_objects/index.ts | 10 ++ ...kage_policies_to_agent_permissions.test.ts | 98 +++++++++++++++++++ .../package_policies_to_agent_permissions.ts | 9 ++ .../server/services/package_policy.test.ts | 96 +++++++++++++++--- .../fleet/server/services/package_policy.ts | 42 +++++--- 9 files changed, 242 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index eaac2a811323..6f107ae44bfa 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -126,6 +126,11 @@ interface RegistryAdditionalProperties { readme?: string; internal?: boolean; // Registry addition[0] and EPM uses it[1] [0]: https://github.com/elastic/package-registry/blob/dd7b021893aa8d66a5a5fde963d8ff2792a9b8fa/util/package.go#L63 [1] data_streams?: RegistryDataStream[]; // Registry addition [0] [0]: https://github.com/elastic/package-registry/blob/dd7b021893aa8d66a5a5fde963d8ff2792a9b8fa/util/package.go#L65 + elasticsearch?: { + privileges?: { + cluster?: string[]; + }; + }; } interface RegistryOverridePropertyValue { icons?: RegistryImage[]; diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index aca537ae31b5..df484646ef66 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -65,6 +65,11 @@ export interface NewPackagePolicy { package?: PackagePolicyPackage; inputs: NewPackagePolicyInput[]; vars?: PackagePolicyConfigRecord; + elasticsearch?: { + privileges?: { + cluster?: string[]; + }; + }; } export interface UpdatePackagePolicy extends NewPackagePolicy { diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 0e7b335da677..c7f6b6fefc41 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -65,7 +65,7 @@ export const xpackMocks = { export const createPackagePolicyServiceMock = (): jest.Mocked => { return { - compilePackagePolicyInputs: jest.fn(), + _compilePackagePolicyInputs: jest.fn(), buildPackagePolicyFromPackage: jest.fn(), bulkCreate: jest.fn(), create: jest.fn(), diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 729417fa9606..5441af0af686 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -35,7 +35,7 @@ jest.mock( } => { return { packagePolicyService: { - compilePackagePolicyInputs: jest.fn((packageInfo, vars, dataInputs) => + _compilePackagePolicyInputs: jest.fn((registryPkgInfo, packageInfo, vars, dataInputs) => Promise.resolve(dataInputs) ), buildPackagePolicyFromPackage: jest.fn(), diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index ac5ca401da00..f0b51b19dda3 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -236,6 +236,16 @@ const getSavedObjectTypes = ( version: { type: 'keyword' }, }, }, + elasticsearch: { + enabled: false, + properties: { + privileges: { + properties: { + cluster: { type: 'keyword' }, + }, + }, + }, + }, vars: { type: 'flattened' }, inputs: { type: 'nested', diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts index 2ce68b46387c..72566a18bd66 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -279,6 +279,104 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }); }); + it('Returns the cluster privileges if there is one in the package policy', async () => { + getPackageInfoMock.mockResolvedValueOnce({ + name: 'test-package', + version: '0.0.0', + latestVersion: '0.0.0', + release: 'experimental', + format_version: '1.0.0', + title: 'Test Package', + description: '', + icons: [], + owner: { github: '' }, + status: 'not_installed', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + tag: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + ml_model: [], + }, + }, + data_streams: [ + { + type: 'logs', + dataset: 'some-logs', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-logs', title: 'Test Logs', template_path: '' }], + }, + ], + }); + + const packagePolicies: PackagePolicy[] = [ + { + id: '12345', + name: 'test-policy', + namespace: 'test', + enabled: true, + package: { name: 'test-package', version: '0.0.0', title: 'Test Package' }, + elasticsearch: { + privileges: { + cluster: ['monitor'], + }, + }, + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + compiled_stream: { data_stream: { dataset: 'compiled' } }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + output_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); + expect(permissions).toMatchObject({ + 'test-policy': { + indices: [ + { + names: ['logs-compiled-test'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + cluster: ['monitor'], + }, + }); + }); + it('Returns the dataset for osquery_manager package', async () => { getPackageInfoMock.mockResolvedValueOnce({ format_version: '1.0.0', diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts index 22dcb8ac7b4c..383747fe126c 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts @@ -121,12 +121,21 @@ export async function storedPackagePoliciesToAgentPermissions( }); } + let clusterRoleDescriptor = {}; + const cluster = packagePolicy?.elasticsearch?.privileges?.cluster ?? []; + if (cluster.length > 0) { + clusterRoleDescriptor = { + cluster, + }; + } + return [ packagePolicy.name, { indices: dataStreamsForPermissions.map((ds) => getDataStreamPrivileges(ds, packagePolicy.namespace) ), + ...clusterRoleDescriptor, }, ]; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 0b6b3579f7b8..9dc05ee2cb4b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -33,6 +33,7 @@ import type { InputsOverride, NewPackagePolicy, NewPackagePolicyInput, + RegistryPackage, } from '../../common'; import { IngestManagerError } from '../errors'; @@ -43,6 +44,7 @@ import { _applyIndexPrivileges, } from './package_policy'; import { appContextService } from './app_context'; +import { fetchInfo } from './epm/registry'; async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { if (dataset === 'dataset1') { @@ -88,6 +90,10 @@ hosts: ]; } +function mockedRegistryInfo(): RegistryPackage { + return {} as RegistryPackage; +} + jest.mock('./epm/packages/assets', () => { return { getAssetsData: mockedGetAssetsData, @@ -100,11 +106,7 @@ jest.mock('./epm/packages', () => { }; }); -jest.mock('./epm/registry', () => { - return { - fetchInfo: () => ({}), - }; -}); +jest.mock('./epm/registry'); jest.mock('./agent_policy', () => { return { @@ -126,12 +128,18 @@ jest.mock('./agent_policy', () => { }; }); +const mockedFetchInfo = fetchInfo as jest.Mock>; + type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback; describe('Package policy service', () => { - describe('compilePackagePolicyInputs', () => { + beforeEach(() => { + mockedFetchInfo.mockResolvedValue({} as RegistryPackage); + }); + describe('_compilePackagePolicyInputs', () => { it('should work with config variables from the stream', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { data_streams: [ { @@ -194,7 +202,8 @@ describe('Package policy service', () => { }); it('should work with a two level dataset name', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { data_streams: [ { @@ -246,7 +255,8 @@ describe('Package policy service', () => { }); it('should work with config variables at the input level', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { data_streams: [ { @@ -309,7 +319,8 @@ describe('Package policy service', () => { }); it('should work with config variables at the package level', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { data_streams: [ { @@ -377,7 +388,8 @@ describe('Package policy service', () => { }); it('should work with an input with a template and no streams', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { data_streams: [], policy_templates: [ @@ -419,7 +431,8 @@ describe('Package policy service', () => { }); it('should work with an input with a template and streams', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { data_streams: [ { @@ -524,7 +537,8 @@ describe('Package policy service', () => { }); it('should work with a package without input', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { policy_templates: [ { @@ -540,7 +554,8 @@ describe('Package policy service', () => { }); it('should work with a package with a empty inputs array', async () => { - const inputs = await packagePolicyService.compilePackagePolicyInputs( + const inputs = await packagePolicyService._compilePackagePolicyInputs( + mockedRegistryInfo(), { policy_templates: [ { @@ -834,6 +849,59 @@ describe('Package policy service', () => { expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south'])); expect(modifiedStream.vars!.period.value).toEqual('12mo'); }); + + it('should update elasticsearch.priviles.cluster when updating', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const mockPackagePolicy = createPackagePolicyMock(); + + const attributes = { + ...mockPackagePolicy, + inputs: [], + }; + + mockedFetchInfo.mockResolvedValue({ + elasticsearch: { + privileges: { + cluster: ['monitor'], + }, + }, + } as RegistryPackage); + + savedObjectsClient.get.mockResolvedValue({ + id: 'test', + type: 'abcd', + references: [], + version: 'test', + attributes, + }); + + savedObjectsClient.update.mockImplementation( + async ( + type: string, + id: string, + attrs: any + ): Promise> => { + savedObjectsClient.get.mockResolvedValue({ + id: 'test', + type: 'abcd', + references: [], + version: 'test', + attributes: attrs, + }); + return attrs; + } + ); + const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + const result = await packagePolicyService.update( + savedObjectsClient, + elasticsearchClient, + 'the-package-policy-id', + { ...mockPackagePolicy, inputs: [] } + ); + + expect(result.elasticsearch).toMatchObject({ privileges: { cluster: ['monitor'] } }); + }); }); describe('runDeleteExternalCallbacks', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index b7772892f542..b0c0b9499c68 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -114,7 +114,7 @@ class PackagePolicyService { 'There is already a package with the same name on this agent policy' ); } - + let elasticsearch: PackagePolicy['elasticsearch']; // Add ids to stream const packagePolicyId = options?.id || uuid.v4(); let inputs: PackagePolicyInput[] = packagePolicy.inputs.map((input) => @@ -155,7 +155,15 @@ class PackagePolicyService { } } - inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs); + const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); + inputs = await this._compilePackagePolicyInputs( + registryPkgInfo, + pkgInfo, + packagePolicy.vars || {}, + inputs + ); + + elasticsearch = registryPkgInfo.elasticsearch; } const isoDate = new Date().toISOString(); @@ -164,6 +172,7 @@ class PackagePolicyService { { ...packagePolicy, inputs, + elasticsearch, revision: 1, created_at: isoDate, created_by: options?.user?.username ?? 'system', @@ -375,15 +384,21 @@ class PackagePolicyService { ); inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs); - + let elasticsearch: PackagePolicy['elasticsearch']; if (packagePolicy.package?.name) { const pkgInfo = await getPackageInfo({ savedObjectsClient: soClient, pkgName: packagePolicy.package.name, pkgVersion: packagePolicy.package.version, }); - - inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs); + const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); + inputs = await this._compilePackagePolicyInputs( + registryPkgInfo, + pkgInfo, + packagePolicy.vars || {}, + inputs + ); + elasticsearch = registryPkgInfo.elasticsearch; } await soClient.update( @@ -392,6 +407,7 @@ class PackagePolicyService { { ...restOfPackagePolicy, inputs, + elasticsearch, revision: oldPackagePolicy.revision + 1, updated_at: new Date().toISOString(), updated_by: options?.user?.username ?? 'system', @@ -563,12 +579,14 @@ class PackagePolicyService { packageInfo, packageToPackagePolicyInputs(packageInfo) as InputsOverride[] ); - - updatePackagePolicy.inputs = await this.compilePackagePolicyInputs( + const registryPkgInfo = await Registry.fetchInfo(packageInfo.name, packageInfo.version); + updatePackagePolicy.inputs = await this._compilePackagePolicyInputs( + registryPkgInfo, packageInfo, updatePackagePolicy.vars || {}, updatePackagePolicy.inputs as PackagePolicyInput[] ); + updatePackagePolicy.elasticsearch = registryPkgInfo.elasticsearch; await this.update(soClient, esClient, id, updatePackagePolicy, options); result.push({ @@ -618,12 +636,14 @@ class PackagePolicyService { packageToPackagePolicyInputs(packageInfo) as InputsOverride[], true ); - - updatedPackagePolicy.inputs = await this.compilePackagePolicyInputs( + const registryPkgInfo = await Registry.fetchInfo(packageInfo.name, packageInfo.version); + updatedPackagePolicy.inputs = await this._compilePackagePolicyInputs( + registryPkgInfo, packageInfo, updatedPackagePolicy.vars || {}, updatedPackagePolicy.inputs as PackagePolicyInput[] ); + updatedPackagePolicy.elasticsearch = registryPkgInfo.elasticsearch; const hasErrors = 'errors' in updatedPackagePolicy; @@ -663,12 +683,12 @@ class PackagePolicyService { } } - public async compilePackagePolicyInputs( + public async _compilePackagePolicyInputs( + registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, vars: PackagePolicy['vars'], inputs: PackagePolicyInput[] ): Promise { - const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); const inputsPromises = inputs.map(async (input) => { const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, vars, input); const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, vars, input);