[Fleet] Allow preconfigured output fields to be edited (#157688)

This commit is contained in:
Nicolas Chaulet 2023-05-16 14:37:57 +02:00 committed by GitHub
parent fa912516e9
commit b23b3d6bac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 87 additions and 27 deletions

View file

@ -1609,6 +1609,9 @@
"shipper": {
"dynamic": false,
"properties": {}
},
"allow_edit": {
"enabled": false
}
}
},

View file

@ -104,7 +104,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"infrastructure-ui-source": "2311f7d0abe2a713aa71e30ee24f78828d4acfc1",
"ingest-agent-policies": "d9906923f595f6f3163672351ed3a46472eb280e",
"ingest-download-sources": "95a15b6589ef46e75aca8f7e534c493f99cc3ccd",
"ingest-outputs": "f5adeb3f6abc732a6067137e170578dbf1f58c62",
"ingest-outputs": "3025d8c0cc110748bd8012b9bf49af2d7e6a7bf5",
"ingest-package-policies": "b0b652adb1b26d056d8ed3c0303d0ad85c2c1ae9",
"ingest_manager_settings": "fb75bff08a8de3435b23664b1191f9244a255701",
"inventory-view": "6d47ef0b38166ecbd1c2fc7394599a4500db1ae4",

View file

@ -27,6 +27,7 @@ export interface NewOutput {
} | null;
proxy_id?: string | null;
shipper?: ShipperOutput | null;
allow_edit?: string[];
}
export type OutputSOAttributes = NewOutput & {

View file

@ -37,4 +37,5 @@ export type PreconfiguredPackage = Omit<PackagePolicyPackage, 'title'>;
export interface PreconfiguredOutput extends Omit<Output, 'config_yaml'> {
config?: Record<string, unknown>;
allow_edit?: string[];
}

View file

@ -352,6 +352,7 @@ export const EditOutputFlyout: React.FunctionComponent<EditOutputFlyoutProps> =
}
options={proxiesOptions}
singleSelection={{ asPlainText: true }}
isDisabled={inputs.proxyIdInput.props.disabled}
isClearable={true}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.proxyIdPlaceholder',

View file

