diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index 4969500eb05df..9b5bbdccb3540 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -17,6 +17,7 @@ import type { import type { ActionsService } from '../../services/agents'; import type { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; import { defaultFleetErrorHandler } from '../../errors'; +import { getCurrentNamespace } from '../../services/spaces/get_current_namespace'; export const postNewAgentActionHandlerBuilder = function ( actionsService: ActionsService @@ -39,6 +40,7 @@ export const postNewAgentActionHandlerBuilder = function ( created_at: new Date().toISOString(), ...newAgentAction, agents: [agent.id], + namespaces: [getCurrentNamespace(soClient)], }); const body: PostNewAgentActionResponse = { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index ff5b6a3c8523b..8ff3f82b7e6c6 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -44,7 +44,8 @@ import { defaultFleetErrorHandler, FleetNotFoundError } from '../../errors'; import * as AgentService from '../../services/agents'; import { fetchAndAssignAgentMetrics } from '../../services/agents/agent_metrics'; import { getAgentStatusForAgentPolicy } from '../../services/agents'; -import { isAgentInNamespace } from '../../services/agents/namespace'; +import { isAgentInNamespace } from '../../services/spaces/agent_namespaces'; +import { getCurrentNamespace } from '../../services/spaces/get_current_namespace'; function verifyNamespace(agent: Agent, namespace?: string) { if (!isAgentInNamespace(agent, namespace)) { @@ -61,7 +62,7 @@ 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()); + verifyNamespace(agent, getCurrentNamespace(coreContext.savedObjects.client)); if (request.query.withMetrics) { agent = (await fetchAndAssignAgentMetrics(esClientCurrentUser, [agent]))[0]; @@ -91,7 +92,7 @@ export const deleteAgentHandler: FleetRequestHandler< try { const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); + verifyNamespace(agent, getCurrentNamespace(coreContext.savedObjects.client)); await AgentService.deleteAgent(esClient, request.params.agentId); @@ -131,7 +132,7 @@ export const updateAgentHandler: FleetRequestHandler< try { const agent = await fleetContext.agentClient.asCurrentUser.getAgent(request.params.agentId); - verifyNamespace(agent, coreContext.savedObjects.client.getCurrentNamespace()); + verifyNamespace(agent, getCurrentNamespace(soClient)); await AgentService.updateAgent(esClient, request.params.agentId, partialAgent); const body = { @@ -387,7 +388,11 @@ export const getActionStatusHandler: RequestHandler< const esClient = coreContext.elasticsearch.client.asInternalUser; try { - const actionStatuses = await AgentService.getActionStatuses(esClient, request.query); + const actionStatuses = await AgentService.getActionStatuses( + esClient, + request.query, + getCurrentNamespace(coreContext.savedObjects.client) + ); const body: GetActionStatusResponse = { items: actionStatuses }; return response.ok({ body }); } catch (error) { diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index 016a731d1c67e..4edea93176de4 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -7,7 +7,6 @@ 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 { GetEnrollmentAPIKeysRequestSchema, @@ -25,6 +24,7 @@ import * as APIKeyService from '../../services/api_keys'; import { agentPolicyService } from '../../services/agent_policy'; import { defaultFleetErrorHandler, AgentPolicyNotFoundError } from '../../errors'; import { appContextService } from '../../services'; +import { getCurrentNamespace } from '../../services/spaces/get_current_namespace'; export const getEnrollmentApiKeysHandler: RequestHandler< undefined, @@ -40,9 +40,7 @@ export const getEnrollmentApiKeysHandler: RequestHandler< page: request.query.page, perPage: request.query.perPage, kuery: request.query.kuery, - spaceId: useSpaceAwareness - ? soClient.getCurrentNamespace() ?? DEFAULT_NAMESPACE_STRING - : undefined, + spaceId: useSpaceAwareness ? getCurrentNamespace(soClient) : undefined, }); const body: GetEnrollmentAPIKeysResponse = { list: items, // deprecated @@ -96,8 +94,7 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler< const { useSpaceAwareness } = appContextService.getExperimentalFeatures(); const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; - const currentNamespace = - coreContext.savedObjects.client.getCurrentNamespace() ?? DEFAULT_NAMESPACE_STRING; + const currentNamespace = getCurrentNamespace(coreContext.savedObjects.client); await APIKeyService.deleteEnrollmentApiKey( esClient, request.params.keyId, @@ -126,8 +123,7 @@ export const getOneEnrollmentApiKeyHandler: RequestHandler< try { const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; - const currentNamespace = - coreContext.savedObjects.client.getCurrentNamespace() ?? DEFAULT_NAMESPACE_STRING; + const currentNamespace = getCurrentNamespace(coreContext.savedObjects.client); const { useSpaceAwareness } = appContextService.getExperimentalFeatures(); const apiKey = await APIKeyService.getEnrollmentAPIKey( diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts index 1435857ffe670..f2ef2b70ce978 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_status.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts @@ -23,6 +23,7 @@ import { AGENT_POLICY_INDEX, } from '../../../common'; import { appContextService } from '..'; +import { addNamespaceFilteringToQuery } from '../spaces/query_namespaces_filtering'; /** * Return current bulk actions. @@ -35,10 +36,11 @@ import { appContextService } from '..'; */ export async function getActionStatuses( esClient: ElasticsearchClient, - options: ActionStatusOptions + options: ActionStatusOptions, + namespace?: string ): Promise { - const actionResults = await getActionResults(esClient, options); - const policyChangeActions = await getPolicyChangeActions(esClient, options); + const actionResults = await getActionResults(esClient, options, namespace); + const policyChangeActions = await getPolicyChangeActions(esClient, options, namespace); const actionStatuses = [...actionResults, ...policyChangeActions] .sort((a: ActionStatus, b: ActionStatus) => (b.creationTime > a.creationTime ? 1 : -1)) .slice(getPage(options), getPerPage(options)); @@ -47,9 +49,10 @@ export async function getActionStatuses( async function getActionResults( esClient: ElasticsearchClient, - options: ActionStatusOptions + options: ActionStatusOptions, + namespace?: string ): Promise { - const actions = await getActions(esClient, options); + const actions = await getActions(esClient, options, namespace); const cancelledActions = await getCancelledActions(esClient); let acks: any; @@ -200,41 +203,41 @@ export function getPerPage(options: ActionStatusOptions) { async function getActions( esClient: ElasticsearchClient, - options: ActionStatusOptions + options: ActionStatusOptions, + namespace?: string ): Promise { + const query = { + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + ...(options.date || options.latest + ? { + filter: [ + { + range: { + '@timestamp': { + // options.date overrides options.latest + gte: options.date ?? `now-${(options.latest ?? 0) / 1000}s/s`, + lte: options.date ? moment(options.date).add(1, 'days').toISOString() : 'now/s', + }, + }, + }, + ], + } + : {}), + }, + }; const res = await esClient.search({ index: AGENT_ACTIONS_INDEX, ignore_unavailable: true, from: 0, size: getPerPage(options), - query: { - bool: { - must_not: [ - { - term: { - type: 'CANCEL', - }, - }, - ], - ...(options.date || options.latest - ? { - filter: [ - { - range: { - '@timestamp': { - // options.date overrides options.latest - gte: options.date ?? `now-${(options.latest ?? 0) / 1000}s/s`, - lte: options.date - ? moment(options.date).add(1, 'days').toISOString() - : 'now/s', - }, - }, - }, - ], - } - : {}), - }, - }, + query: addNamespaceFilteringToQuery(query, namespace), body: { sort: [{ '@timestamp': 'desc' }], }, @@ -340,49 +343,51 @@ export const hasRolloutPeriodPassed = (source: FleetServerAgentAction) => async function getPolicyChangeActions( esClient: ElasticsearchClient, - options: ActionStatusOptions + options: ActionStatusOptions, + namespace?: string ): Promise { // option.latest is used to fetch recent errors, which policy change actions do not contain if (options.latest) { return []; } - const agentPoliciesRes = await esClient.search({ - index: AGENT_POLICY_INDEX, - size: getPerPage(options), - query: { - bool: { - filter: [ - { - range: { - revision_idx: { - gt: 1, - }, + const query = { + bool: { + filter: [ + { + range: { + revision_idx: { + gt: 1, }, }, - // This filter is for retrieving docs created by Kibana, as opposed to Fleet Server (coordinator_idx: 1). - // Note: the coordinator will be removed from Fleet Server (https://github.com/elastic/fleet-server/pull/3131), - // so this filter will eventually not be needed. - { - term: { - coordinator_idx: 0, - }, + }, + // This filter is for retrieving docs created by Kibana, as opposed to Fleet Server (coordinator_idx: 1). + // Note: the coordinator will be removed from Fleet Server (https://github.com/elastic/fleet-server/pull/3131), + // so this filter will eventually not be needed. + { + term: { + coordinator_idx: 0, }, - ...(options.date - ? [ - { - range: { - '@timestamp': { - gte: options.date, - lte: moment(options.date).add(1, 'days').toISOString(), - }, + }, + ...(options.date + ? [ + { + range: { + '@timestamp': { + gte: options.date, + lte: moment(options.date).add(1, 'days').toISOString(), }, }, - ] - : []), - ], - }, + }, + ] + : []), + ], }, + }; + const agentPoliciesRes = await esClient.search({ + index: AGENT_POLICY_INDEX, + size: getPerPage(options), + query: addNamespaceFilteringToQuery(query, namespace), sort: [ { '@timestamp': { diff --git a/x-pack/plugins/fleet/server/services/agents/agent_service.ts b/x-pack/plugins/fleet/server/services/agents/agent_service.ts index 9af97cc307c08..c6eb4e55ed8fe 100644 --- a/x-pack/plugins/fleet/server/services/agents/agent_service.ts +++ b/x-pack/plugins/fleet/server/services/agents/agent_service.ts @@ -19,14 +19,14 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { SortResults } from '@elastic/elasticsearch/lib/api/types'; -import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; - import type { AgentStatus, ListWithKuery } from '../../types'; import type { Agent, GetAgentStatusResponse } from '../../../common/types'; import { getAuthzFromRequest } from '../security'; import { appContextService } from '../app_context'; import { FleetUnauthorizedError } from '../../errors'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + import { getAgentsByKuery, getAgentById } from './crud'; import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status'; import { getLatestAvailableAgentVersion } from './versions'; @@ -183,7 +183,7 @@ export class AgentServiceImpl implements AgentService { this.internalEsClient, soClient, preflightCheck, - soClient.getCurrentNamespace() ?? DEFAULT_NAMESPACE_STRING + getCurrentNamespace(soClient) ); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 368d5c0df834e..64c20cbbc4d6b 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -26,11 +26,11 @@ import { AgentNotFoundError, FleetUnauthorizedError, } from '../../errors'; - import { auditLoggingService } from '../audit_logging'; +import { isAgentInNamespace } from '../spaces/agent_namespaces'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers'; - import { buildAgentStatusRuntimeField } from './build_status_runtime_field'; import { getLatestAvailableAgentVersion } from './versions'; @@ -406,6 +406,10 @@ export async function getAgentById( throw new AgentNotFoundError(`Agent ${agentId} not found`); } + if (!isAgentInNamespace(agentHit, getCurrentNamespace(soClient))) { + throw new AgentNotFoundError(`${agentHit.id} not found in namespace`); + } + return agentHit; } 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 e4a7ca8f0148d..f3443458249b7 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 @@ -12,10 +12,13 @@ import { AgentReassignmentError } from '../../errors'; import { SO_SEARCH_LIMIT } from '../../constants'; +import { agentsKueryNamespaceFilter, isAgentInNamespace } from '../spaces/agent_namespaces'; + +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + 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, @@ -26,7 +29,7 @@ export async function updateAgentTags( ): Promise<{ actionId: string }> { const outgoingErrors: Record = {}; const givenAgents: Agent[] = []; - const currentNameSpace = soClient.getCurrentNamespace(); + const currentNameSpace = getCurrentNamespace(soClient); if ('agentIds' in options) { const maybeAgents = await getAgentsById(esClient, soClient, options.agentIds); 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 44fef8d4a6c2b..8b68e1b6e9fd8 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 @@ -17,6 +17,8 @@ import { appContextService } from '../app_context'; import { FleetError } from '../../errors'; +import { getCurrentNamespace } from '../spaces/get_current_namespace'; + import { ActionRunner } from './action_runner'; import { BulkActionTaskType } from './bulk_action_types'; @@ -149,8 +151,8 @@ export async function updateTagsBatch( const versionConflictCount = res.version_conflicts ?? 0; const versionConflictIds = isLastRetry ? getUuidArray(versionConflictCount) : []; - const currentNameSpace = soClient.getCurrentNamespace(); - const namespaces = currentNameSpace ? [currentNameSpace] : []; + const currentNameSpace = getCurrentNamespace(soClient); + const namespaces = currentNameSpace ? { namespaces: [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 @@ -160,7 +162,7 @@ export async function updateTagsBatch( agents: updatedIds .concat(failures.map((failure) => failure.id)) .concat(isLastRetry ? versionConflictIds : []), - namespaces, + ...namespaces, created_at: new Date().toISOString(), type: 'UPDATE_TAGS', total: options.total ?? res.total, @@ -180,7 +182,7 @@ export async function updateTagsBatch( updatedIds.map((id) => ({ agentId: id, actionId, - namespaces, + ...namespaces, })) ); appContextService.getLogger().debug(`action updated result wrote on ${updatedCount} agents`); diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.test.ts b/x-pack/plugins/fleet/server/services/spaces/agent_namespaces.test.ts similarity index 82% rename from x-pack/plugins/fleet/server/services/agents/namespace.test.ts rename to x-pack/plugins/fleet/server/services/spaces/agent_namespaces.test.ts index b5cda85cc45e4..3cf070ab8fea3 100644 --- a/x-pack/plugins/fleet/server/services/agents/namespace.test.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_namespaces.test.ts @@ -9,7 +9,7 @@ import { appContextService } from '../app_context'; import type { Agent } from '../../types'; -import { agentsKueryNamespaceFilter, isAgentInNamespace } from './namespace'; +import { agentsKueryNamespaceFilter, isAgentInNamespace } from './agent_namespaces'; jest.mock('../app_context'); @@ -37,22 +37,32 @@ describe('isAgentInNamespace', () => { }); describe('when the namespace is defined', () => { - it('returns true if the agent namespaces include the namespace', () => { + it('returns true in a custom space 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', () => { + it('returns false in a custom space 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', () => { + it('returns true in the default space if the agent has zero length namespaces', () => { + const agent = { id: '123', namespaces: [] as string[] } as Agent; + expect(isAgentInNamespace(agent, 'default')).toEqual(true); + }); + + it('returns false in a custom space 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', () => { + it('returns true in the default space if the agent does not have namespaces', () => { + const agent = { id: '123' } as Agent; + expect(isAgentInNamespace(agent, 'default')).toEqual(true); + }); + + it('returns false in a custom space if the agent does not have namespaces', () => { const agent = { id: '123' } as Agent; expect(isAgentInNamespace(agent, 'space1')).toEqual(false); }); diff --git a/x-pack/plugins/fleet/server/services/agents/namespace.ts b/x-pack/plugins/fleet/server/services/spaces/agent_namespaces.ts similarity index 67% rename from x-pack/plugins/fleet/server/services/agents/namespace.ts rename to x-pack/plugins/fleet/server/services/spaces/agent_namespaces.ts index 28cecdf22e30b..1a1834635662b 100644 --- a/x-pack/plugins/fleet/server/services/agents/namespace.ts +++ b/x-pack/plugins/fleet/server/services/spaces/agent_namespaces.ts @@ -17,12 +17,17 @@ export function isAgentInNamespace(agent: Agent, namespace?: string) { return true; } + // In a custom space, only return true if the agent is explicitly in that space. + if (namespace && namespace !== DEFAULT_NAMESPACE_STRING) { + return agent.namespaces?.includes(namespace) ?? false; + } + + // In the default space OR in if the current namespace is not defined, + // return true if the agent is explicitly in the default space OR if it has no defined namespaces. return ( - (namespace && agent.namespaces?.includes(namespace)) || - (!namespace && - (!agent.namespaces || - agent.namespaces.length === 0 || - agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING))) + !agent.namespaces || + agent.namespaces.length === 0 || + agent.namespaces?.includes(DEFAULT_NAMESPACE_STRING) ); } diff --git a/x-pack/plugins/fleet/server/services/spaces/get_current_namespace.ts b/x-pack/plugins/fleet/server/services/spaces/get_current_namespace.ts new file mode 100644 index 0000000000000..534e21ad44ae8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/get_current_namespace.ts @@ -0,0 +1,17 @@ +/* + * 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 { SavedObjectsClientContract } from '@kbn/core/server'; + +/* + * soClient.getCurrentNamespace() returns undefined in the default space. + * This helper returns the name of the current space and 'default' in the default space. + */ +export function getCurrentNamespace(soClient: SavedObjectsClientContract) { + return soClient.getCurrentNamespace() ?? DEFAULT_NAMESPACE_STRING; +} diff --git a/x-pack/plugins/fleet/server/services/spaces/query_namespaces_filtering.test.ts b/x-pack/plugins/fleet/server/services/spaces/query_namespaces_filtering.test.ts new file mode 100644 index 0000000000000..43713597e364a --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/query_namespaces_filtering.test.ts @@ -0,0 +1,296 @@ +/* + * 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 '..'; + +import { addNamespaceFilteringToQuery } from './query_namespaces_filtering'; + +const mockedAppContextService = appContextService as jest.Mocked; + +jest.mock('../app_context'); + +describe('addNamespaceFilteringToQuery', () => { + const baseActionQuery = { + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + }, + }; + + const baseActionQueryWithFilter = { + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + filter: [ + { + range: { + '@timestamp': { + gte: 'now-3600s/s', + lte: 'now/s', + }, + }, + }, + ], + }, + }; + + const basePolicyQuery = { + bool: { + filter: [ + { + range: { + revision_idx: { + gt: 1, + }, + }, + }, + { + term: { + coordinator_idx: 0, + }, + }, + ], + }, + }; + + describe('with the useSpaceAwareness feature flag disabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: false, + } as any); + }); + + it('should return the same query', () => { + expect(addNamespaceFilteringToQuery(baseActionQuery, 'mySpace')).toEqual(baseActionQuery); + }); + }); + + describe('with the useSpaceAwareness feature flag enabled', () => { + beforeEach(() => { + mockedAppContextService.getExperimentalFeatures.mockReturnValue({ + useSpaceAwareness: true, + } as any); + }); + + it('should return the same query if the current namespace is undefined', () => { + expect(addNamespaceFilteringToQuery(baseActionQuery)).toEqual(baseActionQuery); + }); + + it('should add a filter to the base action query in a custom space', () => { + expect(addNamespaceFilteringToQuery(baseActionQuery, 'mySpace')).toEqual({ + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + filter: [ + { + terms: { + namespaces: ['mySpace'], + }, + }, + ], + }, + }); + }); + + it('should add a filter to the base action query in a custom space if there is already filtering', () => { + expect(addNamespaceFilteringToQuery(baseActionQueryWithFilter, 'mySpace')).toEqual({ + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + filter: [ + { + range: { + '@timestamp': { + gte: 'now-3600s/s', + lte: 'now/s', + }, + }, + }, + { + terms: { + namespaces: ['mySpace'], + }, + }, + ], + }, + }); + }); + + it('should add a filter to the base policy query in a custom space', () => { + expect(addNamespaceFilteringToQuery(basePolicyQuery, 'mySpace')).toEqual({ + bool: { + filter: [ + { + range: { + revision_idx: { + gt: 1, + }, + }, + }, + { + term: { + coordinator_idx: 0, + }, + }, + { + terms: { + namespaces: ['mySpace'], + }, + }, + ], + }, + }); + }); + + it('should add a filter to the base action query in the default space', () => { + expect(addNamespaceFilteringToQuery(baseActionQuery, 'default')).toEqual({ + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + filter: [ + { + bool: { + should: [ + { + terms: { + namespaces: ['default'], + }, + }, + { + bool: { + must_not: [ + { + exists: { + field: 'namespaces', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }); + }); + + it('should add a filter to the base action query in the default space if there is already filtering', () => { + expect(addNamespaceFilteringToQuery(baseActionQueryWithFilter, 'default')).toEqual({ + bool: { + must_not: [ + { + term: { + type: 'CANCEL', + }, + }, + ], + filter: [ + { + range: { + '@timestamp': { + gte: 'now-3600s/s', + lte: 'now/s', + }, + }, + }, + { + bool: { + should: [ + { + terms: { + namespaces: ['default'], + }, + }, + { + bool: { + must_not: [ + { + exists: { + field: 'namespaces', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }); + }); + + it('should add a filter to the base policy query in the default space', () => { + expect(addNamespaceFilteringToQuery(basePolicyQuery, 'default')).toEqual({ + bool: { + filter: [ + { + range: { + revision_idx: { + gt: 1, + }, + }, + }, + { + term: { + coordinator_idx: 0, + }, + }, + { + bool: { + should: [ + { + terms: { + namespaces: ['default'], + }, + }, + { + bool: { + must_not: [ + { + exists: { + field: 'namespaces', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/spaces/query_namespaces_filtering.ts b/x-pack/plugins/fleet/server/services/spaces/query_namespaces_filtering.ts new file mode 100644 index 0000000000000..a2c233cbcc21a --- /dev/null +++ b/x-pack/plugins/fleet/server/services/spaces/query_namespaces_filtering.ts @@ -0,0 +1,56 @@ +/* + * 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 '..'; + +export function addNamespaceFilteringToQuery(query: any, namespace?: string) { + const useSpaceAwareness = appContextService.getExperimentalFeatures()?.useSpaceAwareness; + if (!useSpaceAwareness || !namespace) { + return query; + } + + // In the default space, return documents with namespaces: ['default'] OR with no namespaces property. + // In custom spaces, return documents with namespaces: ['custom_space']. + const filter = + namespace === DEFAULT_NAMESPACE_STRING + ? { + bool: { + should: [ + { + terms: { + namespaces: ['default'], + }, + }, + { + bool: { + must_not: [ + { + exists: { + field: 'namespaces', + }, + }, + ], + }, + }, + ], + }, + } + : { + terms: { + namespaces: [namespace], + }, + }; + + return { + bool: { + ...query.bool, + filter: query.bool.filter ? [...query.bool.filter, filter] : [filter], + }, + }; +} diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts new file mode 100644 index 0000000000000..1b9e193bb622a --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/actions.ts @@ -0,0 +1,254 @@ +/* + * 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 expect from '@kbn/expect'; +import { AGENT_POLICY_INDEX, CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { SpaceTestApiClient } from './api_helper'; +import { cleanFleetActionIndices, cleanFleetIndices, createFleetAgent } from './helpers'; +import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esClient = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('actions', async function () { + skipIfNoDockerRegistry(providerContext); + const apiClient = new SpaceTestApiClient(supertest); + + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + }); + + beforeEach(async () => { + await cleanFleetActionIndices(esClient); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + }); + + setupTestSpaces(providerContext); + + let defaultSpacePolicy1: CreateAgentPolicyResponse; + let spaceTest1Policy1: CreateAgentPolicyResponse; + let spaceTest1Policy2: CreateAgentPolicyResponse; + + let defaultSpaceAgent1: string; + let defaultSpaceAgent2: string; + let testSpaceAgent1: string; + let testSpaceAgent2: string; + + before(async () => { + const [_defaultSpacePolicy1, _spaceTest1Policy1, _spaceTest1Policy2] = await Promise.all([ + apiClient.createAgentPolicy(), + apiClient.createAgentPolicy(TEST_SPACE_1), + apiClient.createAgentPolicy(TEST_SPACE_1), + ]); + defaultSpacePolicy1 = _defaultSpacePolicy1; + spaceTest1Policy1 = _spaceTest1Policy1; + spaceTest1Policy2 = _spaceTest1Policy2; + + const [_defaultSpaceAgent1, _defaultSpaceAgent2, _testSpaceAgent1, _testSpaceAgent2] = + await Promise.all([ + createFleetAgent(esClient, defaultSpacePolicy1.item.id, 'default'), + createFleetAgent(esClient, defaultSpacePolicy1.item.id), + createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), + createFleetAgent(esClient, spaceTest1Policy2.item.id, TEST_SPACE_1), + ]); + defaultSpaceAgent1 = _defaultSpaceAgent1; + defaultSpaceAgent2 = _defaultSpaceAgent2; + testSpaceAgent1 = _testSpaceAgent1; + testSpaceAgent2 = _testSpaceAgent2; + }); + + describe('GET /agents/action_status', () => { + it('should return agent actions in the default space', async () => { + // Create UPDATE_TAGS action for agents in default space + await apiClient.bulkUpdateAgentTags({ + agents: [defaultSpaceAgent1, defaultSpaceAgent2], + tagsToAdd: ['tag1'], + }); + + const actionStatusInDefaultSpace = await apiClient.getActionStatus(); + expect(actionStatusInDefaultSpace.items.length).to.eql(1); + expect(actionStatusInDefaultSpace.items[0]).to.have.keys( + 'type', + 'status', + 'nbAgentsActionCreated', + 'nbAgentsAck', + 'nbAgentsActioned', + 'nbAgentsFailed', + 'latestErrors' + ); + expect(actionStatusInDefaultSpace.items[0].type).to.eql('UPDATE_TAGS'); + expect(actionStatusInDefaultSpace.items[0].nbAgentsActioned).to.eql(2); + expect(actionStatusInDefaultSpace.items[0].status).to.eql('COMPLETE'); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(0); + }); + + it('should return agent actions in a custom space', async () => { + // Create UPDATE_TAGS action for agents in custom space + await apiClient.bulkUpdateAgentTags( + { + agents: [testSpaceAgent1, testSpaceAgent2], + tagsToAdd: ['tag1'], + }, + TEST_SPACE_1 + ); + + const actionStatusInDefaultSpace = await apiClient.getActionStatus(); + expect(actionStatusInDefaultSpace.items.length).to.eql(0); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + expect(actionStatusInCustomSpace.items[0]).to.have.keys( + 'type', + 'status', + 'nbAgentsActionCreated', + 'nbAgentsAck', + 'nbAgentsActioned', + 'nbAgentsFailed', + 'latestErrors' + ); + expect(actionStatusInCustomSpace.items[0].type).to.eql('UPDATE_TAGS'); + expect(actionStatusInCustomSpace.items[0].nbAgentsActioned).to.eql(2); + expect(actionStatusInCustomSpace.items[0].status).to.eql('COMPLETE'); + }); + + it('should return agent policy actions in the default space', async () => { + // Index agent policy document with no namespaces + // TODO: can this be done by updating the agent policy using the API? The .fleet-policies index remains empty... + await esClient.index({ + refresh: 'wait_for', + index: AGENT_POLICY_INDEX, + document: { + revision_idx: 2, + policy_id: defaultSpacePolicy1.item.id, + coordinator_idx: 0, + '@timestamp': '2024-07-31T13:00:00.000Z', + }, + }); + + // Index agent policy document in the default space + // TODO: can this be done by updating the agent policy using the API? The .fleet-policies index remains empty... + await esClient.index({ + refresh: 'wait_for', + index: AGENT_POLICY_INDEX, + document: { + revision_idx: 2, + policy_id: defaultSpacePolicy1.item.id, + coordinator_idx: 0, + '@timestamp': '2024-07-31T13:00:00.000Z', + namespaces: ['default'], + }, + }); + + const actionStatusInDefaultSpace = await apiClient.getActionStatus(); + expect(actionStatusInDefaultSpace.items.length).to.eql(1); + expect(actionStatusInDefaultSpace.items[0]).to.have.keys( + 'type', + 'status', + 'nbAgentsActionCreated', + 'nbAgentsAck', + 'nbAgentsActioned', + 'nbAgentsFailed' + ); + expect(actionStatusInDefaultSpace.items[0].type).to.eql('POLICY_CHANGE'); + expect(actionStatusInDefaultSpace.items[0].nbAgentsActioned).to.eql(2); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(0); + }); + + it('should return agent policy actions in a custom space', async () => { + // Index agent policy document in a custom space + // TODO: can this be done by updating the agent policy using the API? The .fleet-policies index remains empty... + await esClient.index({ + refresh: 'wait_for', + index: AGENT_POLICY_INDEX, + document: { + revision_idx: 2, + policy_id: defaultSpacePolicy1.item.id, + coordinator_idx: 0, + '@timestamp': '2024-07-31T13:00:00.000Z', + namespaces: [TEST_SPACE_1], + }, + }); + + const actionStatusInDefaultSpace = await apiClient.getActionStatus(); + expect(actionStatusInDefaultSpace.items.length).to.eql(0); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + expect(actionStatusInCustomSpace.items[0]).to.have.keys( + 'type', + 'status', + 'nbAgentsActionCreated', + 'nbAgentsAck', + 'nbAgentsActioned', + 'nbAgentsFailed' + ); + expect(actionStatusInCustomSpace.items[0].type).to.eql('POLICY_CHANGE'); + expect(actionStatusInCustomSpace.items[0].nbAgentsActioned).to.eql(2); + }); + }); + + describe('POST /agents/{agentId}/actions', () => { + it('should return 404 if the agent is not in the current space', async () => { + const resInDefaultSpace = await supertest + .post(`/api/fleet/agents/${testSpaceAgent1}/actions`) + .set('kbn-xsrf', 'xxxx') + .send({ action: { type: 'UNENROLL' } }) + .expect(404); + expect(resInDefaultSpace.body.message).to.eql(`${testSpaceAgent1} not found in namespace`); + + const resInCustomSpace = await supertest + .post(`/s/${TEST_SPACE_1}/api/fleet/agents/${defaultSpaceAgent1}/actions`) + .set('kbn-xsrf', 'xxxx') + .send({ action: { type: 'UNENROLL' } }) + .expect(404); + expect(resInCustomSpace.body.message).to.eql( + `${defaultSpaceAgent1} not found in namespace` + ); + }); + + it('should create an action with set namespace in the default space', async () => { + await apiClient.postNewAgentAction(defaultSpaceAgent1); + + const actionStatusInDefaultSpace = await apiClient.getActionStatus(); + expect(actionStatusInDefaultSpace.items.length).to.eql(1); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(0); + }); + + it('should create an action with set namespace in a custom space', async () => { + await apiClient.postNewAgentAction(testSpaceAgent1, TEST_SPACE_1); + + const actionStatusInDefaultSpace = await apiClient.getActionStatus(); + expect(actionStatusInDefaultSpace.items.length).to.eql(0); + + const actionStatusInCustomSpace = await apiClient.getActionStatus(TEST_SPACE_1); + expect(actionStatusInCustomSpace.items.length).to.eql(1); + }); + }); + }); +} 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 0a4a1035083e6..047d32a854511 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 @@ -10,7 +10,7 @@ import { CreateAgentPolicyResponse, GetAgentsResponse } from '@kbn/fleet-plugin/ import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; -import { cleanFleetIndices } from './helpers'; +import { cleanFleetIndices, createFleetAgent } from './helpers'; import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; export default function (providerContext: FtrProviderContext) { @@ -18,31 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - const createFleetAgent = async (agentPolicyId: string, spaceId?: string) => { - const agentResponse = await esClient.index({ - index: '.fleet-agents', - refresh: true, - body: { - access_api_key_id: 'api-key-3', - active: true, - policy_id: agentPolicyId, - policy_revision_idx: 1, - last_checkin_status: 'online', - type: 'PERMANENT', - local_metadata: { - host: { hostname: 'host123' }, - elastic: { agent: { version: '8.15.0' } }, - }, - user_provided_metadata: {}, - enrolled_at: new Date().toISOString(), - last_checkin: new Date().toISOString(), - tags: ['tag1'], - namespaces: spaceId ? [spaceId] : undefined, - }, - }); - return agentResponse._id; - }; describe('agents', async function () { skipIfNoDockerRegistry(providerContext); const apiClient = new SpaceTestApiClient(supertest); @@ -85,12 +61,11 @@ export default function (providerContext: FtrProviderContext) { const [_defaultSpaceAgent1, _defaultSpaceAgent2, _testSpaceAgent1, _testSpaceAgent2] = await Promise.all([ - createFleetAgent(defaultSpacePolicy1.item.id, 'default'), - createFleetAgent(defaultSpacePolicy1.item.id), - createFleetAgent(spaceTest1Policy1.item.id, TEST_SPACE_1), - createFleetAgent(spaceTest1Policy2.item.id, TEST_SPACE_1), + createFleetAgent(esClient, defaultSpacePolicy1.item.id, 'default'), + createFleetAgent(esClient, defaultSpacePolicy1.item.id), + createFleetAgent(esClient, spaceTest1Policy1.item.id, TEST_SPACE_1), + createFleetAgent(esClient, spaceTest1Policy2.item.id, TEST_SPACE_1), ]); - defaultSpaceAgent1 = _defaultSpaceAgent1; defaultSpaceAgent2 = _defaultSpaceAgent2; testSpaceAgent1 = _testSpaceAgent1; @@ -154,7 +129,11 @@ export default function (providerContext: FtrProviderContext) { 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); + const testSpaceAgent3 = await createFleetAgent( + esClient, + spaceTest1Policy2.item.id, + TEST_SPACE_1 + ); await apiClient.deleteAgent(testSpaceAgent3, TEST_SPACE_1); }); 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 963c1af66d1df..11fd693d9340b 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 @@ -25,6 +25,8 @@ import { GetInfoResponse, GetSpaceSettingsResponse, PutSpaceSettingsRequest, + GetActionStatusResponse, + PostNewAgentActionResponse, } from '@kbn/fleet-plugin/common/types'; import { GetUninstallTokenResponse, @@ -116,7 +118,7 @@ export class SpaceTestApiClient { return res; } - // Enrollmennt API Keys + // Enrollment API Keys async getEnrollmentApiKey( keyId: string, spaceId?: string @@ -302,4 +304,22 @@ export class SpaceTestApiClient { return res; } + // Actions + async getActionStatus(spaceId?: string): Promise { + const { body: res } = await this.supertest + .get(`${this.getBaseUrl(spaceId)}/api/fleet/agents/action_status`) + .expect(200); + + return res; + } + + async postNewAgentAction(agentId: string, spaceId?: string): Promise { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/agents/${agentId}/actions`) + .set('kbn-xsrf', 'xxxx') + .send({ action: { type: 'UNENROLL' } }) + .expect(200); + + return res; + } } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/enrollment_settings.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/enrollment_settings.ts index d5eb41afb2104..af648ec765971 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/enrollment_settings.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/enrollment_settings.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; -import { cleanFleetIndices } from './helpers'; +import { cleanFleetIndices, createFleetAgent } from './helpers'; import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; export default function (providerContext: FtrProviderContext) { @@ -17,31 +17,7 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - const createFleetAgent = async (agentPolicyId: string, spaceId?: string) => { - const agentResponse = await esClient.index({ - index: '.fleet-agents', - refresh: true, - body: { - access_api_key_id: 'api-key-3', - active: true, - policy_id: agentPolicyId, - policy_revision_idx: 1, - last_checkin_status: 'online', - type: 'PERMANENT', - local_metadata: { - host: { hostname: 'host123' }, - elastic: { agent: { version: '8.15.0' } }, - }, - user_provided_metadata: {}, - enrolled_at: new Date().toISOString(), - last_checkin: new Date().toISOString(), - tags: ['tag1'], - namespaces: spaceId ? [spaceId] : undefined, - }, - }); - return agentResponse._id; - }; describe('enrollment_settings', async function () { skipIfNoDockerRegistry(providerContext); const apiClient = new SpaceTestApiClient(supertest); @@ -104,7 +80,7 @@ export default function (providerContext: FtrProviderContext) { before(async () => { await apiClient.setup(); const testSpaceFleetServerPolicy = await apiClient.createFleetServerPolicy(TEST_SPACE_1); - await createFleetAgent(testSpaceFleetServerPolicy.item.id, TEST_SPACE_1); + await createFleetAgent(esClient, testSpaceFleetServerPolicy.item.id, TEST_SPACE_1); }); describe('GET /enrollments/settings', () => { @@ -142,7 +118,7 @@ export default function (providerContext: FtrProviderContext) { before(async () => { await apiClient.setup(); const defaultFleetServerPolicy = await apiClient.createFleetServerPolicy(); - await createFleetAgent(defaultFleetServerPolicy.item.id); + await createFleetAgent(esClient, defaultFleetServerPolicy.item.id); }); describe('GET /enrollments/settings', () => { diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts index fe731e323ce12..3a6f078844f60 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/helpers.ts @@ -7,19 +7,85 @@ import { Client } from '@elastic/elasticsearch'; +import { + AGENT_ACTIONS_INDEX, + AGENT_ACTIONS_RESULTS_INDEX, + AGENTS_INDEX, + AGENT_POLICY_INDEX, +} from '@kbn/fleet-plugin/common'; +import { ENROLLMENT_API_KEYS_INDEX } from '@kbn/fleet-plugin/common/constants'; + +const ES_INDEX_OPTIONS = { headers: { 'X-elastic-product-origin': 'fleet' } }; + export async function cleanFleetIndices(esClient: Client) { await Promise.all([ esClient.deleteByQuery({ - index: '.fleet-enrollment-api-keys', + index: ENROLLMENT_API_KEYS_INDEX, q: '*', ignore_unavailable: true, refresh: true, }), esClient.deleteByQuery({ - index: '.fleet-agents', + index: AGENTS_INDEX, q: '*', ignore_unavailable: true, refresh: true, }), ]); } + +export async function cleanFleetActionIndices(esClient: Client) { + try { + await Promise.all([ + esClient.deleteByQuery({ + index: AGENT_POLICY_INDEX, + q: '*', + }), + esClient.deleteByQuery({ + index: AGENT_ACTIONS_INDEX, + q: '*', + ignore_unavailable: true, + refresh: true, + }), + esClient.deleteByQuery( + { + index: AGENT_ACTIONS_RESULTS_INDEX, + q: '*', + }, + ES_INDEX_OPTIONS + ), + ]); + } catch (error) { + // swallowing error if does not exist + } +} + +export const createFleetAgent = async ( + esClient: Client, + agentPolicyId: string, + spaceId?: string +) => { + const agentResponse = await esClient.index({ + index: '.fleet-agents', + refresh: true, + body: { + access_api_key_id: 'api-key-3', + active: true, + policy_id: agentPolicyId, + policy_revision_idx: 1, + last_checkin_status: 'online', + type: 'PERMANENT', + local_metadata: { + host: { hostname: 'host123' }, + elastic: { agent: { version: '8.15.0' } }, + }, + user_provided_metadata: {}, + enrolled_at: new Date().toISOString(), + last_checkin: new Date().toISOString(), + tags: ['tag1'], + namespaces: spaceId ? [spaceId] : undefined, + }, + }); + + return agentResponse._id; +}; diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/index.js b/x-pack/test/fleet_api_integration/apis/space_awareness/index.js index fd9bab0e091f2..c684504372736 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/index.js +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/index.js @@ -14,5 +14,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./enrollment_settings')); loadTestFile(require.resolve('./package_install')); loadTestFile(require.resolve('./space_settings')); + loadTestFile(require.resolve('./actions')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts index 4052e9a8de488..0b20df56042ca 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; import { SpaceTestApiClient } from './api_helper'; -import { cleanFleetIndices } from './helpers'; +import { cleanFleetIndices, createFleetAgent } from './helpers'; import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; export default function (providerContext: FtrProviderContext) { @@ -21,31 +21,6 @@ export default function (providerContext: FtrProviderContext) { describe('package install', async function () { skipIfNoDockerRegistry(providerContext); const apiClient = new SpaceTestApiClient(supertest); - const createFleetAgent = async (agentPolicyId: string, spaceId?: string) => { - const agentResponse = await esClient.index({ - index: '.fleet-agents', - refresh: true, - body: { - access_api_key_id: 'api-key-3', - active: true, - policy_id: agentPolicyId, - policy_revision_idx: 1, - last_checkin_status: 'online', - type: 'PERMANENT', - local_metadata: { - host: { hostname: 'host123' }, - elastic: { agent: { version: '8.15.0' } }, - }, - user_provided_metadata: {}, - enrolled_at: new Date().toISOString(), - last_checkin: new Date().toISOString(), - tags: ['tag1'], - namespaces: spaceId ? [spaceId] : undefined, - }, - }); - - return agentResponse._id; - }; before(async () => { await kibanaServer.savedObjects.cleanStandardList(); @@ -275,7 +250,7 @@ export default function (providerContext: FtrProviderContext) { inputs: {}, }); - await createFleetAgent(agentPolicyRes.item.id); + await createFleetAgent(esClient, agentPolicyRes.item.id); }); it('should not allow to delete a package with active agents in the same space', async () => {