From 55389b2f6cbcf5fe81bbf7d9247f3bbd0f6303d7 Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Wed, 17 Jul 2024 10:17:46 +0200 Subject: [PATCH 01/10] [Fleet] RBAC - Make agents write APIs space aware --- .../fleet/server/routes/agent/handlers.ts | 39 +++++++++---------- .../fleet/server/services/agents/namespace.ts | 20 ++++++++++ .../apis/space_awareness/agents.ts | 36 +++++++++++++++++ .../apis/space_awareness/api_helper.ts | 17 ++++++++ 4 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/agents/namespace.ts diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 6f1ac12edb163..2ef2cf0ef9648 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -8,7 +8,6 @@ import { uniq } from 'lodash'; import { type RequestHandler, SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; -import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import type { GetAgentsResponse, @@ -21,7 +20,6 @@ import type { GetAgentUploadsResponse, PostAgentReassignResponse, PostRetrieveAgentsByActionsResponse, - Agent, } from '../../../common/types'; import type { GetAgentsRequestSchema, @@ -45,18 +43,7 @@ import { defaultFleetErrorHandler, FleetNotFoundError } from '../../errors'; import * as AgentService from '../../services/agents'; import { fetchAndAssignAgentMetrics } from '../../services/agents/agent_metrics'; import { getAgentStatusForAgentPolicy } from '../../services/agents'; - -export function verifyNamespace(agent: Agent, currentNamespace?: string) { - const isInNamespace = - (currentNamespace && agent.namespaces?.includes(currentNamespace)) || - (!currentNamespace && - (!agent.namespaces || - agent.namespaces.length === 0 || - agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING))); - if (!isInNamespace) { - throw new FleetNotFoundError(`${agent.id} not found in namespace`); - } -} +import { isAgentInNamespace } from '../../services/agents/namespace'; export const getAgentHandler: FleetRequestHandler< TypeOf, @@ -68,7 +55,9 @@ export const getAgentHandler: FleetRequestHandler< let agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); + if (!isAgentInNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace())) { + throw new FleetNotFoundError(`${agent.id} not found in namespace`); + } if (request.query.withMetrics) { agent = (await fetchAndAssignAgentMetrics(esClientCurrentUser, [agent]))[0]; @@ -90,12 +79,17 @@ export const getAgentHandler: FleetRequestHandler< } }; -export const deleteAgentHandler: RequestHandler< +export const deleteAgentHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { + const [coreContext, fleetContext] = await Promise.all([context.core, context.fleet]); + const esClient = coreContext.elasticsearch.client.asInternalUser; + try { - const coreContext = await context.core; - const esClient = coreContext.elasticsearch.client.asInternalUser; + const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); + if (!isAgentInNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace())) { + throw new FleetNotFoundError(`${agent.id} not found in namespace`); + } await AgentService.deleteAgent(esClient, request.params.agentId); @@ -116,12 +110,12 @@ export const deleteAgentHandler: RequestHandler< } }; -export const updateAgentHandler: RequestHandler< +export const updateAgentHandler: FleetRequestHandler< TypeOf, undefined, TypeOf > = async (context, request, response) => { - const coreContext = await context.core; + const [coreContext, fleetContext] = await Promise.all([context.core, context.fleet]); const esClient = coreContext.elasticsearch.client.asInternalUser; const soClient = coreContext.savedObjects.client; @@ -134,6 +128,11 @@ export const updateAgentHandler: RequestHandler< } try { + const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); + if (!isAgentInNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace())) { + throw new FleetNotFoundError(`${agent.id} not found in namespace`); + } + await AgentService.updateAgent(esClient, request.params.agentId, partialAgent); const body = { item: await AgentService.getAgentById(esClient, soClient, request.params.agentId), diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.ts b/x-pack/plugins/fleet/server/services/agents/namespace.ts new file mode 100644 index 0000000000000..13231604ab272 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/namespace.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; + +import type { Agent } from '../../types'; + +export function isAgentInNamespace(agent: Agent, namespace?: string) { + return ( + (namespace && agent.namespaces?.includes(namespace)) || + (!namespace && + (!agent.namespaces || + agent.namespaces.length === 0 || + agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING))) + ); +} diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts index 73c2675d7b2d5..511347337bb86 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts @@ -132,5 +132,41 @@ export default function (providerContext: FtrProviderContext) { expect(err?.message).to.match(/404 "Not Found"/); }); }); + + describe('PUT /agents/{id}', () => { + it('should allow to update an agent in the same space', async () => { + await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }, TEST_SPACE_1); + }); + + it('should not allow to update an agent from a different space from the default space', async () => { + let err: Error | undefined; + try { + await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + }); + + describe('DELETE /agents/{id}', () => { + it('should allow to delete an agent in the same space', async () => { + await apiClient.deleteAgent(testSpaceAgent1, TEST_SPACE_1); + }); + + it('should not allow to delete an agent from a different space from the default space', async () => { + let err: Error | undefined; + try { + await apiClient.deleteAgent(testSpaceAgent1); + } catch (_err) { + err = _err; + } + + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index 633d34cfa2d15..5e63cd0154727 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -173,6 +173,23 @@ export class SpaceTestApiClient { return res; } + async updateAgent(agentId: string, data: any, spaceId?: string) { + const { body: res } = await this.supertest + .put(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + + return res; + } + async deleteAgent(agentId: string, spaceId?: string) { + const { body: res } = await this.supertest + .delete(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + return res; + } // Enrollment Settings async getEnrollmentSettings(spaceId?: string): Promise { const { body: res } = await this.supertest From 828a0afbf0e06c8bebbbaf712ccf3026a4b33535 Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Thu, 18 Jul 2024 17:37:41 +0200 Subject: [PATCH 02/10] Add bulk update agent tags endpoint --- .../fleet/server/services/agents/namespace.ts | 9 ++ .../services/agents/update_agent_tags.test.ts | 114 ++++++++++++++++++ .../services/agents/update_agent_tags.ts | 9 +- 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.ts b/x-pack/plugins/fleet/server/services/agents/namespace.ts index 13231604ab272..611a8e9f67fe0 100644 --- a/x-pack/plugins/fleet/server/services/agents/namespace.ts +++ b/x-pack/plugins/fleet/server/services/agents/namespace.ts @@ -18,3 +18,12 @@ export function isAgentInNamespace(agent: Agent, namespace?: string) { agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING))) ); } + +export function agentsKueryNamespaceFilter(namespace?: string) { + if (!namespace) { + return; + } + return namespace === DEFAULT_NAMESPACE_STRING + ? `namespaces:(${DEFAULT_NAMESPACE_STRING}) or not namespaces:*` + : `namespaces:(${namespace})`; +} diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index b118b8ff03ac8..a92c865910b5e 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -374,4 +374,118 @@ describe('update_agent_tags', () => { }) ); }); + + it('should update tags for agents in the space', async () => { + soClient.getCurrentNamespace.mockReturnValue('default'); + esClient.search.mockResolvedValue({ + hits: { + hits: [ + { + _id: 'agent1', + _source: { + tags: ['one', 'two', 'three'], + namespaces: ['default'], + }, + fields: { + status: 'online', + }, + }, + ], + }, + } as any); + + await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], ['two']); + + expect(esClient.updateByQuery).toHaveBeenCalledWith( + expect.objectContaining({ + conflicts: 'proceed', + index: '.fleet-agents', + query: { + terms: { _id: ['agent1'] }, + }, + script: expect.objectContaining({ + lang: 'painless', + params: expect.objectContaining({ + tagsToAdd: ['one'], + tagsToRemove: ['two'], + updatedAt: expect.anything(), + }), + source: expect.anything(), + }), + }) + ); + }); + + it('should not update tags for agents in another space', async () => { + soClient.getCurrentNamespace.mockReturnValue('default'); + esClient.search.mockResolvedValue({ + hits: { + hits: [ + { + _id: 'agent1', + _source: { + tags: ['one', 'two', 'three'], + namespaces: ['myspace'], + }, + fields: { + status: 'online', + }, + }, + ], + }, + } as any); + + await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], ['two']); + + expect(esClient.updateByQuery).not.toHaveBeenCalled(); + }); + + it('should add namespace filter to kuery in the default space', async () => { + soClient.getCurrentNamespace.mockReturnValue('default'); + + await updateAgentTags( + soClient, + esClient, + { kuery: 'status:healthy OR status:offline' }, + [], + ['remove'] + ); + + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 10000, + kuery: + '(namespaces:(default) or not namespaces:*) AND (status:healthy OR status:offline) AND (tags:remove)', + tagsToAdd: [], + tagsToRemove: ['remove'], + }), + expect.anything() + ); + }); + + it('should add namespace filter to kuery in a custom space', async () => { + soClient.getCurrentNamespace.mockReturnValue('myspace'); + + await updateAgentTags( + soClient, + esClient, + { kuery: 'status:healthy OR status:offline' }, + [], + ['remove'] + ); + + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 10000, + kuery: '(namespaces:(myspace)) AND (status:healthy OR status:offline) AND (tags:remove)', + tagsToAdd: [], + tagsToRemove: ['remove'], + }), + expect.anything() + ); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts index 5e335bfd41996..e4a7ca8f0148d 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.ts @@ -15,6 +15,7 @@ import { SO_SEARCH_LIMIT } from '../../constants'; import { getAgentsById, getAgentsByKuery, openPointInTime } from './crud'; import type { GetAgentsOptions } from '.'; import { UpdateAgentTagsActionRunner, updateTagsBatch } from './update_agent_tags_action_runner'; +import { agentsKueryNamespaceFilter, isAgentInNamespace } from './namespace'; export async function updateAgentTags( soClient: SavedObjectsClientContract, @@ -25,6 +26,7 @@ export async function updateAgentTags( ): Promise<{ actionId: string }> { const outgoingErrors: Record = {}; const givenAgents: Agent[] = []; + const currentNameSpace = soClient.getCurrentNamespace(); if ('agentIds' in options) { const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds); @@ -33,6 +35,10 @@ export async function updateAgentTags( outgoingErrors[maybeAgent.id] = new AgentReassignmentError( `Cannot find agent ${maybeAgent.id}` ); + } else if (!isAgentInNamespace(maybeAgent, currentNameSpace)) { + outgoingErrors[maybeAgent.id] = new AgentReassignmentError( + `Agent ${maybeAgent.id} is not in the current space` + ); } else { givenAgents.push(maybeAgent); } @@ -40,7 +46,8 @@ export async function updateAgentTags( } else if ('kuery' in options) { const batchSize = options.batchSize ?? SO_SEARCH_LIMIT; - const filters = []; + const namespaceFilter = agentsKueryNamespaceFilter(currentNameSpace); + const filters = namespaceFilter ? [namespaceFilter] : []; if (options.kuery !== '') { filters.push(options.kuery); } From 95cd473c02d5e25548e689dbf0ddc368ecc3d3a5 Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Thu, 18 Jul 2024 19:22:46 +0200 Subject: [PATCH 03/10] Add API integration test for bulk update tags --- .../apis/space_awareness/agents.ts | 80 ++++++++++++++++++- .../apis/space_awareness/api_helper.ts | 9 +++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts index 511347337bb86..f924acfb2f9aa 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common'; +import { CreateAgentPolicyResponse, GetAgentsResponse } from '@kbn/fleet-plugin/common'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; @@ -168,5 +168,83 @@ export default function (providerContext: FtrProviderContext) { expect(err?.message).to.match(/404 "Not Found"/); }); }); + + describe('POST /agents/bulkUpdateAgentTags', () => { + function getAgentTags(agents: GetAgentsResponse) { + return agents.items?.reduce((acc, item) => { + acc[item.id] = item.tags; + return acc; + }, {} as any); + } + + it('should only update tags of agents in the same space when passing a list of agent ids', async () => { + let agents = await apiClient.getAgents(TEST_SPACE_1); + let agentTags = getAgentTags(agents); + expect(agentTags[testSpaceAgent1]).to.eql(['tag1']); + expect(agentTags[testSpaceAgent2]).to.eql(['tag1']); + // Add tag + await apiClient.bulkUpdateAgentTags( + { + agents: [defaultSpaceAgent1, testSpaceAgent1], + tagsToAdd: ['space1'], + }, + TEST_SPACE_1 + ); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentTags = getAgentTags(agents); + expect(agentTags[testSpaceAgent1]).to.eql(['tag1', 'space1']); + expect(agentTags[testSpaceAgent2]).to.eql(['tag1']); + // Reset tags + await apiClient.bulkUpdateAgentTags( + { + agents: [testSpaceAgent1], + tagsToRemove: ['space1'], + }, + TEST_SPACE_1 + ); + agents = await apiClient.getAgents(TEST_SPACE_1); + agentTags = getAgentTags(agents); + expect(agentTags[testSpaceAgent1]).to.eql(['tag1']); + }); + + it('should only update tags of agents in the same space when passing a kuery', async () => { + let agentsInDefaultSpace = await apiClient.getAgents(); + let agentInDefaultSpaceTags = getAgentTags(agentsInDefaultSpace); + let agentsInTestSpace = await apiClient.getAgents(TEST_SPACE_1); + let agentInTestSpaceTags = getAgentTags(agentsInTestSpace); + expect(agentInDefaultSpaceTags[defaultSpaceAgent1]).to.eql(['tag1']); + expect(agentInDefaultSpaceTags[defaultSpaceAgent2]).to.eql(['tag1']); + expect(agentInTestSpaceTags[testSpaceAgent1]).to.eql(['tag1']); + expect(agentInTestSpaceTags[testSpaceAgent2]).to.eql(['tag1']); + // Add tag + await apiClient.bulkUpdateAgentTags( + { + agents: '', + tagsToAdd: ['space1'], + }, + TEST_SPACE_1 + ); + agentsInDefaultSpace = await apiClient.getAgents(); + agentInDefaultSpaceTags = getAgentTags(agentsInDefaultSpace); + agentsInTestSpace = await apiClient.getAgents(TEST_SPACE_1); + agentInTestSpaceTags = getAgentTags(agentsInTestSpace); + expect(agentInDefaultSpaceTags[defaultSpaceAgent1]).to.eql(['tag1']); + expect(agentInDefaultSpaceTags[defaultSpaceAgent2]).to.eql(['tag1']); + expect(agentInTestSpaceTags[testSpaceAgent1]).to.eql(['tag1', 'space1']); + expect(agentInTestSpaceTags[testSpaceAgent2]).to.eql(['tag1', 'space1']); + // Reset tags + await apiClient.bulkUpdateAgentTags( + { + agents: '', + tagsToRemove: ['space1'], + }, + TEST_SPACE_1 + ); + agentsInTestSpace = await apiClient.getAgents(TEST_SPACE_1); + agentInTestSpaceTags = getAgentTags(agentsInTestSpace); + expect(agentInTestSpaceTags[testSpaceAgent1]).to.eql(['tag1']); + expect(agentInTestSpaceTags[testSpaceAgent2]).to.eql(['tag1']); + }); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index 5e63cd0154727..c98324aba9e1c 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -190,6 +190,15 @@ export class SpaceTestApiClient { return res; } + async bulkUpdateAgentTags(data: any, spaceId?: string) { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/bulk_update_agent_tags`) + .set('kbn-xsrf', 'xxxx') + .send(data) + .expect(200); + + return res; + } // Enrollment Settings async getEnrollmentSettings(spaceId?: string): Promise { const { body: res } = await this.supertest From 6c11b4225d0345971ef7a772bd36feb362d7c326 Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Fri, 19 Jul 2024 11:06:15 +0200 Subject: [PATCH 04/10] Add unit tests for agent namespace helpers --- .../server/services/agents/namespace.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 x-pack/plugins/fleet/server/services/agents/namespace.test.ts diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.test.ts b/x-pack/plugins/fleet/server/services/agents/namespace.test.ts new file mode 100644 index 0000000000000..7030ea91b17ef --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/namespace.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Agent } from '../../types'; + +import { agentsKueryNamespaceFilter, isAgentInNamespace } from './namespace'; + +describe('isAgentInNamespace', () => { + describe('when the namespace is defined', () => { + it('returns true if the agent namespaces include the namespace', () => { + const agent = { id: '123', namespaces: ['default', 'space1'] } as Agent; + expect(isAgentInNamespace(agent, 'space1')).toEqual(true); + }); + + it('returns false if the agent namespaces do not include the namespace', () => { + const agent = { id: '123', namespaces: ['default', 'space1'] } as Agent; + expect(isAgentInNamespace(agent, 'space2')).toEqual(false); + }); + + it('returns false if the agent has zero length namespaces', () => { + const agent = { id: '123', namespaces: [] as string[] } as Agent; + expect(isAgentInNamespace(agent, 'space1')).toEqual(false); + }); + + it('returns false if the agent does not have namespaces', () => { + const agent = { id: '123' } as Agent; + expect(isAgentInNamespace(agent, 'space1')).toEqual(false); + }); + }); + + describe('when the namespace is undefined', () => { + it('returns true if the agent does not have namespaces', () => { + const agent = { id: '123' } as Agent; + expect(isAgentInNamespace(agent)).toEqual(true); + }); + + it('returns true if the agent has zero length namespaces', () => { + const agent = { id: '123', namespaces: [] as string[] } as Agent; + expect(isAgentInNamespace(agent)).toEqual(true); + }); + + it('returns true if the agent namespaces include the default one', () => { + const agent = { id: '123', namespaces: ['default'] } as Agent; + expect(isAgentInNamespace(agent)).toEqual(true); + }); + + it('returns false if the agent namespaces include the default one', () => { + const agent = { id: '123', namespaces: ['space1'] } as Agent; + expect(isAgentInNamespace(agent)).toEqual(false); + }); + }); +}); + +describe('agentsKueryNamespaceFilter', () => { + it('returns undefined if the namespace is undefined', () => { + expect(agentsKueryNamespaceFilter()).toBeUndefined(); + }); + + it('returns a kuery for the default space', () => { + expect(agentsKueryNamespaceFilter('default')).toEqual( + 'namespaces:(default) or not namespaces:*' + ); + }); + + it('returns a kuery for custom spaces', () => { + expect(agentsKueryNamespaceFilter('space1')).toEqual('namespaces:(space1)'); + }); +}); From 5d0061430d5f0f2e6602f968dc7110b71bedbf3d Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Mon, 22 Jul 2024 16:54:15 +0200 Subject: [PATCH 05/10] Add namespace property to actions and action results --- x-pack/plugins/fleet/common/types/models/agent.ts | 3 +++ x-pack/plugins/fleet/server/services/agents/actions.ts | 3 +++ .../services/agents/update_agent_tags_action_runner.ts | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 2add5750f1855..2fd8438cdea33 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -61,6 +61,7 @@ export interface NewAgentAction { ack_data?: any; sent_at?: string; agents: string[]; + namespace?: string; created_at?: string; id?: string; expiration?: string; @@ -408,6 +409,8 @@ export interface FleetServerAgentAction { */ agents?: string[]; + namespace?: string; + /** * Date when the agent should execute that agent. This field could be altered by Fleet server for progressive rollout of the action. */ diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index bda3090f25d0e..829888e2e423d 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -52,6 +52,7 @@ export async function createAgentAction( ? undefined : newAgentAction.expiration ?? new Date(now + ONE_MONTH_IN_MS).toISOString(), agents: newAgentAction.agents, + namespace: newAgentAction.namespace, action_id: actionId, data: newAgentAction.data, type: newAgentAction.type, @@ -182,6 +183,7 @@ export async function bulkCreateAgentActionResults( results: Array<{ actionId: string; agentId: string; + namespace?: string; error?: string; }> ): Promise { @@ -194,6 +196,7 @@ export async function bulkCreateAgentActionResults( '@timestamp': new Date().toISOString(), action_id: result.actionId, agent_id: result.agentId, + namespace: result.namespace, error: result.error, }; diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts index 0a8232d6e5715..9cf80f7c06213 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -149,6 +149,8 @@ export async function updateTagsBatch( const versionConflictCount = res.version_conflicts ?? 0; const versionConflictIds = isLastRetry ? getUuidArray(versionConflictCount) : []; + const currentNameSpace = soClient.getCurrentNamespace(); + // creating an action doc so that update tags shows up in activity // the logic only saves agent count in the action that updated, failed or in case of last retry, conflicted // this ensures that the action status count will be accurate @@ -157,6 +159,7 @@ export async function updateTagsBatch( agents: updatedIds .concat(failures.map((failure) => failure.id)) .concat(isLastRetry ? versionConflictIds : []), + namespace: currentNameSpace, created_at: new Date().toISOString(), type: 'UPDATE_TAGS', total: options.total ?? res.total, @@ -176,6 +179,7 @@ export async function updateTagsBatch( updatedIds.map((id) => ({ agentId: id, actionId, + namespace: currentNameSpace, })) ); appContextService.getLogger().debug(`action updated result wrote on ${updatedCount} agents`); @@ -188,6 +192,7 @@ export async function updateTagsBatch( failures.map((failure) => ({ agentId: failure.id, actionId, + namespace: currentNameSpace, error: failure.cause.reason, })) ); @@ -202,6 +207,7 @@ export async function updateTagsBatch( versionConflictIds.map((id) => ({ agentId: id, actionId, + namespace: currentNameSpace, error: 'version conflict on last retry', })) ); From 6a87d0933d0312303b31170b72e187c45a7fd9bf Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Tue, 23 Jul 2024 11:30:32 +0200 Subject: [PATCH 06/10] Change namespace to namespaces --- x-pack/plugins/fleet/common/types/models/agent.ts | 4 ++-- x-pack/plugins/fleet/server/services/agents/actions.ts | 6 +++--- .../services/agents/update_agent_tags_action_runner.ts | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 2fd8438cdea33..cefe1d035cba2 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -61,7 +61,7 @@ export interface NewAgentAction { ack_data?: any; sent_at?: string; agents: string[]; - namespace?: string; + namespaces?: string[]; created_at?: string; id?: string; expiration?: string; @@ -409,7 +409,7 @@ export interface FleetServerAgentAction { */ agents?: string[]; - namespace?: string; + namespaces?: string[]; /** * Date when the agent should execute that agent. This field could be altered by Fleet server for progressive rollout of the action. diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index 829888e2e423d..f344aa24e59dd 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -52,7 +52,7 @@ export async function createAgentAction( ? undefined : newAgentAction.expiration ?? new Date(now + ONE_MONTH_IN_MS).toISOString(), agents: newAgentAction.agents, - namespace: newAgentAction.namespace, + namespaces: newAgentAction.namespaces, action_id: actionId, data: newAgentAction.data, type: newAgentAction.type, @@ -183,7 +183,7 @@ export async function bulkCreateAgentActionResults( results: Array<{ actionId: string; agentId: string; - namespace?: string; + namespaces?: string[]; error?: string; }> ): Promise { @@ -196,7 +196,7 @@ export async function bulkCreateAgentActionResults( '@timestamp': new Date().toISOString(), action_id: result.actionId, agent_id: result.agentId, - namespace: result.namespace, + namespaces: result.namespaces, error: result.error, }; diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts index 9cf80f7c06213..44fef8d4a6c2b 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags_action_runner.ts @@ -150,6 +150,7 @@ export async function updateTagsBatch( const versionConflictIds = isLastRetry ? getUuidArray(versionConflictCount) : []; const currentNameSpace = soClient.getCurrentNamespace(); + const namespaces = currentNameSpace ? [currentNameSpace] : []; // creating an action doc so that update tags shows up in activity // the logic only saves agent count in the action that updated, failed or in case of last retry, conflicted @@ -159,7 +160,7 @@ export async function updateTagsBatch( agents: updatedIds .concat(failures.map((failure) => failure.id)) .concat(isLastRetry ? versionConflictIds : []), - namespace: currentNameSpace, + namespaces, created_at: new Date().toISOString(), type: 'UPDATE_TAGS', total: options.total ?? res.total, @@ -179,7 +180,7 @@ export async function updateTagsBatch( updatedIds.map((id) => ({ agentId: id, actionId, - namespace: currentNameSpace, + namespaces, })) ); appContextService.getLogger().debug(`action updated result wrote on ${updatedCount} agents`); From 82d815808b5612c7d31a9a06c6e20744ddbb16e8 Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Tue, 23 Jul 2024 11:58:12 +0200 Subject: [PATCH 07/10] Fix integration tests --- .../test/fleet_api_integration/apis/space_awareness/agents.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts index f924acfb2f9aa..0a4a1035083e6 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/agents.ts @@ -136,6 +136,7 @@ export default function (providerContext: FtrProviderContext) { describe('PUT /agents/{id}', () => { it('should allow to update an agent in the same space', async () => { await apiClient.updateAgent(testSpaceAgent1, { tags: ['foo'] }, TEST_SPACE_1); + await apiClient.updateAgent(testSpaceAgent1, { tags: ['tag1'] }, TEST_SPACE_1); }); it('should not allow to update an agent from a different space from the default space', async () => { @@ -153,7 +154,8 @@ export default function (providerContext: FtrProviderContext) { describe('DELETE /agents/{id}', () => { it('should allow to delete an agent in the same space', async () => { - await apiClient.deleteAgent(testSpaceAgent1, TEST_SPACE_1); + const testSpaceAgent3 = await createFleetAgent(spaceTest1Policy2.item.id, TEST_SPACE_1); + await apiClient.deleteAgent(testSpaceAgent3, TEST_SPACE_1); }); it('should not allow to delete an agent from a different space from the default space', async () => { From ff98d7af3ca21b6c217fdd89319b4300070e078d Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Tue, 23 Jul 2024 16:42:41 +0200 Subject: [PATCH 08/10] Put behind feature flag --- .../plugins/fleet/server/services/agents/namespace.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.ts b/x-pack/plugins/fleet/server/services/agents/namespace.ts index 611a8e9f67fe0..28cecdf22e30b 100644 --- a/x-pack/plugins/fleet/server/services/agents/namespace.ts +++ b/x-pack/plugins/fleet/server/services/agents/namespace.ts @@ -7,9 +7,16 @@ import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; +import { appContextService } from '../app_context'; + import type { Agent } from '../../types'; export function isAgentInNamespace(agent: Agent, namespace?: string) { + const useSpaceAwareness = appContextService.getExperimentalFeatures()?.useSpaceAwareness; + if (!useSpaceAwareness) { + return true; + } + return ( (namespace && agent.namespaces?.includes(namespace)) || (!namespace && @@ -20,7 +27,8 @@ export function isAgentInNamespace(agent: Agent, namespace?: string) { } export function agentsKueryNamespaceFilter(namespace?: string) { - if (!namespace) { + const useSpaceAwareness = appContextService.getExperimentalFeatures()?.useSpaceAwareness; + if (!useSpaceAwareness || !namespace) { return; } return namespace === DEFAULT_NAMESPACE_STRING From 2e3e2cb7a175a722ff81d4411566af1149b21301 Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Wed, 24 Jul 2024 09:32:36 +0200 Subject: [PATCH 09/10] Fix unit tests --- .../server/services/agents/namespace.test.ts | 123 +++++++++++----- .../services/agents/update_agent_tags.test.ts | 138 ++++++++++-------- 2 files changed, 159 insertions(+), 102 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.test.ts b/x-pack/plugins/fleet/server/services/agents/namespace.test.ts index 7030ea91b17ef..b5cda85cc45e4 100644 --- a/x-pack/plugins/fleet/server/services/agents/namespace.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/namespace.test.ts @@ -5,68 +5,115 @@ * 2.0. */ +import { appContextService } from '../app_context'; + import type { Agent } from '../../types'; import { agentsKueryNamespaceFilter, isAgentInNamespace } from './namespace'; +jest.mock('../app_context'); + +const mockedAppContextService = appContextService as jest.Mocked; + describe('isAgentInNamespace', () => { - describe('when the namespace is defined', () => { - it('returns true if the agent namespaces include the namespace', () => { - const agent = { id: '123', namespaces: ['default', 'space1'] } as Agent; - expect(isAgentInNamespace(agent, 'space1')).toEqual(true); + describe('with the useSpaceAwareness feature flag disabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: false, + } as any); }); - it('returns false if the agent namespaces do not include the namespace', () => { + it('returns true even if the agent is in a different space', () => { const agent = { id: '123', namespaces: ['default', 'space1'] } as Agent; - expect(isAgentInNamespace(agent, 'space2')).toEqual(false); + expect(isAgentInNamespace(agent, 'space2')).toEqual(true); }); + }); - it('returns false if the agent has zero length namespaces', () => { - const agent = { id: '123', namespaces: [] as string[] } as Agent; - expect(isAgentInNamespace(agent, 'space1')).toEqual(false); + describe('with the useSpaceAwareness feature flag enabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: true, + } as any); }); - it('returns false if the agent does not have namespaces', () => { - const agent = { id: '123' } as Agent; - expect(isAgentInNamespace(agent, 'space1')).toEqual(false); - }); - }); + describe('when the namespace is defined', () => { + it('returns true if the agent namespaces include the namespace', () => { + const agent = { id: '123', namespaces: ['default', 'space1'] } as Agent; + expect(isAgentInNamespace(agent, 'space1')).toEqual(true); + }); - describe('when the namespace is undefined', () => { - it('returns true if the agent does not have namespaces', () => { - const agent = { id: '123' } as Agent; - expect(isAgentInNamespace(agent)).toEqual(true); - }); + it('returns false if the agent namespaces do not include the namespace', () => { + const agent = { id: '123', namespaces: ['default', 'space1'] } as Agent; + expect(isAgentInNamespace(agent, 'space2')).toEqual(false); + }); - it('returns true if the agent has zero length namespaces', () => { - const agent = { id: '123', namespaces: [] as string[] } as Agent; - expect(isAgentInNamespace(agent)).toEqual(true); - }); + it('returns false if the agent has zero length namespaces', () => { + const agent = { id: '123', namespaces: [] as string[] } as Agent; + expect(isAgentInNamespace(agent, 'space1')).toEqual(false); + }); - it('returns true if the agent namespaces include the default one', () => { - const agent = { id: '123', namespaces: ['default'] } as Agent; - expect(isAgentInNamespace(agent)).toEqual(true); + it('returns false if the agent does not have namespaces', () => { + const agent = { id: '123' } as Agent; + expect(isAgentInNamespace(agent, 'space1')).toEqual(false); + }); }); - it('returns false if the agent namespaces include the default one', () => { - const agent = { id: '123', namespaces: ['space1'] } as Agent; - expect(isAgentInNamespace(agent)).toEqual(false); + describe('when the namespace is undefined', () => { + it('returns true if the agent does not have namespaces', () => { + const agent = { id: '123' } as Agent; + expect(isAgentInNamespace(agent)).toEqual(true); + }); + + it('returns true if the agent has zero length namespaces', () => { + const agent = { id: '123', namespaces: [] as string[] } as Agent; + expect(isAgentInNamespace(agent)).toEqual(true); + }); + + it('returns true if the agent namespaces include the default one', () => { + const agent = { id: '123', namespaces: ['default'] } as Agent; + expect(isAgentInNamespace(agent)).toEqual(true); + }); + + it('returns false if the agent namespaces include the default one', () => { + const agent = { id: '123', namespaces: ['space1'] } as Agent; + expect(isAgentInNamespace(agent)).toEqual(false); + }); }); }); }); describe('agentsKueryNamespaceFilter', () => { - it('returns undefined if the namespace is undefined', () => { - expect(agentsKueryNamespaceFilter()).toBeUndefined(); - }); + describe('with the useSpaceAwareness feature flag disabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: false, + } as any); + }); - it('returns a kuery for the default space', () => { - expect(agentsKueryNamespaceFilter('default')).toEqual( - 'namespaces:(default) or not namespaces:*' - ); + it('returns undefined if the useSpaceAwareness feature flag disabled', () => { + expect(agentsKueryNamespaceFilter('space1')).toBeUndefined(); + }); }); - it('returns a kuery for custom spaces', () => { - expect(agentsKueryNamespaceFilter('space1')).toEqual('namespaces:(space1)'); + describe('with the useSpaceAwareness feature flag enabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: true, + } as any); + }); + + it('returns undefined if the namespace is undefined', () => { + expect(agentsKueryNamespaceFilter()).toBeUndefined(); + }); + + it('returns a kuery for the default space', () => { + expect(agentsKueryNamespaceFilter('default')).toEqual( + 'namespaces:(default) or not namespaces:*' + ); + }); + + it('returns a kuery for custom spaces', () => { + expect(agentsKueryNamespaceFilter('space1')).toEqual('namespaces:(space1)'); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts index a92c865910b5e..35163288e97dc 100644 --- a/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/update_agent_tags.test.ts @@ -8,6 +8,8 @@ import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { appContextService } from '../app_context'; + import type { Agent } from '../../types'; import { createClientMock } from './action.mock'; @@ -416,76 +418,84 @@ describe('update_agent_tags', () => { ); }); - it('should not update tags for agents in another space', async () => { - soClient.getCurrentNamespace.mockReturnValue('default'); - esClient.search.mockResolvedValue({ - hits: { - hits: [ - { - _id: 'agent1', - _source: { - tags: ['one', 'two', 'three'], - namespaces: ['myspace'], - }, - fields: { - status: 'online', - }, - }, - ], - }, - } as any); - - await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], ['two']); + describe('with the useSpaceAwareness feature flag enabled', () => { + beforeEach(() => { + jest.mocked(appContextService.getExperimentalFeatures).mockReturnValue({ + useSpaceAwareness: true, + } as any); + }); - expect(esClient.updateByQuery).not.toHaveBeenCalled(); - }); + it('should not update tags for agents in another space', async () => { + soClient.getCurrentNamespace.mockReturnValue('default'); + esClient.search.mockResolvedValue({ + hits: { + hits: [ + { + _id: 'agent1', + _source: { + tags: ['one', 'two', 'three'], + namespaces: ['myspace'], + }, + fields: { + status: 'online', + }, + }, + ], + }, + } as any); - it('should add namespace filter to kuery in the default space', async () => { - soClient.getCurrentNamespace.mockReturnValue('default'); + await updateAgentTags(soClient, esClient, { agentIds: ['agent1'] }, ['one'], ['two']); - await updateAgentTags( - soClient, - esClient, - { kuery: 'status:healthy OR status:offline' }, - [], - ['remove'] - ); + expect(esClient.updateByQuery).not.toHaveBeenCalled(); + }); - expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - batchSize: 10000, - kuery: - '(namespaces:(default) or not namespaces:*) AND (status:healthy OR status:offline) AND (tags:remove)', - tagsToAdd: [], - tagsToRemove: ['remove'], - }), - expect.anything() - ); - }); + it('should add namespace filter to kuery in the default space', async () => { + soClient.getCurrentNamespace.mockReturnValue('default'); - it('should add namespace filter to kuery in a custom space', async () => { - soClient.getCurrentNamespace.mockReturnValue('myspace'); + await updateAgentTags( + soClient, + esClient, + { kuery: 'status:healthy OR status:offline' }, + [], + ['remove'] + ); + + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 10000, + kuery: + '(namespaces:(default) or not namespaces:*) AND (status:healthy OR status:offline) AND (tags:remove)', + tagsToAdd: [], + tagsToRemove: ['remove'], + }), + expect.anything() + ); + }); - await updateAgentTags( - soClient, - esClient, - { kuery: 'status:healthy OR status:offline' }, - [], - ['remove'] - ); + it('should add namespace filter to kuery in a custom space', async () => { + soClient.getCurrentNamespace.mockReturnValue('myspace'); - expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - batchSize: 10000, - kuery: '(namespaces:(myspace)) AND (status:healthy OR status:offline) AND (tags:remove)', - tagsToAdd: [], - tagsToRemove: ['remove'], - }), - expect.anything() - ); + await updateAgentTags( + soClient, + esClient, + { kuery: 'status:healthy OR status:offline' }, + [], + ['remove'] + ); + + expect(UpdateAgentTagsActionRunner).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + batchSize: 10000, + kuery: '(namespaces:(myspace)) AND (status:healthy OR status:offline) AND (tags:remove)', + tagsToAdd: [], + tagsToRemove: ['remove'], + }), + expect.anything() + ); + }); }); }); From 616d3bf4343e5e42cc3788835b9a216a0688a61d Mon Sep 17 00:00:00 2001 From: jillguyonnet Date: Wed, 24 Jul 2024 16:08:18 +0200 Subject: [PATCH 10/10] Add small utility function --- .../fleet/server/routes/agent/handlers.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 2ef2cf0ef9648..ff5b6a3c8523b 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -20,6 +20,7 @@ import type { GetAgentUploadsResponse, PostAgentReassignResponse, PostRetrieveAgentsByActionsResponse, + Agent, } from '../../../common/types'; import type { GetAgentsRequestSchema, @@ -45,6 +46,12 @@ import { fetchAndAssignAgentMetrics } from '../../services/agents/agent_metrics' import { getAgentStatusForAgentPolicy } from '../../services/agents'; import { isAgentInNamespace } from '../../services/agents/namespace'; +function verifyNamespace(agent: Agent, namespace?: string) { + if (!isAgentInNamespace(agent, namespace)) { + throw new FleetNotFoundError(`${agent.id} not found in namespace`); + } +} + export const getAgentHandler: FleetRequestHandler< TypeOf, TypeOf @@ -54,10 +61,7 @@ export const getAgentHandler: FleetRequestHandler< const esClientCurrentUser = coreContext.elasticsearch.client.asCurrentUser; let agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - - if (!isAgentInNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace())) { - throw new FleetNotFoundError(`${agent.id} not found in namespace`); - } + verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); if (request.query.withMetrics) { agent = (await fetchAndAssignAgentMetrics(esClientCurrentUser, [agent]))[0]; @@ -87,9 +91,7 @@ export const deleteAgentHandler: FleetRequestHandler< try { const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - if (!isAgentInNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace())) { - throw new FleetNotFoundError(`${agent.id} not found in namespace`); - } + verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); await AgentService.deleteAgent(esClient, request.params.agentId); @@ -129,9 +131,7 @@ export const updateAgentHandler: FleetRequestHandler< try { const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - if (!isAgentInNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace())) { - throw new FleetNotFoundError(`${agent.id} not found in namespace`); - } + verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); await AgentService.updateAgent(esClient, request.params.agentId, partialAgent); const body = {