@ -78,37 +78,45 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
// preconfigured output do not allow edition
const isPreconfigured = output?.is_preconfigured ?? false;
const allowEdit = output?.allow_edit ?? [];
function isDisabled(field: keyof Output) {
if (!isPreconfigured) {
return false;
}
return !allowEdit.includes(field);
}
// Define inputs
// Shared inputs
const nameInput = useInput(output?.name ?? '', validateName, isPreconfigured);
const typeInput = useInput(output?.type ?? 'elasticsearch', undefined, isPreconfigured);
const nameInput = useInput(output?.name ?? '', validateName, isDisabled('name'));
const typeInput = useInput(output?.type ?? 'elasticsearch', undefined, isDisabled('type'));
const additionalYamlConfigInput = useInput(
output?.config_yaml ?? '',
validateYamlConfig,
isPreconfigured
isDisabled('config_yaml')
);
const defaultOutputInput = useSwitchInput(
output?.is_default ?? false,
isPreconfigured || output?.is_default
isDisabled('is_default') || output?.is_default
);
const defaultMonitoringOutputInput = useSwitchInput(
output?.is_default_monitoring ?? false,
isPreconfigured || output?.is_default_monitoring
isDisabled('is_default_monitoring') || output?.is_default_monitoring
);
// ES inputs
const caTrustedFingerprintInput = useInput(
output?.ca_trusted_fingerprint ?? '',
validateCATrustedFingerPrint,
isPreconfigured
isDisabled('ca_trusted_fingerprint')
);
const elasticsearchUrlInput = useComboInput(
'esHostsComboxBox',
output?.hosts ?? [],
validateESHosts,
isPreconfigured
isDisabled('hosts')
);
/*
Shipper feature flag - currently depends on the content of the yaml
@ -166,28 +174,28 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
const queueFlushTimeout = useNumberInput(output?.shipper?.queue_flush_timeout || undefined);
const maxBatchBytes = useNumberInput(output?.shipper?.max_batch_bytes || undefined);
const isSSLEditable = isDisabled('ssl');
// Logstash inputs
const logstashHostsInput = useComboInput(
'logstashHostsComboxBox',
output?.hosts ?? [],
validateLogstashHosts,
isPreconfigured
isDisabled('hosts')
);
const sslCertificateAuthoritiesInput = useComboInput(
'sslCertificateAuthoritiesComboxBox',
output?.ssl?.certificate_authorities ?? [],
undefined,
isPreconfigured
isSSLEditable
);
const sslCertificateInput = useInput(
output?.ssl?.certificate ?? '',
validateSSLCertificate,
isPreconfigured
isSSLEditable
);
const sslKeyInput = useInput(output?.ssl?.key ?? '', validateSSLKey, isSSLEditable);
const proxyIdInput = useInput(output?.proxy_id ?? '', () => undefined, isPreconfigured);
const sslKeyInput = useInput(output?.ssl?.key ?? '', validateSSLKey, isPreconfigured);
const proxyIdInput = useInput(output?.proxy_id ?? '', () => undefined, isDisabled('proxy_id'));
const isLogstash = typeInput.value === 'logstash';
@ -402,9 +410,6 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
hasEncryptedSavedObjectConfigured,
isShipperEnabled: !isShipperDisabled,
isDisabled:
isLoading ||
isPreconfigured ||
(output && !hasChanged) ||
(isLogstash && !hasEncryptedSavedObjectConfigured),
isLoading || (output && !hasChanged) || (isLogstash && !hasEncryptedSavedObjectConfigured),
};
}

View file

@ -162,6 +162,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields
properties: {},
},
allow_edit: { enabled: false },
},
},
migrations: {

View file

@ -89,6 +89,13 @@ function getMockedSoClient(
});
}
case outputIdToUuid('existing-preconfigured-default-output-allow-edit-name'): {
return mockOutputSO('existing-preconfigured-default-output-allow-edit-name', {
name: 'test',
allow_edit: ['name'],
});
}
case outputIdToUuid('existing-logstash-output'): {
return mockOutputSO('existing-logstash-output', {
type: 'logstash',
@ -321,7 +328,7 @@ describe('Output Service', () => {
{ id: 'output-test' }
)
).rejects.toThrow(
`Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.`
`Preconfigured output existing-preconfigured-default-output is_default cannot be updated outside of kibana config file.`
);
});
@ -587,10 +594,10 @@ describe('Output Service', () => {
const soClient = getMockedSoClient();
await expect(
outputService.update(soClient, esClientMock, 'existing-preconfigured-default-output', {
config_yaml: '',
config_yaml: 'test: 123',
})
).rejects.toThrow(
'Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.'
'Preconfigured output existing-preconfigured-default-output config_yaml cannot be updated outside of kibana config file.'
);
});
@ -611,6 +618,23 @@ describe('Output Service', () => {
expect(soClient.update).toBeCalled();
});
it('Allow to update preconfigured output allowed to edit field from preconfiguration', async () => {
const soClient = getMockedSoClient();
await outputService.update(
soClient,
esClientMock,
'existing-preconfigured-default-output-allow-edit-name',
{
name: 'test 123',
},
{
fromPreconfiguration: false,
}
);
expect(soClient.update).toBeCalled();
});
it('Should throw when an existing preconfigured default output and updating an output to become the default one outside of preconfiguration', async () => {
const soClient = getMockedSoClient({
defaultOutputId: 'existing-preconfigured-default-output',
@ -624,7 +648,7 @@ describe('Output Service', () => {
type: 'elasticsearch',
})
).rejects.toThrow(
`Preconfigured output existing-preconfigured-default-output cannot be updated outside of kibana config file.`
`Preconfigured output existing-preconfigured-default-output is_default cannot be updated outside of kibana config file.`
);
});

View file

@ -7,6 +7,7 @@
import { v5 as uuidv5 } from 'uuid';
import { omit } from 'lodash';
import { safeLoad } from 'js-yaml';
import deepEqual from 'fast-deep-equal';
import { SavedObjectsUtils } from '@kbn/core/server';
import type {
@ -554,11 +555,23 @@ class OutputService {
}
) {
const originalOutput = await this.get(soClient, id);
if (originalOutput.is_preconfigured) {
if (!fromPreconfiguration) {
const allowEditFields = originalOutput.allow_edit ?? [];
if (originalOutput.is_preconfigured && !fromPreconfiguration) {
throw new OutputUnauthorizedError(
`Preconfigured output ${id} cannot be updated outside of kibana config file.`
);
const allKeys = Array.from(new Set([...Object.keys(data)])) as Array<keyof Output>;
for (const key of allKeys) {
if (
(!!originalOutput[key] || !!data[key]) &&
!allowEditFields.includes(key) &&
!deepEqual(originalOutput[key], data[key])
) {
throw new OutputUnauthorizedError(
`Preconfigured output ${id} ${key} cannot be updated outside of kibana config file.`
);
}
}
}
}
const updateData: Nullable<Partial<OutputSOAttributes>> = { ...omit(data, 'ssl') };

View file

@ -88,6 +88,15 @@ export async function createOrUpdatePreconfiguredOutputs(
}
const isCreate = !existingOutput;
// field in allow edit are not updated through preconfiguration
if (!isCreate && output.allow_edit) {
for (const key of output.allow_edit) {
// @ts-expect-error
data[key] = existingOutput[key];
}
}
const isUpdateWithNewData =
existingOutput && isPreconfiguredOutputDifferentFromCurrent(existingOutput, data);
@ -181,6 +190,7 @@ function isPreconfiguredOutputDifferentFromCurrent(
preconfiguredOutput.ca_trusted_fingerprint
) ||
isDifferent(existingOutput.config_yaml, preconfiguredOutput.config_yaml) ||
isDifferent(existingOutput.proxy_id, preconfiguredOutput.proxy_id)
isDifferent(existingOutput.proxy_id, preconfiguredOutput.proxy_id) ||
isDifferent(existingOutput.allow_edit ?? [], preconfiguredOutput.allow_edit ?? [])
);
}

View file

@ -78,6 +78,7 @@ export const PreconfiguredOutputsSchema = schema.arrayOf(
id: schema.string(),
config: schema.maybe(schema.object({}, { unknowns: 'allow' })),
config_yaml: schema.never(),
allow_edit: schema.maybe(schema.arrayOf(schema.string())),
}),
{
defaultValue: [],