mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Allow preconfigured output fields to be edited (#157688)
This commit is contained in:
parent
fa912516e9
commit
b23b3d6bac
11 changed files with 87 additions and 27 deletions
|
@ -1609,6 +1609,9 @@
|
|||
"shipper": {
|
||||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"allow_edit": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface NewOutput {
|
|||
} | null;
|
||||
proxy_id?: string | null;
|
||||
shipper?: ShipperOutput | null;
|
||||
allow_edit?: string[];
|
||||
}
|
||||
|
||||
export type OutputSOAttributes = NewOutput & {
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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') };
|
||||
|
|
|
@ -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 ?? [])
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue