Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Managed Agent Policy #88688

Merged
merged 27 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dcaf8da
Add is_managed (default false) to SO
Jan 15, 2021
42d4848
is_managed SO prop defaults false. +some ftr tests
Jan 16, 2021
6187b12
Add is_managed in some tests to fix type errors
Jan 19, 2021
73414f9
Block unenrolling from a managed agent policy
Jan 19, 2021
a4c39c5
Cannot reassign agent enrolled in managed policy
Jan 19, 2021
d676705
Merge branch 'master' into 76843-managed-agent-policy
kibanamachine Jan 20, 2021
bb3923f
Add tests for AgentPolicyService create & update
Jan 21, 2021
ac10a47
API tests for agents (bulk_)unenroll endpoints
Jan 22, 2021
988b700
Add a method to get the agent policy for an agent
Jan 22, 2021
acb08cd
Merge branch 'master' into 76843-managed-agent-policy
Jan 25, 2021
a01e524
Add unsaved/missing esClient arguments after merging main
Jan 25, 2021
448f12d
Merge branch 'master' into 76843-managed-agent-policy
kibanamachine Jan 25, 2021
9936144
Don't throw if there's no associated agent policy
Jan 27, 2021
bbefa04
typo
Jan 27, 2021
8cddf6d
Tests for reassign
Jan 27, 2021
e19b0d6
Tests for reassign
Jan 27, 2021
445e746
Add migration for managed agent policy
Jan 28, 2021
e8554de
Merge branch '76843-managed-agent-policy' of github.com:jfsiii/kibana…
Jan 28, 2021
c77c851
Fix broken code in reassignment permissions check
Jan 29, 2021
f46437e
Merge branch 'master' into 76843-managed-agent-policy
kibanamachine Jan 29, 2021
92f888b
Fix broken test. Pass correct value
Jan 30, 2021
bd6b286
Merge branch '76843-managed-agent-policy' of github.com:jfsiii/kibana…
Jan 30, 2021
4a0d342
Merge branch 'master' into 76843-managed-agent-policy
kibanamachine Feb 1, 2021
86d6958
Add IngestManagerError and tests
Feb 2, 2021
a15a0d0
Merge branch 'master' into 76843-managed-agent-policy
kibanamachine Feb 2, 2021
b6b8ce0
Merge branch 'master' into 76843-managed-agent-policy
Feb 4, 2021
13994c7
Fix license header
Feb 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/constants/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const DEFAULT_AGENT_POLICY: Omit<
status: agentPolicyStatuses.Active,
package_policies: [],
is_default: true,
is_managed: false,
monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
};

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/types/models/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ export interface NewAgentPolicy {
namespace: string;
description?: string;
is_default?: boolean;
is_managed?: boolean; // Optional when creating a policy
monitoring_enabled?: Array<ValueOf<DataType>>;
}

export interface AgentPolicy extends NewAgentPolicy {
id: string;
status: ValueOf<AgentPolicyStatus>;
package_policies: string[] | PackagePolicy[];
is_managed: boolean; // required for created policy
updated_at: string;
updated_by: string;
revision: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
'e8a37031-2907-44f6-89d2-98bd493f60dc',
],
is_default: true,
is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 6,
updated_at: '2020-12-09T13:46:31.840Z',
Expand All @@ -701,6 +702,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
status: 'active',
package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'],
is_default: false,
is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 2,
updated_at: '2020-12-09T13:46:31.840Z',
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ export class PackageCacheError extends IngestManagerError {}
export class PackageOperationNotSupportedError extends IngestManagerError {}
export class FleetAdminUserInvalidError extends IngestManagerError {}
export class ConcurrentInstallOperationError extends IngestManagerError {}
export class AgentReassignmentError extends IngestManagerError {}
export class AgentUnenrollmentError extends IngestManagerError {}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const postAgentUnenrollHandler: RequestHandler<
if (request.body?.force === true) {
await AgentService.forceUnenrollAgent(soClient, esClient, request.params.agentId);
} else {
await AgentService.unenrollAgent(soClient, request.params.agentId);
await AgentService.unenrollAgent(soClient, esClient, request.params.agentId);
}

