Skip to content

Commit

Permalink
[Fleet] Do not allow to use logstash output with APM integration
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet committed Mar 15, 2022
1 parent 67566ab commit cd0cc99
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 22 deletions.
5 changes: 4 additions & 1 deletion x-pack/plugins/fleet/server/services/agent_policies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
*/

export { getFullAgentPolicy } from './full_agent_policy';
export { validateOutputForPolicy } from './validate_outputs_for_policy';
export {
validateOutputForPolicy,
getDataOutputForAgentPolicy,
} from './validate_outputs_for_policy';
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { savedObjectsClientMock } from 'src/core/server/mocks';

import { appContextService } from '..';

import { validateOutputForPolicy } from '.';
Expand All @@ -23,23 +25,23 @@ describe('validateOutputForPolicy', () => {
describe('Without oldData (create)', () => {
it('should allow default outputs without platinum licence', async () => {
mockHasLicence(false);
await validateOutputForPolicy({
await validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: null,
monitoring_output_id: null,
});
});

it('should allow default outputs with platinum licence', async () => {
mockHasLicence(false);
await validateOutputForPolicy({
await validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: null,
monitoring_output_id: null,
});
});

it('should not allow custom data outputs without platinum licence', async () => {
mockHasLicence(false);
const res = validateOutputForPolicy({
const res = validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: 'test1',
monitoring_output_id: null,
});
Expand All @@ -50,7 +52,7 @@ describe('validateOutputForPolicy', () => {

it('should not allow custom monitoring outputs without platinum licence', async () => {
mockHasLicence(false);
const res = validateOutputForPolicy({
const res = validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: null,
monitoring_output_id: 'test1',
});
Expand All @@ -61,23 +63,23 @@ describe('validateOutputForPolicy', () => {

it('should allow custom data output with platinum licence', async () => {
mockHasLicence(true);
await validateOutputForPolicy({
await validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: 'test1',
monitoring_output_id: null,
});
});

it('should allow custom monitoring output with platinum licence', async () => {
mockHasLicence(true);
await validateOutputForPolicy({
await validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: null,
monitoring_output_id: 'test1',
});
});

it('should allow custom outputs for managed preconfigured policy without licence', async () => {
mockHasLicence(false);
await validateOutputForPolicy({
await validateOutputForPolicy(savedObjectsClientMock.create(), {
is_managed: true,
is_preconfigured: true,
data_output_id: 'test1',
Expand All @@ -90,6 +92,7 @@ describe('validateOutputForPolicy', () => {
it('should allow default outputs without platinum licence', async () => {
mockHasLicence(false);
await validateOutputForPolicy(
savedObjectsClientMock.create(),
{
data_output_id: null,
monitoring_output_id: null,
Expand All @@ -104,6 +107,7 @@ describe('validateOutputForPolicy', () => {
it('should not allow custom data outputs without platinum licence', async () => {
mockHasLicence(false);
const res = validateOutputForPolicy(
savedObjectsClientMock.create(),
{
data_output_id: 'test1',
monitoring_output_id: null,
Expand All @@ -121,6 +125,7 @@ describe('validateOutputForPolicy', () => {
it('should not allow custom monitoring outputs without platinum licence', async () => {
mockHasLicence(false);
const res = validateOutputForPolicy(
savedObjectsClientMock.create(),
{
data_output_id: null,
monitoring_output_id: 'test1',
Expand All @@ -138,6 +143,7 @@ describe('validateOutputForPolicy', () => {
it('should allow custom data output with platinum licence', async () => {
mockHasLicence(true);
await validateOutputForPolicy(
savedObjectsClientMock.create(),
{
data_output_id: 'test1',
monitoring_output_id: null,
Expand All @@ -151,7 +157,7 @@ describe('validateOutputForPolicy', () => {

it('should allow custom monitoring output with platinum licence', async () => {
mockHasLicence(true);
await validateOutputForPolicy({
await validateOutputForPolicy(savedObjectsClientMock.create(), {
data_output_id: null,
monitoring_output_id: 'test1',
});
Expand All @@ -160,6 +166,7 @@ describe('validateOutputForPolicy', () => {
it('should allow custom outputs for managed preconfigured policy without licence', async () => {
mockHasLicence(false);
await validateOutputForPolicy(
savedObjectsClientMock.create(),
{
data_output_id: 'test1',
monitoring_output_id: 'test1',
Expand All @@ -171,6 +178,7 @@ describe('validateOutputForPolicy', () => {
it('should allow custom outputs if they did not change without licence', async () => {
mockHasLicence(false);
await validateOutputForPolicy(
savedObjectsClientMock.create(),
{
data_output_id: 'test1',
monitoring_output_id: 'test1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,55 @@
* 2.0.
*/

import type { SavedObjectsClientContract } from 'kibana/server';

import type { AgentPolicySOAttributes } from '../../types';
import { LICENCE_FOR_PER_POLICY_OUTPUT } from '../../../common';
import { LICENCE_FOR_PER_POLICY_OUTPUT, outputType } from '../../../common';
import { appContextService } from '..';
import { outputService } from '../output';

export async function getDataOutputForAgentPolicy(
soClient: SavedObjectsClientContract,
agentPolicy: Partial<AgentPolicySOAttributes>
) {
const dataOutputId =
agentPolicy.data_output_id || (await outputService.getDefaultDataOutputId(soClient));

if (!dataOutputId) {
throw new Error('No default data output found.');
}

return outputService.get(soClient, dataOutputId);
}

/**
* Validate outputs are valid for a policy using the current kibana licence or throw.
* @param data
* @returns
*/
export async function validateOutputForPolicy(
soClient: SavedObjectsClientContract,
newData: Partial<AgentPolicySOAttributes>,
oldData: Partial<AgentPolicySOAttributes> = {}
existingData: Partial<AgentPolicySOAttributes> = {},
isPolicyUsingAPM = false
) {
if (
newData.data_output_id === oldData.data_output_id &&
newData.monitoring_output_id === oldData.monitoring_output_id
newData.data_output_id === existingData.data_output_id &&
newData.monitoring_output_id === existingData.monitoring_output_id
) {
return;
}

const data = { ...oldData, ...newData };
const data = { ...existingData, ...newData };

// TODO test
if (isPolicyUsingAPM) {
const dataOutput = await getDataOutputForAgentPolicy(soClient, data);

if (dataOutput.type === outputType.Logstash) {
throw new Error('Logstash output is not usable with policy using the APM integration.');
}
}

if (!data.data_output_id && !data.monitoring_output_id) {
return;
Expand Down
26 changes: 19 additions & 7 deletions x-pack/plugins/fleet/server/services/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
packageToPackagePolicy,
AGENT_POLICY_INDEX,
UUID_V5_NAMESPACE,
FLEET_APM_PACKAGE,
} from '../../common';
import type {
DeleteAgentPolicyResponse,
Expand Down Expand Up @@ -85,26 +86,31 @@ class AgentPolicyService {
user?: AuthenticatedUser,
options: { bumpRevision: boolean } = { bumpRevision: true }
): Promise<AgentPolicy> {
const oldAgentPolicy = await this.get(soClient, id, false);
const existingAgentPolicy = await this.get(soClient, id, true);

if (!oldAgentPolicy) {
if (!existingAgentPolicy) {
throw new Error('Agent policy not found');
}

if (
oldAgentPolicy.status === agentPolicyStatuses.Inactive &&
existingAgentPolicy.status === agentPolicyStatuses.Inactive &&
agentPolicy.status !== agentPolicyStatuses.Active
) {
throw new Error(
`Agent policy ${id} cannot be updated because it is ${oldAgentPolicy.status}`
`Agent policy ${id} cannot be updated because it is ${existingAgentPolicy.status}`
);
}

await validateOutputForPolicy(agentPolicy);
await validateOutputForPolicy(
soClient,
agentPolicy,
existingAgentPolicy,
this.hasAPMIntegration(existingAgentPolicy)
);

await soClient.update<AgentPolicySOAttributes>(SAVED_OBJECT_TYPE, id, {
...agentPolicy,
...(options.bumpRevision ? { revision: oldAgentPolicy.revision + 1 } : {}),
...(options.bumpRevision ? { revision: existingAgentPolicy.revision + 1 } : {}),
updated_at: new Date().toISOString(),
updated_by: user ? user.username : 'system',
});
Expand Down Expand Up @@ -164,6 +170,12 @@ class AgentPolicyService {
};
}

public hasAPMIntegration(agentPolicy: AgentPolicy) {
return agentPolicy.package_policies.some(
(p) => typeof p !== 'string' && p.package?.name === FLEET_APM_PACKAGE
);
}

public async create(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
Expand All @@ -172,7 +184,7 @@ class AgentPolicyService {
): Promise<AgentPolicy> {
await this.requireUniqueName(soClient, agentPolicy);

await validateOutputForPolicy(agentPolicy);
await validateOutputForPolicy(soClient, agentPolicy);

const newSo = await soClient.create<AgentPolicySOAttributes>(
SAVED_OBJECT_TYPE,
Expand Down
14 changes: 13 additions & 1 deletion x-pack/plugins/fleet/server/services/package_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
validatePackagePolicy,
validationHasErrors,
SO_SEARCH_LIMIT,
FLEET_APM_PACKAGE,
outputType,
} from '../../common';
import type {
DeletePackagePoliciesResponse,
Expand Down Expand Up @@ -62,6 +64,7 @@ import type {
import type { ExternalCallback } from '..';

import { agentPolicyService } from './agent_policy';
import { getDataOutputForAgentPolicy } from './agent_policies';
import { outputService } from './output';
import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages';
import { getAssetsData } from './epm/packages/assets';
Expand Down Expand Up @@ -103,6 +106,16 @@ class PackagePolicyService implements PackagePolicyServiceInterface {
overwrite?: boolean;
}
): Promise<PackagePolicy> {
const agentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id, true);

// TODO throw if no agent policy
if (agentPolicy && packagePolicy.package?.name === FLEET_APM_PACKAGE) {
const dataOutput = await getDataOutputForAgentPolicy(soClient, agentPolicy);
if (dataOutput.type === outputType.Logstash) {
throw new IngestManagerError('You cannot add APM to a policy using a logstash output');
}
}

// trailing whitespace causes issues creating API keys
packagePolicy.name = packagePolicy.name.trim();
if (!options?.skipUniqueNameVerification) {
Expand Down Expand Up @@ -154,7 +167,6 @@ class PackagePolicyService implements PackagePolicyServiceInterface {
// Check if it is a limited package, and if so, check that the corresponding agent policy does not
// already contain a package policy for this package
if (isPackageLimited(pkgInfo)) {
const agentPolicy = await agentPolicyService.get(soClient, packagePolicy.policy_id, true);
if (agentPolicy && doesAgentPolicyAlreadyIncludePackage(agentPolicy, pkgInfo.name)) {
throw new IngestManagerError(
`Unable to create package policy. Package '${pkgInfo.name}' already exists on this agent policy.`
Expand Down

0 comments on commit cd0cc99

Please sign in to comment.