[Fleet] Reuse shared integration policies when duplicating agent policies (#217872)

## Summary

Closes https://github.com/elastic/kibana/issues/215335

Currently, when an agent policy is duplicated, shared integration
policies are also duplicated. This PR adds logic where the duplicated
agent policy also shares these integration policies.

### Testing

* Run ES with an [Entreprise
license](https://www.elastic.co/subscriptions) to avail of reusable
integration policies.
* Create an agent policy with a shared integration policy and a
non-shared integration policy.
* Duplicate the agent policy: the duplicated policy should only
duplicate the non-shared integration policy and the shared integration
policy should be reused.

### Checklist

- [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
- [ ] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Incorrect package policies in duplicated agent policies.
This commit is contained in:
Jill Guyonnet 2025-04-11 17:16:17 +02:00 committed by GitHub
parent a3766fd1ef
commit 5c78ff1848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 22 deletions

View file

@ -1251,6 +1251,98 @@ describe('Agent policy', () => {
);
}
});
it('should link shared package policies', async () => {
agentPolicyService.requireUniqueName = async () => {};
soClient = savedObjectsClientMock.create();
const mockPolicy = {
type: LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE,
references: [],
attributes: { revision: 1, package_policies: ['package-1'] } as any,
};
soClient.get.mockImplementation(async (type: string, id: string) => {
return {
id,
...mockPolicy,
};
});
soClient.find
.mockImplementationOnce(async () => ({
saved_objects: [
{
id: 'agent-policy-id',
score: 1,
...{ ...mockPolicy, name: 'mocked-policy' },
},
],
total: 1,
page: 1,
per_page: 1,
}))
.mockImplementationOnce(async () => ({
saved_objects: [
{
id: 'agent-policy-id-copy',
score: 1,
...{ ...mockPolicy, name: 'mocked-policy' },
},
],
total: 1,
page: 1,
per_page: 1,
}));
soClient.create.mockImplementation(async (type, attributes) => {
return {
attributes: attributes as unknown as NewAgentPolicy,
id: 'mocked',
type: 'mocked',
references: [],
};
});
const packagePolicies = [
{
id: 'package-1',
name: 'package-1',
policy_id: 'policy_1',
policy_ids: ['policy_1', 'policy_2'],
},
{
id: 'package-2',
name: 'package-2',
policy_id: 'policy_1',
policy_ids: ['policy_1'],
},
] as any;
mockedPackagePolicyService.findAllForAgentPolicy.mockReturnValue(packagePolicies);
mockedPackagePolicyService.list.mockResolvedValue({ items: packagePolicies } as any);
await agentPolicyService.copy(soClient, esClient, 'mocked', {
name: 'copy mocked',
});
expect(mockedPackagePolicyService.bulkCreate).toBeCalledWith(
expect.anything(),
expect.anything(),
[
{
name: 'package-2 (copy)',
policy_id: 'policy_1',
policy_ids: ['mocked'],
},
],
expect.anything()
);
expect(mockedPackagePolicyService.bulkUpdate).toBeCalledWith(
expect.anything(),
expect.anything(),
[
{
id: 'package-1',
name: 'package-1',
policy_id: 'policy_1',
policy_ids: ['policy_1', 'policy_2', 'mocked'],
},
]
);
});
});
describe('deployPolicy', () => {

View file

@ -849,32 +849,48 @@ class AgentPolicyService {
options
);
// Copy all package policies and append (copy n) to their names
if (baseAgentPolicy.package_policies) {
const newPackagePolicies = await pMap(
baseAgentPolicy.package_policies as PackagePolicy[],
async (packagePolicy: PackagePolicy) => {
const { id: packagePolicyId, version, ...newPackagePolicy } = packagePolicy;
// Copy non-shared package policies and append (copy n) to their names.
const basePackagePolicies = baseAgentPolicy.package_policies.filter(
(packagePolicy) => packagePolicy.policy_ids.length < 2
);
if (basePackagePolicies.length > 0) {
const newPackagePolicies = await pMap(
basePackagePolicies,
async (packagePolicy: PackagePolicy) => {
const { id: packagePolicyId, version, ...newPackagePolicy } = packagePolicy;
const updatedPackagePolicy = {
const updatedPackagePolicy = {
...newPackagePolicy,
name: await incrementPackagePolicyCopyName(soClient, packagePolicy.name),
};
return updatedPackagePolicy;
}
);
await packagePolicyService.bulkCreate(
soClient,
esClient,
newPackagePolicies.map((newPackagePolicy) => ({
...newPackagePolicy,
name: await incrementPackagePolicyCopyName(soClient, packagePolicy.name),
};
return updatedPackagePolicy;
}
);
await packagePolicyService.bulkCreate(
soClient,
esClient,
newPackagePolicies.map((newPackagePolicy) => ({
...newPackagePolicy,
policy_ids: [newAgentPolicy.id],
})),
{
...options,
bumpRevision: false,
}
policy_ids: [newAgentPolicy.id],
})),
{
...options,
bumpRevision: false,
}
);
}
// Link shared package policies to new agent policy.
const sharedBasePackagePolicies = baseAgentPolicy.package_policies.filter(
(packagePolicy) => packagePolicy.policy_ids.length > 1
);
if (sharedBasePackagePolicies.length > 0) {
const updatedSharedPackagePolicies = sharedBasePackagePolicies.map((packagePolicy) => ({
...packagePolicy,
policy_ids: [...packagePolicy.policy_ids, newAgentPolicy.id],
}));
await packagePolicyService.bulkUpdate(soClient, esClient, updatedSharedPackagePolicies);
}
}
// Tamper protection is dependent on endpoint package policy