Skip to content

Commit

Permalink
[Fleet] Couple agent and package policies spaces (#197487)
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet authored Oct 28, 2024
1 parent 8fc7df2 commit 84dc8da
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 24 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const createAppContextStartContractMock = (
experimentalFeatures: {
agentTamperProtectionEnabled: true,
diagnosticFileUploadEnabled: true,
enableReusableIntegrationPolicies: true,
} as ExperimentalFeatures,
isProductionMode: true,
configInitialValue: {
Expand Down
68 changes: 67 additions & 1 deletion x-pack/plugins/fleet/server/services/package_policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ import { sendTelemetryEvents } from './upgrade_sender';
import { auditLoggingService } from './audit_logging';
import { agentPolicyService } from './agent_policy';
import { isSpaceAwarenessEnabled } from './spaces/helpers';
import { licenseService } from './license';

jest.mock('./spaces/helpers');

jest.mock('./license');

const mockedSendTelemetryEvents = sendTelemetryEvents as jest.MockedFunction<
typeof sendTelemetryEvents
>;
Expand Down Expand Up @@ -207,7 +210,7 @@ const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof audi

type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback;

const mockAgentPolicyGet = () => {
const mockAgentPolicyGet = (spaceIds: string[] = ['default']) => {
mockAgentPolicyService.get.mockImplementation(
(_soClient: SavedObjectsClientContract, id: string, _force = false, _errorMessage?: string) => {
return Promise.resolve({
Expand All @@ -220,9 +223,29 @@ const mockAgentPolicyGet = () => {
updated_by: 'test',
revision: 1,
is_protected: false,
space_ids: spaceIds,
});
}
);
mockAgentPolicyService.getByIDs.mockImplementation(
// @ts-ignore
(_soClient: SavedObjectsClientContract, ids: string[]) => {
return Promise.resolve(
ids.map((id) => ({
id,
name: 'Test Agent Policy',
namespace: 'test',
status: 'active',
is_managed: false,
updated_at: new Date().toISOString(),
updated_by: 'test',
revision: 1,
is_protected: false,
space_ids: spaceIds,
}))
);
}
);
};

describe('Package policy service', () => {
Expand All @@ -240,6 +263,9 @@ describe('Package policy service', () => {
});

describe('create', () => {
beforeEach(() => {
jest.mocked(licenseService.hasAtLeast).mockReturnValue(true);
});
it('should call audit logger', async () => {
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const soClient = savedObjectsClientMock.create();
Expand Down Expand Up @@ -279,6 +305,46 @@ describe('Package policy service', () => {
savedObjectType: LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE,
});
});

it('should not allow to add a reusable integration policies to an agent policies belonging to multiple spaces', async () => {
jest.mocked(isSpaceAwarenessEnabled).mockResolvedValue(true);

const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const soClient = savedObjectsClientMock.create();

soClient.create.mockResolvedValueOnce({
id: 'test-package-policy',
attributes: {},
references: [],
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
});

mockAgentPolicyGet(['test', 'default']);

await expect(
packagePolicyService.create(
soClient,
esClient,
{
name: 'Test Package Policy',
namespace: 'test',
enabled: true,
policy_id: 'test',
policy_ids: ['test1', 'test2'],
inputs: [],
package: {
name: 'test',
title: 'Test',
version: '0.0.1',
},
},
// Skipping unique name verification just means we have to less mocking/setup
{ id: 'test-package-policy', skipUniqueNameVerification: true }
)
).rejects.toThrowError(
/Reusable integration policies cannot be used with agent policies belonging to multiple spaces./
);
});
});

describe('inspect', () => {
Expand Down
111 changes: 92 additions & 19 deletions x-pack/plugins/fleet/server/services/package_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/* eslint-disable max-classes-per-file */

import { omit, partition, isEqual, cloneDeep, without } from 'lodash';
import { indexBy } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import semverLt from 'semver/functions/lt';
import { getFlattenedObject } from '@kbn/std';
Expand Down Expand Up @@ -144,6 +145,7 @@ import { validateAgentPolicyOutputForIntegration } from './agent_policies/output
import type { PackagePolicyClientFetchAllItemIdsOptions } from './package_policy_service';
import { validatePolicyNamespaceForSpace } from './spaces/policy_namespaces';
import { isSpaceAwarenessEnabled, isSpaceAwarenessMigrationPending } from './spaces/helpers';
import { updatePackagePolicySpaces } from './spaces/package_policy';

export type InputsOverride = Partial<NewPackagePolicyInput> & {
vars?: Array<NewPackagePolicyInput['vars'] & { name: string }>;
Expand Down Expand Up @@ -227,6 +229,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
context?: RequestHandlerContext,
request?: KibanaRequest
): Promise<PackagePolicy> {
const useSpaceAwareness = await isSpaceAwarenessEnabled();
const packagePolicyId = options?.id || uuidv4();

let authorizationHeader = options.authorizationHeader;
Expand Down Expand Up @@ -274,6 +277,10 @@ class PackagePolicyClientImpl implements PackagePolicyClient {

for (const policyId of enrichedPackagePolicy.policy_ids) {
const agentPolicy = await agentPolicyService.get(soClient, policyId, true);
if (!agentPolicy) {
throw new AgentPolicyNotFoundError('Agent policy not found');
}

agentPolicies.push(agentPolicy);

// If package policy did not set an output_id, see if the agent policy's output is compatible
Expand All @@ -285,7 +292,10 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
);
}

await validateIsNotHostedPolicy(soClient, policyId, options?.force);
validateIsNotHostedPolicy(agentPolicy, options?.force);
if (useSpaceAwareness) {
validateReusableIntegrationsAndSpaceAwareness(enrichedPackagePolicy, agentPolicies);
}
}

// trailing whitespace causes issues creating API keys
Expand Down Expand Up @@ -413,6 +423,21 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
{ ...options, id: packagePolicyId }
);

for (const agentPolicy of agentPolicies) {
if (
useSpaceAwareness &&
agentPolicy &&
agentPolicy.space_ids &&
agentPolicy.space_ids.length > 1
) {
await updatePackagePolicySpaces({
packagePolicyId: newSo.id,
currentSpaceId: soClient.getCurrentNamespace() ?? DEFAULT_SPACE_ID,
newSpaceIds: agentPolicy.space_ids,
});
}
}

if (options?.bumpRevision ?? true) {
for (const policyId of enrichedPackagePolicy.policy_ids) {
await agentPolicyService.bumpRevision(soClient, esClient, policyId, {
Expand Down Expand Up @@ -460,6 +485,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
created: PackagePolicy[];
failed: Array<{ packagePolicy: NewPackagePolicy; error?: Error | SavedObjectError }>;
}> {
const useSpaceAwareness = await isSpaceAwarenessEnabled();
const savedObjectType = await getPackagePolicySavedObjectType();
for (const packagePolicy of packagePolicies) {
const basePkgInfo = packagePolicy.package
Expand All @@ -486,8 +512,20 @@ class PackagePolicyClientImpl implements PackagePolicyClient {

const agentPolicyIds = new Set(packagePolicies.flatMap((pkgPolicy) => pkgPolicy.policy_ids));

for (const agentPolicyId of agentPolicyIds) {
await validateIsNotHostedPolicy(soClient, agentPolicyId, options?.force);
const agentPolicies = await agentPolicyService.getByIDs(soClient, [...agentPolicyIds]);
const agentPoliciesIndexById = indexBy('id', agentPolicies);
for (const agentPolicy of agentPolicies) {
validateIsNotHostedPolicy(agentPolicy, options?.force);
}
if (useSpaceAwareness) {
for (const packagePolicy of packagePolicies) {
validateReusableIntegrationsAndSpaceAwareness(
packagePolicy,
packagePolicy.policy_ids
.map((policyId) => agentPoliciesIndexById[policyId])
.filter((policy) => policy !== undefined)
);
}
}

const packageInfos = await getPackageInfoForPackagePolicies(packagePolicies, soClient);
Expand Down Expand Up @@ -604,6 +642,23 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
}
});

if (useSpaceAwareness) {
for (const newSo of newSos) {
// Do not support multpile spaces for reusable integrations
if (newSo.attributes.policy_ids.length > 1) {
continue;
}
const agentPolicy = agentPoliciesIndexById[newSo.attributes.policy_ids[0]];
if (agentPolicy && agentPolicy.space_ids && agentPolicy.space_ids.length > 1) {
await updatePackagePolicySpaces({
packagePolicyId: newSo.id,
currentSpaceId: soClient.getCurrentNamespace() ?? DEFAULT_SPACE_ID,
newSpaceIds: agentPolicy.space_ids,
});
}
}
}

// Assign it to the given agent policy

if (options?.bumpRevision ?? true) {
Expand Down Expand Up @@ -1001,6 +1056,17 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
}
}

if ((packagePolicyUpdate.policy_ids?.length ?? 0) > 1) {
for (const policyId of packagePolicyUpdate.policy_ids) {
const agentPolicy = await agentPolicyService.get(soClient, policyId, true);
if ((agentPolicy?.space_ids?.length ?? 0) > 1) {
throw new FleetError(
'Reusable integration policies cannot be used with agent policies belonging to multiple spaces.'
);
}
}
}

// Handle component template/mappings updates for experimental features, e.g. synthetic source
await handleExperimentalDatastreamFeatureOptIn({
soClient,
Expand Down Expand Up @@ -1391,9 +1457,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {

for (const agentPolicyId of uniqueAgentPolicyIds) {
try {
const agentPolicy = await validateIsNotHostedPolicy(
soClient,
agentPolicyId,
const agentPolicy = await agentPolicyService.get(soClient, agentPolicyId);
if (!agentPolicy) {
throw new AgentPolicyNotFoundError('Agent policy not found');
}

validateIsNotHostedPolicy(
agentPolicy,
options?.force,
'Cannot remove integrations of hosted agent policy'
);
Expand Down Expand Up @@ -3025,27 +3095,30 @@ export function _validateRestrictedFieldsNotModifiedOrThrow(opts: {
}
}

async function validateIsNotHostedPolicy(
soClient: SavedObjectsClientContract,
id: string,
force = false,
errorMessage?: string
): Promise<AgentPolicy> {
const agentPolicy = await agentPolicyService.get(soClient, id, false);

if (!agentPolicy) {
throw new AgentPolicyNotFoundError('Agent policy not found');
function validateReusableIntegrationsAndSpaceAwareness(
packagePolicy: Pick<NewPackagePolicy, 'policy_ids'>,
agentPolicies: AgentPolicy[]
) {
if ((packagePolicy.policy_ids.length ?? 0) <= 1) {
return;
}
for (const agentPolicy of agentPolicies) {
if ((agentPolicy?.space_ids?.length ?? 0) > 1) {
throw new FleetError(
'Reusable integration policies cannot be used with agent policies belonging to multiple spaces.'
);
}
}
}

function validateIsNotHostedPolicy(agentPolicy: AgentPolicy, force = false, errorMessage?: string) {
const isManagedPolicyWithoutServerlessSupport = agentPolicy.is_managed && !force;

if (isManagedPolicyWithoutServerlessSupport) {
throw new HostedAgentPolicyRestrictionRelatedError(
errorMessage ?? `Cannot update integrations of hosted agent policy ${id}`
errorMessage ?? `Cannot update integrations of hosted agent policy ${agentPolicy.id}`
);
}

return agentPolicy;
}

export function sendUpdatePackagePolicyTelemetryEvent(
Expand Down
Loading

0 comments on commit 84dc8da

Please sign in to comment.