[Fleet] Prevent deletion of agent policies with inactive agents from UI (#175815)

Fixes https://github.com/elastic/kibana/issues/155925

## Summary
Prevent deleting an agent policy that has inactive agents assigned. The
workaround to fix existing "orphaned" agents is outlined
[here](https://github.com/elastic/kibana/issues/155925#issuecomment-1919250162).

### API
Adding check for inactive agents to the agent policy "delete" endpoint -
It will now fail if the policy has either active or inactive agents:

```
  POST /api/fleet/agent_policies/delete
  {
    agentPolicyId: 1234534,
  }
```

### UI 
Warning added to the "delete" action in the UI when trying to delete a
policy that has active or inactive agents.

## Testing
- Have a policy with many inactive agents assigned to it
- Try to delete it from the agent policy list or settings
- The deletion is prevented and a warning is raised:

![Screenshot 2024-01-31 at 12 03
11](644c9e9e-9820-4251-81fb-58f74ab57377)



### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2024-02-01 21:25:45 +01:00 committed by GitHub
parent 1fe7833a23
commit 222e894b8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 91 additions and 16 deletions

View file

@ -12,15 +12,15 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useHistory } from 'react-router-dom';
import { AGENTS_PREFIX } from '../../../constants';
import { SO_SEARCH_LIMIT } from '../../../../../constants';
import {
useStartServices,
useConfig,
sendRequest,
useLink,
useDeleteAgentPolicyMutation,
sendGetAgents,
} from '../../../hooks';
import { API_VERSIONS } from '../../../../../../common/constants';
interface Props {
children: (deleteAgentPolicy: DeleteAgentPolicy) => React.ReactElement;
@ -111,15 +111,13 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent<Props> = ({
return;
}
setIsLoadingAgentsCount(true);
const { data } = await sendRequest<{ total: number }>({
path: `/api/fleet/agents`,
method: 'get',
query: {
kuery: `${AGENTS_PREFIX}.policy_id : ${agentPolicyToCheck}`,
},
version: API_VERSIONS.public.v1,
// filtering out the unenrolled agents assigned to this policy
const agents = await sendGetAgents({
showInactive: true,
kuery: `policy_id:"${agentPolicyToCheck}" and not status: unenrolled`,
perPage: SO_SEARCH_LIMIT,
});
setAgentsCount(data?.total || 0);
setAgentsCount(agents.data?.total ?? 0);
setIsLoadingAgentsCount(false);
};
@ -168,6 +166,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent<Props> = ({
) : agentsCount ? (
<EuiCallOut
color="danger"
iconType="warning"
title={i18n.translate(
'xpack.fleet.deleteAgentPolicy.confirmModal.affectedAgentsTitle',
{

View file

@ -387,6 +387,18 @@ describe('agent policy', () => {
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
});
});
it('should throw error if active agents are assigned to the policy', async () => {
(getAgentsByKuery as jest.Mock).mockResolvedValue({
agents: [],
total: 2,
page: 1,
perPage: 10,
});
await expect(agentPolicyService.delete(soClient, esClient, 'mocked')).rejects.toThrowError(
'Cannot delete an agent policy that is assigned to any active or inactive agents'
);
});
});
describe('bumpRevision', () => {

View file

@ -853,16 +853,18 @@ class AgentPolicyService {
if (agentPolicy.is_managed && !options?.force) {
throw new HostedAgentPolicyRestrictionRelatedError(`Cannot delete hosted agent policy ${id}`);
}
// Prevent deleting policy when assigned agents are inactive
const { total } = await getAgentsByKuery(esClient, soClient, {
showInactive: false,
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${id}`,
kuery: `${AGENTS_PREFIX}.policy_id:${id} and not status: unenrolled`,
});
if (total > 0) {
throw new FleetError('Cannot delete agent policy that is assigned to agent(s)');
throw new FleetError(
'Cannot delete an agent policy that is assigned to any active or inactive agents'
);
}
const packagePolicies = await packagePolicyService.findAllForAgentPolicy(soClient, id);

View file

@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { FLEET_AGENT_POLICIES_SCHEMA_VERSION } from '@kbn/fleet-plugin/server/constants';
import { skipIfNoDockerRegistry } from '../../helpers';
import { skipIfNoDockerRegistry, generateAgent } from '../../helpers';
import { setupFleetAndAgents } from '../agents/services';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
@ -1186,6 +1186,68 @@ export default function (providerContext: FtrProviderContext) {
name: 'Regular policy',
});
});
describe('Errors when trying to delete', () => {
it('should prevent policies having agents from being deleted', async () => {
const {
body: { item: policyWithAgents },
} = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Policy with agents',
namespace: 'default',
})
.expect(200);
await generateAgent(providerContext, 'healhty', 'agent-healthy-1', policyWithAgents.id);
const { body } = await supertest
.post('/api/fleet/agent_policies/delete')
.set('kbn-xsrf', 'xxx')
.send({ agentPolicyId: policyWithAgents.id })
.expect(400);
expect(body.message).to.contain(
'Cannot delete an agent policy that is assigned to any active or inactive agents'
);
await supertest
.delete(`/api/fleet/agents/agent-healthy-1`)
.set('kbn-xsrf', 'xx')
.expect(200);
});
it('should prevent policies having inactive agents from being deleted', async () => {
const {
body: { item: policyWithInactiveAgents },
} = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Policy with inactive agents',
namespace: 'default',
})
.expect(200);
await generateAgent(
providerContext,
'inactive',
'agent-inactive-1',
policyWithInactiveAgents.id
);
const { body } = await supertest
.post('/api/fleet/agent_policies/delete')
.set('kbn-xsrf', 'xxx')
.send({ agentPolicyId: policyWithInactiveAgents.id })
.expect(400);
expect(body.message).to.contain(
'Cannot delete an agent policy that is assigned to any active or inactive agents'
);
await supertest
.delete(`/api/fleet/agents/agent-inactive-1`)
.set('kbn-xsrf', 'xx')
.expect(200);
});
});
});
describe('POST /api/fleet/agent_policies/_bulk_get', () => {