const body: PostAgentUnenrollResponse = {};
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugins/fleet/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
migrateSettingsToV7100,
migrateAgentActionToV7100,
} from './migrations/to_v7_10_0';
import { migrateAgentToV7120 } from './migrations/to_v7_12_0';
import { migrateAgentToV7120, migrateAgentPolicyToV7120 } from './migrations/to_v7_12_0';

/*
* Saved object types and mappings
Expand Down Expand Up @@ -161,6 +161,7 @@ const getSavedObjectTypes = (
description: { type: 'text' },
namespace: { type: 'keyword' },
is_default: { type: 'boolean' },
is_managed: { type: 'boolean' },
status: { type: 'keyword' },
package_policies: { type: 'keyword' },
updated_at: { type: 'date' },
Expand All @@ -171,6 +172,7 @@ const getSavedObjectTypes = (
},
migrations: {
'7.10.0': migrateAgentPolicyToV7100,
'7.12.0': migrateAgentPolicyToV7120,
},
},
[ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: {
Expand Down
15 changes: 13 additions & 2 deletions x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* 2.0.
*/

import { SavedObjectMigrationFn } from 'kibana/server';
import { Agent } from '../../types';
import type { SavedObjectMigrationFn } from 'kibana/server';
import type { Agent, AgentPolicy } from '../../types';

export const migrateAgentToV7120: SavedObjectMigrationFn<Agent & { shared_id?: string }, Agent> = (
agentDoc
Expand All @@ -15,3 +15,14 @@ export const migrateAgentToV7120: SavedObjectMigrationFn<Agent & { shared_id?: s

return agentDoc;
};

export const migrateAgentPolicyToV7120: SavedObjectMigrationFn<
Exclude<AgentPolicy, 'is_managed'>,
AgentPolicy
> = (agentPolicyDoc) => {
const isV12 = 'is_managed' in agentPolicyDoc.attributes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the isV12 here? the migration will only run when it's migrated one time right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. It's left over from when I was debug logging; it also felt like a long if. I guess it also be hasManagedAttr or not a problem to inline it.

if (!isV12) {
agentPolicyDoc.attributes.is_managed = false;
}
return agentPolicyDoc;
};
87 changes: 84 additions & 3 deletions x-pack/plugins/fleet/server/services/agent_policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@
import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
import { agentPolicyService } from './agent_policy';
import { agentPolicyUpdateEventHandler } from './agent_policy_update';
import { Output } from '../types';
import type { AgentPolicy, NewAgentPolicy, Output } from '../types';

function getSavedObjectMock(agentPolicyAttributes: any) {
const mock = savedObjectsClientMock.create();

mock.get.mockImplementation(async (type: string, id: string) => {
return {
type,
id,
references: [],
attributes: agentPolicyAttributes,
attributes: agentPolicyAttributes as AgentPolicy,
};
});
mock.find.mockImplementation(async (options) => {
Expand Down Expand Up @@ -69,10 +68,59 @@ function getAgentPolicyUpdateMock() {
>;
}

function getAgentPolicyCreateMock() {
const soClient = savedObjectsClientMock.create();
soClient.create.mockImplementation(async (type, attributes) => {
return {
attributes: (attributes as unknown) as NewAgentPolicy,
id: 'mocked',
type: 'mocked',
references: [],
};
});
return soClient;
}
describe('agent policy', () => {
beforeEach(() => {
getAgentPolicyUpdateMock().mockClear();
});

describe('create', () => {
it('is_managed present and false by default', async () => {
// ignore unrelated unique name constraint
agentPolicyService.requireUniqueName = async () => {};
const soClient = getAgentPolicyCreateMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

await expect(
agentPolicyService.create(soClient, esClient, {
name: 'No is_managed provided',
namespace: 'default',
})
).resolves.toHaveProperty('is_managed', false);

const [, attributes] = soClient.create.mock.calls[0];
expect(attributes).toHaveProperty('is_managed', false);
});

it('should set is_managed property, if given', async () => {
// ignore unrelated unique name constraint
agentPolicyService.requireUniqueName = async () => {};
const soClient = getAgentPolicyCreateMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await expect(
agentPolicyService.create(soClient, esClient, {
name: 'is_managed: true provided',
namespace: 'default',
is_managed: true,
})
).resolves.toHaveProperty('is_managed', true);

const [, attributes] = soClient.create.mock.calls[0];
expect(attributes).toHaveProperty('is_managed', true);
});
});

describe('bumpRevision', () => {
it('should call agentPolicyUpdateEventHandler with updated event once', async () => {
const soClient = getSavedObjectMock({
Expand Down Expand Up @@ -208,4 +256,37 @@ describe('agent policy', () => {
});
});
});

describe('update', () => {
it('should update is_managed property, if given', async () => {
// ignore unrelated unique name constraint
agentPolicyService.requireUniqueName = async () => {};
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

soClient.get.mockResolvedValue({
attributes: {},
id: 'mocked',
type: 'mocked',
references: [],
});
await agentPolicyService.update(soClient, esClient, 'mocked', {
name: 'mocked',
namespace: 'default',
is_managed: false,
});
// soClient.update is called with updated values
let calledWith = soClient.update.mock.calls[0];
expect(calledWith[2]).toHaveProperty('is_managed', false);

await agentPolicyService.update(soClient, esClient, 'mocked', {
name: 'is_managed: true provided',
namespace: 'default',
is_managed: true,
});
// soClient.update is called with updated values
calledWith = soClient.update.mock.calls[1];
expect(calledWith[2]).toHaveProperty('is_managed', true);
});
});
});
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/services/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class AgentPolicyService {
SAVED_OBJECT_TYPE,
{
...agentPolicy,
is_managed: agentPolicy.is_managed ?? false,
revision: 1,
updated_at: new Date().toISOString(),
updated_by: options?.user?.username || 'system',
Expand Down
18 changes: 17 additions & 1 deletion x-pack/plugins/fleet/server/services/agents/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
import { AgentSOAttributes, Agent, ListWithKuery } from '../../types';
import { escapeSearchQueryPhrase } from '../saved_object';
import { savedObjectToAgent } from './saved_objects';
import { appContextService } from '../../services';
import { appContextService, agentPolicyService } from '../../services';
import * as crudServiceSO from './crud_so';
import * as crudServiceFleetServer from './crud_fleet_server';

Expand Down Expand Up @@ -86,6 +86,22 @@ export async function getAgents(soClient: SavedObjectsClientContract, agentIds:
return agents;
}

export async function getAgentPolicyForAgent(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentId: string
) {
const agent = await getAgent(soClient, esClient, agentId);
if (!agent.policy_id) {
return;
}

const agentPolicy = await agentPolicyService.get(soClient, agent.policy_id, false);
if (agentPolicy) {
return agentPolicy;
}
}

export async function getAgentByAccessAPIKeyId(
soClient: SavedObjectsClientContract,
accessAPIKeyId: string
Expand Down
132 changes: 132 additions & 0 deletions x-pack/plugins/fleet/server/services/agents/reassign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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 { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
import type { SavedObject } from 'kibana/server';
import type { Agent, AgentPolicy } from '../../types';
import { AgentReassignmentError } from '../../errors';
import { reassignAgent, reassignAgents } from './reassign';

const agentInManagedSO = {
id: 'agent-in-managed-policy',
attributes: { policy_id: 'managed-agent-policy' },
} as SavedObject<Agent>;
const agentInManagedSO2 = {
id: 'agent-in-managed-policy2',
attributes: { policy_id: 'managed-agent-policy' },
} as SavedObject<Agent>;
const agentInUnmanagedSO = {
id: 'agent-in-unmanaged-policy',
attributes: { policy_id: 'unmanaged-agent-policy' },
} as SavedObject<Agent>;
const agentInUnmanagedSO2 = {
id: 'agent-in-unmanaged-policy2',
attributes: { policy_id: 'unmanaged-agent-policy' },
} as SavedObject<Agent>;
const unmanagedAgentPolicySO = {
id: 'unmanaged-agent-policy',
attributes: { is_managed: false },
} as SavedObject<AgentPolicy>;
const managedAgentPolicySO = {
id: 'managed-agent-policy',
attributes: { is_managed: true },
} as SavedObject<AgentPolicy>;

describe('reassignAgent (singular)', () => {
it('can reassign from unmanaged policy to unmanaged', async () => {
const soClient = createClientMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await reassignAgent(soClient, esClient, agentInUnmanagedSO.id, agentInUnmanagedSO2.id);

// calls ES update with correct values
expect(soClient.update).toBeCalledTimes(1);
const calledWith = soClient.update.mock.calls[0];
expect(calledWith[1]).toBe(agentInUnmanagedSO.id);
expect(calledWith[2]).toHaveProperty('policy_id', agentInUnmanagedSO2.id);
});

it('cannot reassign from unmanaged policy to managed', async () => {
const soClient = createClientMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await expect(
reassignAgent(
soClient,
esClient,
agentInUnmanagedSO.id,
agentInManagedSO.attributes.policy_id!
)
).rejects.toThrowError(AgentReassignmentError);

// does not call ES update
expect(soClient.update).toBeCalledTimes(0);
});

it('cannot reassign from managed policy', async () => {
const soClient = createClientMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await expect(
reassignAgent(soClient, esClient, agentInManagedSO.id, agentInManagedSO2.id)
).rejects.toThrowError(AgentReassignmentError);
// does not call ES update
expect(soClient.update).toBeCalledTimes(0);

await expect(
reassignAgent(soClient, esClient, agentInManagedSO.id, agentInUnmanagedSO.id)
).rejects.toThrowError(AgentReassignmentError);
// does not call ES update
expect(soClient.update).toBeCalledTimes(0);
});
});

describe('reassignAgents (plural)', () => {
it('agents in managed policies are not updated', async () => {
const soClient = createClientMock();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const idsToReassign = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO.id];
await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, agentInUnmanagedSO.id);

// calls ES update with correct values
const calledWith = soClient.bulkUpdate.mock.calls[0][0];
const expectedResults = [agentInUnmanagedSO.id, agentInUnmanagedSO.id];
expect(calledWith.length).toBe(expectedResults.length); // only 2 are unmanaged
expect(calledWith.map(({ id }) => id)).toEqual(expectedResults);
});
});

function createClientMock() {
const soClientMock = savedObjectsClientMock.create();

// need to mock .create & bulkCreate due to (bulk)createAgentAction(s) in reassignAgent(s)
soClientMock.create.mockResolvedValue(agentInUnmanagedSO);
soClientMock.bulkCreate.mockImplementation(async ([{ type, attributes }]) => {
return {
saved_objects: [await soClientMock.create(type, attributes)],
};
});

soClientMock.get.mockImplementation(async (_, id) => {
switch (id) {
case unmanagedAgentPolicySO.id:
return unmanagedAgentPolicySO;
case managedAgentPolicySO.id:
return managedAgentPolicySO;
case agentInManagedSO.id:
return agentInManagedSO;
case agentInUnmanagedSO.id:
default:
return agentInUnmanagedSO;
}
});

soClientMock.bulkGet.mockImplementation(async (options) => {
return {
saved_objects: await Promise.all(options!.map(({ type, id }) => soClientMock.get(type, id))),
};
});

return soClientMock;
}
Loading