diff --git a/x-pack/plugins/fleet/common/constants/package_policy.ts b/x-pack/plugins/fleet/common/constants/package_policy.ts index f1d6f00d05773..42bbe5bb5b79a 100644 --- a/x-pack/plugins/fleet/common/constants/package_policy.ts +++ b/x-pack/plugins/fleet/common/constants/package_policy.ts @@ -6,3 +6,5 @@ */ export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; + +export const PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES = ['auto_configure', 'create_doc']; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index bbb571f963dc9..66852bc965b07 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -281,7 +281,6 @@ export enum RegistryDataStreamKeys { ingest_pipeline = 'ingest_pipeline', elasticsearch = 'elasticsearch', dataset_is_prefix = 'dataset_is_prefix', - permissions = 'permissions', } export interface RegistryDataStream { @@ -297,15 +296,15 @@ export interface RegistryDataStream { [RegistryDataStreamKeys.ingest_pipeline]?: string; [RegistryDataStreamKeys.elasticsearch]?: RegistryElasticsearch; [RegistryDataStreamKeys.dataset_is_prefix]?: boolean; - [RegistryDataStreamKeys.permissions]?: RegistryDataStreamPermissions; } export interface RegistryElasticsearch { + privileges?: RegistryDataStreamPrivileges; 'index_template.settings'?: estypes.IndicesIndexSettings; 'index_template.mappings'?: estypes.MappingTypeMapping; } -export interface RegistryDataStreamPermissions { +export interface RegistryDataStreamPrivileges { cluster?: string[]; indices?: string[]; } 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 ab977c5d67d0d..aca537ae31b52 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -25,6 +25,11 @@ export interface NewPackagePolicyInputStream { data_stream: { dataset: string; type: string; + elasticsearch?: { + privileges?: { + indices?: string[]; + }; + }; }; vars?: PackagePolicyConfigRecord; config?: PackagePolicyConfigRecord; diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 28f3ea96f732e..0dcd5e7f47800 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -49,6 +49,7 @@ export { DEFAULT_FLEET_SERVER_AGENT_POLICY, DEFAULT_OUTPUT, DEFAULT_PACKAGES, + PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, // Fleet Server index FLEET_SERVER_SERVERS_INDEX, ENROLLMENT_API_KEYS_INDEX, diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 449a1984aa53b..5c117909432b0 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -251,6 +251,11 @@ const getSavedObjectTypes = ( properties: { dataset: { type: 'keyword' }, type: { type: 'keyword' }, + elasticsearch: { + properties: { + privileges: { type: 'flattened' }, + }, + }, }, }, vars: { type: 'flattened' }, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 9fe275bb9a3c9..38fb07754bdd3 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -21,6 +21,7 @@ import { AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, } from '../constants'; import type { PackagePolicy, @@ -825,7 +826,7 @@ class AgentPolicyService { permissions._elastic_agent_checks.indices = [ { names, - privileges: ['auto_configure', 'create_doc'], + privileges: PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, }, ]; } 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 a84118cdf1bfa..9f8ac01afe6c9 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 @@ -13,7 +13,7 @@ import type { PackagePolicy, RegistryDataStream } from '../types'; import { getPackageInfo } from './epm/packages'; import { - getDataStreamPermissions, + getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, } from './package_policies_to_agent_permissions'; @@ -380,12 +380,12 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }); }); -describe('getDataStreamPermissions()', () => { - it('returns defaults for a datastream with no permissions', () => { +describe('getDataStreamPrivileges()', () => { + it('returns defaults for a datastream with no privileges', () => { const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream); + const privileges = getDataStreamPrivileges(dataStream); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test-*'], privileges: ['auto_configure', 'create_doc'], }); @@ -393,9 +393,9 @@ describe('getDataStreamPermissions()', () => { it('adds the namespace to the index name', () => { const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test-namespace'], privileges: ['auto_configure', 'create_doc'], }); @@ -407,9 +407,9 @@ describe('getDataStreamPermissions()', () => { dataset: 'test', dataset_is_prefix: true, } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test.*-namespace'], privileges: ['auto_configure', 'create_doc'], }); @@ -421,25 +421,27 @@ describe('getDataStreamPermissions()', () => { dataset: 'test', hidden: true, } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['.logs-test-namespace'], privileges: ['auto_configure', 'create_doc'], }); }); - it('uses custom permissions if they are present in the datastream', () => { + it('uses custom privileges if they are present in the datastream', () => { const dataStream = { type: 'logs', dataset: 'test', - permissions: { indices: ['read', 'write'] }, + elasticsearch: { + privileges: { indices: ['read', 'monitor'] }, + }, } as RegistryDataStream; - const permissions = getDataStreamPermissions(dataStream, 'namespace'); + const privileges = getDataStreamPrivileges(dataStream, 'namespace'); - expect(permissions).toMatchObject({ + expect(privileges).toMatchObject({ names: ['logs-test-namespace'], - privileges: ['read', 'write'], + privileges: ['read', 'monitor'], }); }); }); 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 07ad892adc653..22dcb8ac7b4cb 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 @@ -6,9 +6,9 @@ */ import type { SavedObjectsClientContract } from 'kibana/server'; -import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPermissions } from '../../common'; +import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPrivileges } from '../../common'; +import { PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES } from '../constants'; import { getPackageInfo } from '../../server/services/epm/packages'; - import type { PackagePolicy } from '../types'; export const DEFAULT_PERMISSIONS = { @@ -22,7 +22,7 @@ export const DEFAULT_PERMISSIONS = { 'synthetics-*', '.logs-endpoint.diagnostic.collection-*', ], - privileges: ['auto_configure', 'create_doc'], + privileges: PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, }, ], }; @@ -104,12 +104,16 @@ export async function storedPackagePoliciesToAgentPermissions( return; } - const ds = { + const ds: DataStreamMeta = { type: stream.data_stream.type, dataset: stream.compiled_stream?.data_stream?.dataset ?? stream.data_stream.dataset, }; + if (stream.data_stream.elasticsearch) { + ds.elasticsearch = stream.data_stream.elasticsearch; + } + dataStreams_.push(ds); }); @@ -121,7 +125,7 @@ export async function storedPackagePoliciesToAgentPermissions( packagePolicy.name, { indices: dataStreamsForPermissions.map((ds) => - getDataStreamPermissions(ds, packagePolicy.namespace) + getDataStreamPrivileges(ds, packagePolicy.namespace) ), }, ]; @@ -136,10 +140,12 @@ interface DataStreamMeta { dataset: string; dataset_is_prefix?: boolean; hidden?: boolean; - permissions?: RegistryDataStreamPermissions; + elasticsearch?: { + privileges?: RegistryDataStreamPrivileges; + }; } -export function getDataStreamPermissions(dataStream: DataStreamMeta, namespace: string = '*') { +export function getDataStreamPrivileges(dataStream: DataStreamMeta, namespace: string = '*') { let index = `${dataStream.type}-${dataStream.dataset}`; if (dataStream.dataset_is_prefix) { @@ -152,8 +158,12 @@ export function getDataStreamPermissions(dataStream: DataStreamMeta, namespace: index += `-${namespace}`; + const privileges = dataStream?.elasticsearch?.privileges?.indices?.length + ? dataStream.elasticsearch.privileges.indices + : PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES; + return { names: [index], - privileges: dataStream.permissions?.indices || ['auto_configure', 'create_doc'], + privileges, }; } 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 aa70fc155fe74..fe5a3030bd95a 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -14,19 +14,25 @@ import { import type { SavedObjectsClient, SavedObjectsUpdateResponse } from 'src/core/server'; import type { KibanaRequest } from 'kibana/server'; -import type { PackageInfo, PackagePolicySOAttributes, AgentPolicySOAttributes } from '../types'; +import type { + PackageInfo, + PackagePolicySOAttributes, + AgentPolicySOAttributes, + PostPackagePolicyDeleteCallback, + RegistryDataStream, + PackagePolicyInputStream, +} from '../types'; import { createPackagePolicyMock } from '../../common/mocks'; + import type { PutPackagePolicyUpdateCallback, PostPackagePolicyCreateCallback } from '..'; import { createAppContextStartContractMock, xpackMocks } from '../mocks'; -import type { PostPackagePolicyDeleteCallback } from '../types'; - import type { DeletePackagePoliciesResponse } from '../../common'; import { IngestManagerError } from '../errors'; -import { packagePolicyService } from './package_policy'; +import { packagePolicyService, _applyIndexPrivileges } from './package_policy'; import { appContextService } from './app_context'; async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { @@ -1069,3 +1075,119 @@ describe('Package policy service', () => { }); }); }); + +describe('_applyIndexPrivileges()', () => { + function createPackageStream(indexPrivileges?: string[]): RegistryDataStream { + const stream: RegistryDataStream = { + type: '', + dataset: '', + title: '', + release: '', + package: '', + path: '', + }; + + if (indexPrivileges) { + stream.elasticsearch = { + privileges: { + indices: indexPrivileges, + }, + }; + } + + return stream; + } + + function createInputStream( + opts: Partial = {} + ): PackagePolicyInputStream { + return { + id: '', + enabled: true, + data_stream: { + dataset: '', + type: '', + }, + ...opts, + }; + } + + beforeAll(async () => { + appContextService.start(createAppContextStartContractMock()); + }); + + it('should do nothing if packageStream has no privileges', () => { + const packageStream = createPackageStream(); + const inputStream = createInputStream(); + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(inputStream); + }); + + it('should not apply privileges if all privileges are forbidden', () => { + const forbiddenPrivileges = ['write', 'delete', 'delete_index', 'all']; + const packageStream = createPackageStream(forbiddenPrivileges); + const inputStream = createInputStream(); + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(inputStream); + }); + + it('should not apply privileges if all privileges are unrecognized', () => { + const unrecognizedPrivileges = ['idnotexist', 'invalidperm']; + const packageStream = createPackageStream(unrecognizedPrivileges); + const inputStream = createInputStream(); + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(inputStream); + }); + + it('should apply privileges if all privileges are valid', () => { + const validPrivileges = [ + 'auto_configure', + 'create_doc', + 'maintenance', + 'monitor', + 'read', + 'read_cross_cluster', + ]; + + const packageStream = createPackageStream(validPrivileges); + const inputStream = createInputStream(); + const expectedStream = { + ...inputStream, + data_stream: { + ...inputStream.data_stream, + elasticsearch: { + privileges: { + indices: validPrivileges, + }, + }, + }, + }; + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(expectedStream); + }); + + it('should only apply valid privileges when there is a mix of valid and invalid', () => { + const mixedPrivileges = ['auto_configure', 'read_cross_cluster', 'idontexist', 'delete']; + + const packageStream = createPackageStream(mixedPrivileges); + const inputStream = createInputStream(); + const expectedStream = { + ...inputStream, + data_stream: { + ...inputStream.data_stream, + elasticsearch: { + privileges: { + indices: ['auto_configure', 'read_cross_cluster'], + }, + }, + }, + }; + + const streamOut = _applyIndexPrivileges(packageStream, inputStream); + expect(streamOut).toEqual(expectedStream); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 598dd16b2928e..c806b37f88153 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { omit } from 'lodash'; +import { omit, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import semverLte from 'semver/functions/lte'; import { getFlattenedObject } from '@kbn/std'; @@ -38,6 +38,7 @@ import type { ListWithKuery, ListResult, UpgradePackagePolicyDryRunResponseItem, + RegistryDataStream, } from '../../common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; import { @@ -71,6 +72,15 @@ export type InputsOverride = Partial & { const SAVED_OBJECT_TYPE = PACKAGE_POLICY_SAVED_OBJECT_TYPE; +export const DATA_STREAM_ALLOWED_INDEX_PRIVILEGES = new Set([ + 'auto_configure', + 'create_doc', + 'maintenance', + 'monitor', + 'read', + 'read_cross_cluster', +]); + class PackagePolicyService { public async create( soClient: SavedObjectsClientContract, @@ -794,13 +804,51 @@ async function _compilePackageStreams( return await Promise.all(streamsPromises); } +// temporary export to enable testing pending refactor https://github.com/elastic/kibana/issues/112386 +export function _applyIndexPrivileges( + packageDataStream: RegistryDataStream, + stream: PackagePolicyInputStream +): PackagePolicyInputStream { + const streamOut = { ...stream }; + + const indexPrivileges = packageDataStream?.elasticsearch?.privileges?.indices; + + if (!indexPrivileges?.length) { + return streamOut; + } + + const [valid, invalid] = partition(indexPrivileges, (permission) => + DATA_STREAM_ALLOWED_INDEX_PRIVILEGES.has(permission) + ); + + if (invalid.length) { + appContextService + .getLogger() + .warn( + `Ignoring invalid or forbidden index privilege(s) in "${stream.id}" data stream: ${invalid}` + ); + } + + if (valid.length) { + stream.data_stream.elasticsearch = { + privileges: { + indices: valid, + }, + }; + } + + return streamOut; +} + async function _compilePackageStream( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, vars: PackagePolicy['vars'], input: PackagePolicyInput, - stream: PackagePolicyInputStream + streamIn: PackagePolicyInputStream ) { + let stream = streamIn; + if (!stream.enabled) { return { ...stream, compiled_stream: undefined }; } @@ -820,6 +868,8 @@ async function _compilePackageStream( ); } + stream = _applyIndexPrivileges(packageDataStream, streamIn); + const streamFromPkg = (packageDataStream.streams || []).find( (pkgStream) => pkgStream.input === input.type ); diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index e69e38c187284..30321bdca3309 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -63,7 +63,19 @@ const PackagePolicyBaseSchema = { id: schema.maybe(schema.string()), // BWC < 7.11 enabled: schema.boolean(), keep_enabled: schema.maybe(schema.boolean()), - data_stream: schema.object({ dataset: schema.string(), type: schema.string() }), + data_stream: schema.object({ + dataset: schema.string(), + type: schema.string(), + elasticsearch: schema.maybe( + schema.object({ + privileges: schema.maybe( + schema.object({ + indices: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }) + ), + }), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf(