From 9e71c7e83014340e74185e2e54ea768d8dbf2d0d Mon Sep 17 00:00:00 2001 From: Jill Guyonnet Date: Thu, 25 Jul 2024 08:36:35 +0100 Subject: [PATCH] [Fleet] RBAC - Make agents write APIs space aware (#188507) ## Summary Relates to https://github.com/elastic/kibana/issues/185040 This PR makes the following Fleet agents API space aware: * `PUT /agents/{agentId}` * `DELETE /agents/{agentId}` * `POST /agents/bulk_update_agent_tags` Actions created from `POST /agents/bulk_update_agent_tags` have the `namespaces` property populated with the current space. I am opening this PR with a few endpoints to get early feedback and make this more agile. Other endpoints will be implemented in a followup PR. ### Testing 1. Enroll an agent in the default space. 2. Create a custom space and enroll an agent in it. 3. From the default space, test the `PUT /agents/{agentId}` and `DELETE /agents/{agentId}` endpoints and check that the request fails for the agent in the custom space. 4. Same test from the custom space. 5. From the default space, test the `POST /agents/bulk_update_agent_tags` with all agents ids and check that only the agents in the default space get updated. 6. Same test from the custom space. 7. Review the actions created from the bulk tag updates (the easiest way is `GET .fleet-actions/_search`) and ensure the `namespaces` property is correct. ### 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 --------- Co-authored-by: Elastic Machine --- .../fleet/common/types/models/agent.ts | 3 + .../fleet/server/routes/agent/handlers.ts | 33 ++--- .../fleet/server/services/agents/actions.ts | 3 + .../server/services/agents/namespace.test.ts | 119 +++++++++++++++++ .../fleet/server/services/agents/namespace.ts | 37 ++++++ .../services/agents/update_agent_tags.test.ts | 124 ++++++++++++++++++ .../services/agents/update_agent_tags.ts | 9 +- .../agents/update_agent_tags_action_runner.ts | 7 + .../apis/space_awareness/agents.ts | 118 ++++++++++++++++- .../apis/space_awareness/api_helper.ts | 26 ++++ 10 files changed, 458 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/agents/namespace.test.ts create mode 100644 x-pack/plugins/fleet/server/services/agents/namespace.ts diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 2add5750f1855..cefe1d035cba2 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[]; + namespaces?: string[]; created_at?: string; id?: string; expiration?: string; @@ -408,6 +409,8 @@ export interface FleetServerAgentAction { */ agents?: 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/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 52a0540c384fa..ff5b6a3c8523b 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, @@ -45,19 +44,10 @@ import { defaultFleetErrorHandler, FleetNotFoundError } from '../../errors'; import * as AgentService from '../../services/agents'; import { fetchAndAssignAgentMetrics } from '../../services/agents/agent_metrics'; import { getAgentStatusForAgentPolicy } from '../../services/agents'; -import { appContextService } from '../../services'; +import { isAgentInNamespace } from '../../services/agents/namespace'; -export function verifyNamespace(agent: Agent, currentNamespace?: string) { - if (!appContextService.getExperimentalFeatures().useSpaceAwareness) { - return; - } - const isInNamespace = - (currentNamespace && agent.namespaces?.includes(currentNamespace)) || - (!currentNamespace && - (!agent.namespaces || - agent.namespaces.length === 0 || - agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING))); - if (!isInNamespace) { +function verifyNamespace(agent: Agent, namespace?: string) { + if (!isAgentInNamespace(agent, namespace)) { throw new FleetNotFoundError(`${agent.id} not found in namespace`); } } @@ -71,7 +61,6 @@ export const getAgentHandler: FleetRequestHandler< const esClientCurrentUser = coreContext.elasticsearch.client.asCurrentUser; let agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); if (request.query.withMetrics) { @@ -94,12 +83,15 @@ 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); + verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); await AgentService.deleteAgent(esClient, request.params.agentId); @@ -120,12 +112,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; @@ -138,6 +130,9 @@ export const updateAgentHandler: RequestHandler< } try { + const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); + verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); + 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/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index bda3090f25d0e..f344aa24e59dd 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, + namespaces: newAgentAction.namespaces, action_id: actionId, data: newAgentAction.data, type: newAgentAction.type, @@ -182,6 +183,7 @@ export async function bulkCreateAgentActionResults( results: Array<{ actionId: string; agentId: string; + namespaces?: string[]; error?: string; }> ): Promise { @@ -194,6 +196,7 @@ export async function bulkCreateAgentActionResults( '@timestamp': new Date().toISOString(), action_id: result.actionId, agent_id: result.agentId, + namespaces: result.namespaces, error: result.error, }; 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..b5cda85cc45e4 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/namespace.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { 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('with the useSpaceAwareness feature flag disabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: false, + } as any); + }); + + 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(true); + }); + }); + + describe('with the useSpaceAwareness feature flag enabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: true, + } as any); + }); + + 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', () => { + describe('with the useSpaceAwareness feature flag disabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: false, + } as any); + }); + + it('returns undefined if the useSpaceAwareness feature flag disabled', () => { + expect(agentsKueryNamespaceFilter('space1')).toBeUndefined(); + }); + }); + + 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/namespace.ts b/x-pack/plugins/fleet/server/services/agents/namespace.ts new file mode 100644 index 0000000000000..28cecdf22e30b --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/namespace.ts @@ -0,0 +1,37 @@ +/* + * 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 { 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 && + (!agent.namespaces || + agent.namespaces.length === 0 || + agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING))) + ); +} + +export function agentsKueryNamespaceFilter(namespace?: string) { + const useSpaceAwareness = appContextService.getExperimentalFeatures()?.useSpaceAwareness; + if (!useSpaceAwareness || !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..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'; @@ -374,4 +376,126 @@ 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(), + }), + }) + ); + }); + + describe('with the useSpaceAwareness feature flag enabled', () => { + beforeEach(() => { + jest.mocked(appContextService.getExperimentalFeatures).mockReturnValue({ + useSpaceAwareness: true, + } as any); + }); + + 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); } 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..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 @@ -149,6 +149,9 @@ export async function updateTagsBatch( const versionConflictCount = res.version_conflicts ?? 0; 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 // this ensures that the action status count will be accurate @@ -157,6 +160,7 @@ export async function updateTagsBatch( agents: updatedIds .concat(failures.map((failure) => failure.id)) .concat(isLastRetry ? versionConflictIds : []), + namespaces, created_at: new Date().toISOString(), type: 'UPDATE_TAGS', total: options.total ?? res.total, @@ -176,6 +180,7 @@ export async function updateTagsBatch( updatedIds.map((id) => ({ agentId: id, actionId, + namespaces, })) ); appContextService.getLogger().debug(`action updated result wrote on ${updatedCount} agents`); @@ -188,6 +193,7 @@ export async function updateTagsBatch( failures.map((failure) => ({ agentId: failure.id, actionId, + namespace: currentNameSpace, error: failure.cause.reason, })) ); @@ -202,6 +208,7 @@ export async function updateTagsBatch( versionConflictIds.map((id) => ({ agentId: id, actionId, + namespace: currentNameSpace, error: 'version conflict on last retry', })) ); 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..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 @@ -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'; @@ -132,5 +132,121 @@ 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); + 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 () => { + 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 () => { + 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 () => { + 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"/); + }); + }); + + 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 b9e01ba70b11e..963c1af66d1df 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 @@ -187,6 +187,32 @@ 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; + } + 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