From deaa7794d542efa01c141b789f0c0c5575653af7 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 14 Apr 2021 17:00:18 -0500 Subject: [PATCH] [Fleet] Add ability to specify which integration variables should be configurable (#97163) --- .../common/types/models/package_policy.ts | 1 + .../package_policy_input_config.tsx | 3 +- .../package_policy_input_stream.tsx | 3 +- .../package_policy_input_var_field.tsx | 19 ++- .../server/services/package_policy.test.ts | 153 +++++++++++++++++- .../fleet/server/services/package_policy.ts | 39 +++++ .../server/types/models/preconfiguration.ts | 3 +- 7 files changed, 211 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index cb84c0a2fc09..f30cc0f87d05 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -14,6 +14,7 @@ export interface PackagePolicyPackage { export interface PackagePolicyConfigRecordEntry { type?: string; value?: any; + frozen?: boolean; } export type PackagePolicyConfigRecord = Record; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx index 037c716b42a3..33ee95910daa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx @@ -105,12 +105,13 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{ {requiredVars.map((varDef) => { const { name: varName, type: varType } = varDef; - const value = packagePolicyInput.vars![varName].value; + const { value, frozen } = packagePolicyInput.vars![varName]; return ( { updatePackagePolicyInput({ vars: { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx index 3337af743711..84f097813d48 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx @@ -106,12 +106,13 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ {requiredVars.map((varDef) => { const { name: varName, type: varType } = varDef; - const value = packagePolicyInputStream.vars![varName].value; + const { value, frozen } = packagePolicyInputStream.vars![varName]; return ( { updatePackagePolicyInputStream({ vars: { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx index 15712f9042eb..7841e8bb6245 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx @@ -15,6 +15,7 @@ import { EuiComboBox, EuiText, EuiCodeEditor, + EuiTextArea, EuiFieldPassword, } from '@elastic/eui'; @@ -29,7 +30,8 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onChange: (newValue: any) => void; errors?: string[] | null; forceShowErrors?: boolean; -}> = memo(({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => { + frozen?: boolean; +}> = memo(({ varDef, value, onChange, errors: varErrors, forceShowErrors, frozen }) => { const [isDirty, setIsDirty] = useState(false); const { multi, required, type, title, name, description } = varDef; const isInvalid = (isDirty || forceShowErrors) && !!varErrors; @@ -50,12 +52,20 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ onChange(newVals.map((val) => val.label)); }} onBlur={() => setIsDirty(true)} + isDisabled={frozen} /> ); } switch (type) { case 'yaml': - return ( + return frozen ? ( + + ) : ( onChange(e.target.checked)} onBlur={() => setIsDirty(true)} + disabled={frozen} /> ); case 'password': @@ -89,6 +100,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ value={value === undefined ? '' : value} onChange={(e) => onChange(e.target.value)} onBlur={() => setIsDirty(true)} + disabled={frozen} /> ); default: @@ -98,10 +110,11 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ value={value === undefined ? '' : value} onChange={(e) => onChange(e.target.value)} onBlur={() => setIsDirty(true)} + disabled={frozen} /> ); } - }, [isInvalid, multi, onChange, type, value, fieldLabel]); + }, [isInvalid, multi, onChange, type, value, fieldLabel, frozen]); // Boolean cannot be optional by default set to false const isOptional = useMemo(() => type !== 'bool' && !required, [required, type]); diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index b3e726bdf7c9..2516073793a8 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -11,10 +11,10 @@ import { httpServerMock, } from 'src/core/server/mocks'; -import type { SavedObjectsUpdateResponse } from 'src/core/server'; +import type { SavedObjectsClient, SavedObjectsUpdateResponse } from 'src/core/server'; import type { KibanaRequest } from 'kibana/server'; -import type { PackageInfo, PackagePolicySOAttributes } from '../types'; +import type { PackageInfo, PackagePolicySOAttributes, AgentPolicySOAttributes } from '../types'; import { createPackagePolicyMock } from '../../common/mocks'; import type { ExternalCallback } from '..'; @@ -68,6 +68,26 @@ jest.mock('./epm/registry', () => { }; }); +jest.mock('./agent_policy', () => { + return { + agentPolicyService: { + get: async (soClient: SavedObjectsClient, id: string) => { + const agentPolicySO = await soClient.get( + 'ingest-agent-policies', + id + ); + if (!agentPolicySO) { + return null; + } + const agentPolicy = { id: agentPolicySO.id, ...agentPolicySO.attributes }; + agentPolicy.package_policies = []; + return agentPolicy; + }, + bumpRevision: () => {}, + }, + }; +}); + describe('Package policy service', () => { describe('compilePackagePolicyInputs', () => { it('should work with config variables from the stream', async () => { @@ -346,8 +366,8 @@ describe('Package policy service', () => { }); savedObjectsClient.update.mockImplementation( async ( - type: string, - id: string + _type: string, + _id: string ): Promise> => { throw savedObjectsClient.errors.createConflictError('abc', '123'); } @@ -362,6 +382,131 @@ describe('Package policy service', () => { ) ).rejects.toThrow('Saved object [abc/123] conflict'); }); + + it('should only update input vars that are not frozen', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const mockPackagePolicy = createPackagePolicyMock(); + const mockInputs = [ + { + config: {}, + enabled: true, + type: 'endpoint', + vars: { + dog: { + type: 'text', + value: 'dalmatian', + }, + cat: { + type: 'text', + value: 'siamese', + frozen: true, + }, + }, + streams: [ + { + data_stream: { + type: 'birds', + dataset: 'migratory.patterns', + }, + enabled: false, + id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`, + vars: { + paths: { + value: ['north', 'south'], + type: 'text', + frozen: true, + }, + period: { + value: '6mo', + type: 'text', + }, + }, + }, + ], + }, + ]; + const inputsUpdate = [ + { + config: {}, + enabled: true, + type: 'endpoint', + vars: { + dog: { + type: 'text', + value: 'labrador', + }, + cat: { + type: 'text', + value: 'tabby', + }, + }, + streams: [ + { + data_stream: { + type: 'birds', + dataset: 'migratory.patterns', + }, + enabled: false, + id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`, + vars: { + paths: { + value: ['east', 'west'], + type: 'text', + }, + period: { + value: '12mo', + type: 'text', + }, + }, + }, + ], + }, + ]; + const attributes = { + ...mockPackagePolicy, + inputs: mockInputs, + }; + + savedObjectsClient.get.mockResolvedValue({ + id: 'test', + type: 'abcd', + references: [], + version: 'test', + attributes, + }); + + savedObjectsClient.update.mockImplementation( + async ( + type: string, + id: string, + attrs: any + ): Promise> => { + savedObjectsClient.get.mockResolvedValue({ + id: 'test', + type: 'abcd', + references: [], + version: 'test', + attributes: attrs, + }); + return attrs; + } + ); + const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + const result = await packagePolicyService.update( + savedObjectsClient, + elasticsearchClient, + 'the-package-policy-id', + { ...mockPackagePolicy, inputs: inputsUpdate } + ); + + const [modifiedInput] = result.inputs; + expect(modifiedInput.vars!.dog.value).toEqual('labrador'); + expect(modifiedInput.vars!.cat.value).toEqual('siamese'); + const [modifiedStream] = modifiedInput.streams; + expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south'])); + expect(modifiedStream.vars!.period.value).toEqual('12mo'); + }); }); describe('runExternalCallbacks', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 1d2295a55346..085733846979 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -23,6 +23,7 @@ import type { DeletePackagePoliciesResponse, PackagePolicyInput, NewPackagePolicyInput, + PackagePolicyConfigRecordEntry, PackagePolicyInputStream, PackageInfo, ListWithKuery, @@ -346,6 +347,8 @@ class PackagePolicyService { assignStreamIdToInput(oldPackagePolicy.id, input) ); + inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs); + if (packagePolicy.package?.name) { const pkgInfo = await getPackageInfo({ savedObjectsClient: soClient, @@ -602,6 +605,42 @@ async function _compilePackageStream( return { ...stream }; } +function enforceFrozenInputs(oldInputs: PackagePolicyInput[], newInputs: PackagePolicyInput[]) { + const resultInputs = [...newInputs]; + + for (const input of resultInputs) { + const oldInput = oldInputs.find((i) => i.type === input.type); + if (input.vars && oldInput?.vars) { + input.vars = _enforceFrozenVars(oldInput.vars, input.vars); + } + if (input.streams && oldInput?.streams) { + for (const stream of input.streams) { + const oldStream = oldInput.streams.find((s) => s.id === stream.id); + if (stream.vars && oldStream?.vars) { + stream.vars = _enforceFrozenVars(oldStream.vars, stream.vars); + } + } + } + } + + return resultInputs; +} + +function _enforceFrozenVars( + oldVars: Record, + newVars: Record +) { + const resultVars: Record = {}; + for (const [key, val] of Object.entries(oldVars)) { + if (val.frozen) { + resultVars[key] = val; + } else { + resultVars[key] = newVars[key]; + } + } + return resultVars; +} + export type PackagePolicyServiceInterface = PackagePolicyService; export const packagePolicyService = new PackagePolicyService(); diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 0dc0ae8f1db8..f697e436fcf4 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -16,7 +16,8 @@ const varsSchema = schema.maybe( schema.object({ name: schema.string(), type: schema.maybe(schema.string()), - value: schema.oneOf([schema.string(), schema.number()]), + value: schema.maybe(schema.oneOf([schema.string(), schema.number()])), + frozen: schema.maybe(schema.boolean()), }) ) );