Skip to content

Commit

Permalink
[Fleet] RBAC - Make agents write APIs space aware (#188507)
Browse files Browse the repository at this point in the history
## Summary

Relates to #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 <elasticmachine@users.noreply.github.com>
  • Loading branch information
jillguyonnet and elasticmachine authored Jul 25, 2024
1 parent b7e66eb commit 9e71c7e
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 21 deletions.
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface NewAgentAction {
ack_data?: any;
sent_at?: string;
agents: string[];
namespaces?: string[];
created_at?: string;
id?: string;
expiration?: string;
Expand Down Expand Up @@ -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.
*/
Expand Down
33 changes: 14 additions & 19 deletions x-pack/plugins/fleet/server/routes/agent/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`);
}
}
Expand All @@ -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) {
Expand All @@ -94,12 +83,15 @@ export const getAgentHandler: FleetRequestHandler<
}
};

export const deleteAgentHandler: RequestHandler<
export const deleteAgentHandler: FleetRequestHandler<
TypeOf<typeof DeleteAgentRequestSchema.params>
> = 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);

Expand All @@ -120,12 +112,12 @@ export const deleteAgentHandler: RequestHandler<
}
};

export const updateAgentHandler: RequestHandler<
export const updateAgentHandler: FleetRequestHandler<
TypeOf<typeof UpdateAgentRequestSchema.params>,
undefined,
TypeOf<typeof UpdateAgentRequestSchema.body>
> = 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;

Expand All @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/server/services/agents/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -182,6 +183,7 @@ export async function bulkCreateAgentActionResults(
results: Array<{
actionId: string;
agentId: string;
namespaces?: string[];
error?: string;
}>
): Promise<void> {
Expand All @@ -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,
};

Expand Down
119 changes: 119 additions & 0 deletions x-pack/plugins/fleet/server/services/agents/namespace.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof appContextService>;

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)');
});
});
});
37 changes: 37 additions & 0 deletions x-pack/plugins/fleet/server/services/agents/namespace.ts
Original file line number Diff line number Diff line change
@@ -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})`;
}
Loading

0 comments on commit 9e71c7e

Please sign in to comment.