[Fleet] Allow package to specify cluster privileges (#114945)

This commit is contained in:
Nicolas Chaulet 2021-10-18 13:16:13 -04:00 committed by GitHub
parent dba055c654
commit 2f27ccffbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 242 additions and 27 deletions

View file

@ -126,6 +126,11 @@ interface RegistryAdditionalProperties {
readme?: string; 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] 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 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 { interface RegistryOverridePropertyValue {
icons?: RegistryImage[]; icons?: RegistryImage[];

View file

@ -65,6 +65,11 @@ export interface NewPackagePolicy {
package?: PackagePolicyPackage; package?: PackagePolicyPackage;
inputs: NewPackagePolicyInput[]; inputs: NewPackagePolicyInput[];
vars?: PackagePolicyConfigRecord; vars?: PackagePolicyConfigRecord;
elasticsearch?: {
privileges?: {
cluster?: string[];
};
};
} }
export interface UpdatePackagePolicy extends NewPackagePolicy { export interface UpdatePackagePolicy extends NewPackagePolicy {

View file

@ -65,7 +65,7 @@ export const xpackMocks = {
export const createPackagePolicyServiceMock = (): jest.Mocked<PackagePolicyServiceInterface> => { export const createPackagePolicyServiceMock = (): jest.Mocked<PackagePolicyServiceInterface> => {
return { return {
compilePackagePolicyInputs: jest.fn(), _compilePackagePolicyInputs: jest.fn(),
buildPackagePolicyFromPackage: jest.fn(), buildPackagePolicyFromPackage: jest.fn(),
bulkCreate: jest.fn(), bulkCreate: jest.fn(),
create: jest.fn(), create: jest.fn(),

View file

@ -35,7 +35,7 @@ jest.mock(
} => { } => {
return { return {
packagePolicyService: { packagePolicyService: {
compilePackagePolicyInputs: jest.fn((packageInfo, vars, dataInputs) => _compilePackagePolicyInputs: jest.fn((registryPkgInfo, packageInfo, vars, dataInputs) =>
Promise.resolve(dataInputs) Promise.resolve(dataInputs)
), ),
buildPackagePolicyFromPackage: jest.fn(), buildPackagePolicyFromPackage: jest.fn(),

View file

@ -236,6 +236,16 @@ const getSavedObjectTypes = (
version: { type: 'keyword' }, version: { type: 'keyword' },
}, },
}, },
elasticsearch: {
enabled: false,
properties: {
privileges: {
properties: {
cluster: { type: 'keyword' },
},
},
},
},
vars: { type: 'flattened' }, vars: { type: 'flattened' },
inputs: { inputs: {
type: 'nested', type: 'nested',

View file

@ -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 () => { it('Returns the dataset for osquery_manager package', async () => {
getPackageInfoMock.mockResolvedValueOnce({ getPackageInfoMock.mockResolvedValueOnce({
format_version: '1.0.0', format_version: '1.0.0',

View file

@ -121,12 +121,21 @@ export async function storedPackagePoliciesToAgentPermissions(
}); });
} }
let clusterRoleDescriptor = {};
const cluster = packagePolicy?.elasticsearch?.privileges?.cluster ?? [];
if (cluster.length > 0) {
clusterRoleDescriptor = {
cluster,
};
}
return [ return [
packagePolicy.name, packagePolicy.name,
{ {
indices: dataStreamsForPermissions.map((ds) => indices: dataStreamsForPermissions.map((ds) =>
getDataStreamPrivileges(ds, packagePolicy.namespace) getDataStreamPrivileges(ds, packagePolicy.namespace)
), ),
...clusterRoleDescriptor,
}, },
]; ];
} }

View file

@ -33,6 +33,7 @@ import type {
InputsOverride, InputsOverride,
NewPackagePolicy, NewPackagePolicy,
NewPackagePolicyInput, NewPackagePolicyInput,
RegistryPackage,
} from '../../common'; } from '../../common';
import { IngestManagerError } from '../errors'; import { IngestManagerError } from '../errors';
@ -43,6 +44,7 @@ import {
_applyIndexPrivileges, _applyIndexPrivileges,
} from './package_policy'; } from './package_policy';
import { appContextService } from './app_context'; import { appContextService } from './app_context';
import { fetchInfo } from './epm/registry';
async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { async function mockedGetAssetsData(_a: any, _b: any, dataset: string) {
if (dataset === 'dataset1') { if (dataset === 'dataset1') {
@ -88,6 +90,10 @@ hosts:
]; ];
} }
function mockedRegistryInfo(): RegistryPackage {
return {} as RegistryPackage;
}
jest.mock('./epm/packages/assets', () => { jest.mock('./epm/packages/assets', () => {
return { return {
getAssetsData: mockedGetAssetsData, getAssetsData: mockedGetAssetsData,
@ -100,11 +106,7 @@ jest.mock('./epm/packages', () => {
}; };
}); });
jest.mock('./epm/registry', () => { jest.mock('./epm/registry');
return {
fetchInfo: () => ({}),
};
});
jest.mock('./agent_policy', () => { jest.mock('./agent_policy', () => {
return { return {
@ -126,12 +128,18 @@ jest.mock('./agent_policy', () => {
}; };
}); });
const mockedFetchInfo = fetchInfo as jest.Mock<ReturnType<typeof fetchInfo>>;
type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback; type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback;
describe('Package policy service', () => { describe('Package policy service', () => {
describe('compilePackagePolicyInputs', () => { beforeEach(() => {
mockedFetchInfo.mockResolvedValue({} as RegistryPackage);
});
describe('_compilePackagePolicyInputs', () => {
it('should work with config variables from the stream', async () => { it('should work with config variables from the stream', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs( const inputs = await packagePolicyService._compilePackagePolicyInputs(
mockedRegistryInfo(),
{ {
data_streams: [ data_streams: [
{ {
@ -194,7 +202,8 @@ describe('Package policy service', () => {
}); });
it('should work with a two level dataset name', async () => { it('should work with a two level dataset name', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs( const inputs = await packagePolicyService._compilePackagePolicyInputs(
mockedRegistryInfo(),
{ {
data_streams: [ data_streams: [
{ {
@ -246,7 +255,8 @@ describe('Package policy service', () => {
}); });
it('should work with config variables at the input level', async () => { it('should work with config variables at the input level', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs( const inputs = await packagePolicyService._compilePackagePolicyInputs(
mockedRegistryInfo(),
{ {
data_streams: [ data_streams: [
{ {
@ -309,7 +319,8 @@ describe('Package policy service', () => {
}); });
it('should work with config variables at the package level', async () => { it('should work with config variables at the package level', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs( const inputs = await packagePolicyService._compilePackagePolicyInputs(
mockedRegistryInfo(),
{ {
data_streams: [ data_streams: [
{ {
@ -377,7 +388,8 @@ describe('Package policy service', () => {
}); });
it('should work with an input with a template and no streams', async () => { 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: [], data_streams: [],
policy_templates: [ policy_templates: [
@ -419,7 +431,8 @@ describe('Package policy service', () => {
}); });
it('should work with an input with a template and streams', async () => { 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: [ data_streams: [
{ {
@ -524,7 +537,8 @@ describe('Package policy service', () => {
}); });
it('should work with a package without input', async () => { it('should work with a package without input', async () => {
const inputs = await packagePolicyService.compilePackagePolicyInputs( const inputs = await packagePolicyService._compilePackagePolicyInputs(
mockedRegistryInfo(),
{ {
policy_templates: [ policy_templates: [
{ {
@ -540,7 +554,8 @@ describe('Package policy service', () => {
}); });
it('should work with a package with a empty inputs array', async () => { 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: [ policy_templates: [
{ {
@ -834,6 +849,59 @@ describe('Package policy service', () => {
expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south'])); expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south']));
expect(modifiedStream.vars!.period.value).toEqual('12mo'); 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<SavedObjectsUpdateResponse<PackagePolicySOAttributes>> => {
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', () => { describe('runDeleteExternalCallbacks', () => {

View file

@ -114,7 +114,7 @@ class PackagePolicyService {
'There is already a package with the same name on this agent policy' 'There is already a package with the same name on this agent policy'
); );
} }
let elasticsearch: PackagePolicy['elasticsearch'];
// Add ids to stream // Add ids to stream
const packagePolicyId = options?.id || uuid.v4(); const packagePolicyId = options?.id || uuid.v4();
let inputs: PackagePolicyInput[] = packagePolicy.inputs.map((input) => 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(); const isoDate = new Date().toISOString();
@ -164,6 +172,7 @@ class PackagePolicyService {
{ {
...packagePolicy, ...packagePolicy,
inputs, inputs,
elasticsearch,
revision: 1, revision: 1,
created_at: isoDate, created_at: isoDate,
created_by: options?.user?.username ?? 'system', created_by: options?.user?.username ?? 'system',
@ -375,15 +384,21 @@ class PackagePolicyService {
); );
inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs); inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs);
let elasticsearch: PackagePolicy['elasticsearch'];
if (packagePolicy.package?.name) { if (packagePolicy.package?.name) {
const pkgInfo = await getPackageInfo({ const pkgInfo = await getPackageInfo({
savedObjectsClient: soClient, savedObjectsClient: soClient,
pkgName: packagePolicy.package.name, pkgName: packagePolicy.package.name,
pkgVersion: packagePolicy.package.version, pkgVersion: packagePolicy.package.version,
}); });
const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version);
inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs); inputs = await this._compilePackagePolicyInputs(
registryPkgInfo,
pkgInfo,
packagePolicy.vars || {},
inputs
);
elasticsearch = registryPkgInfo.elasticsearch;
} }
await soClient.update<PackagePolicySOAttributes>( await soClient.update<PackagePolicySOAttributes>(
@ -392,6 +407,7 @@ class PackagePolicyService {
{ {
...restOfPackagePolicy, ...restOfPackagePolicy,
inputs, inputs,
elasticsearch,
revision: oldPackagePolicy.revision + 1, revision: oldPackagePolicy.revision + 1,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
updated_by: options?.user?.username ?? 'system', updated_by: options?.user?.username ?? 'system',
@ -563,12 +579,14 @@ class PackagePolicyService {
packageInfo, packageInfo,
packageToPackagePolicyInputs(packageInfo) as InputsOverride[] packageToPackagePolicyInputs(packageInfo) as InputsOverride[]
); );
const registryPkgInfo = await Registry.fetchInfo(packageInfo.name, packageInfo.version);
updatePackagePolicy.inputs = await this.compilePackagePolicyInputs( updatePackagePolicy.inputs = await this._compilePackagePolicyInputs(
registryPkgInfo,
packageInfo, packageInfo,
updatePackagePolicy.vars || {}, updatePackagePolicy.vars || {},
updatePackagePolicy.inputs as PackagePolicyInput[] updatePackagePolicy.inputs as PackagePolicyInput[]
); );
updatePackagePolicy.elasticsearch = registryPkgInfo.elasticsearch;
await this.update(soClient, esClient, id, updatePackagePolicy, options); await this.update(soClient, esClient, id, updatePackagePolicy, options);
result.push({ result.push({
@ -618,12 +636,14 @@ class PackagePolicyService {
packageToPackagePolicyInputs(packageInfo) as InputsOverride[], packageToPackagePolicyInputs(packageInfo) as InputsOverride[],
true true
); );
const registryPkgInfo = await Registry.fetchInfo(packageInfo.name, packageInfo.version);
updatedPackagePolicy.inputs = await this.compilePackagePolicyInputs( updatedPackagePolicy.inputs = await this._compilePackagePolicyInputs(
registryPkgInfo,
packageInfo, packageInfo,
updatedPackagePolicy.vars || {}, updatedPackagePolicy.vars || {},
updatedPackagePolicy.inputs as PackagePolicyInput[] updatedPackagePolicy.inputs as PackagePolicyInput[]
); );
updatedPackagePolicy.elasticsearch = registryPkgInfo.elasticsearch;
const hasErrors = 'errors' in updatedPackagePolicy; const hasErrors = 'errors' in updatedPackagePolicy;
@ -663,12 +683,12 @@ class PackagePolicyService {
} }
} }
public async compilePackagePolicyInputs( public async _compilePackagePolicyInputs(
registryPkgInfo: RegistryPackage,
pkgInfo: PackageInfo, pkgInfo: PackageInfo,
vars: PackagePolicy['vars'], vars: PackagePolicy['vars'],
inputs: PackagePolicyInput[] inputs: PackagePolicyInput[]
): Promise<PackagePolicyInput[]> { ): Promise<PackagePolicyInput[]> {
const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version);
const inputsPromises = inputs.map(async (input) => { const inputsPromises = inputs.map(async (input) => {
const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, vars, input); const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, vars, input);
const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, vars, input); const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, vars, input);