From a8f8f087d3ba622970ff9075beab2a1693e3a7d3 Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Thu, 7 Dec 2023 14:14:56 +0100
Subject: [PATCH] [Fleet] fix validation of output secret fields (#172795)
## Summary
Closes https://github.com/elastic/kibana/issues/172481
Fixed validation of secret output fields. Updated cypress tests that
validates output secrets.
Create a new remote elasticsearch output and verify that service token
field is required when clicking Save without filling it in.
Same for Logstash ssl key:
Kafka password and ssl key:
In edit mode, when the secret field is empty, the validation is shown
again and goes away when clicking on Cancel changes.
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../cypress/e2e/fleet_settings_outputs.cy.ts | 79 ++++++++++++++++++-
.../fleet/cypress/screens/fleet_outputs.ts | 11 +++
.../components/edit_output_flyout/index.tsx | 1 +
.../output_form_kafka_authentication.tsx | 2 +
.../output_form_remote_es.tsx | 1 +
.../output_form_secret_form_row.test.tsx | 14 +++-
.../output_form_secret_form_row.tsx | 15 +++-
.../output_form_validators.tsx | 4 +-
8 files changed, 118 insertions(+), 9 deletions(-)
diff --git a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts
index 28b124116502a..5101c4d93fd39 100644
--- a/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts
+++ b/x-pack/plugins/fleet/cypress/e2e/fleet_settings_outputs.cy.ts
@@ -21,6 +21,7 @@ import {
loadESOutput,
loadKafkaOutput,
loadLogstashOutput,
+ loadRemoteESOutput,
resetKafkaOutputForm,
selectESOutput,
selectKafkaOutput,
@@ -134,6 +135,80 @@ describe('Outputs', () => {
});
});
+ describe('Remote ES', () => {
+ it('displays proper error messages', () => {
+ selectRemoteESOutput();
+ cy.getBySel(SETTINGS_SAVE_BTN).click();
+
+ cy.contains('Name is required');
+ cy.contains('URL is required');
+ cy.contains('Service Token is required');
+ shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT);
+ shouldDisplayError('serviceTokenSecretInput');
+ });
+
+ describe('Form submit', () => {
+ let outputId: string;
+
+ before(() => {
+ interceptOutputId((id) => {
+ outputId = id;
+ });
+ });
+
+ after(() => {
+ cleanupOutput(outputId);
+ });
+
+ it('saves the output', () => {
+ selectRemoteESOutput();
+
+ cy.getBySel(SETTINGS_OUTPUTS.NAME_INPUT).type('name');
+ cy.get('[placeholder="Specify host URL"').clear().type('https://localhost:5000');
+ cy.getBySel('serviceTokenSecretInput').type('service_token');
+
+ cy.intercept('POST', '**/api/fleet/outputs').as('saveOutput');
+
+ cy.getBySel(SETTINGS_SAVE_BTN).click();
+
+ cy.wait('@saveOutput').then((interception) => {
+ const responseBody = interception.response?.body;
+ cy.visit(`/app/fleet/settings/outputs/${responseBody?.item?.id}`);
+ expect(responseBody?.item.service_token).to.equal(undefined);
+ expect(responseBody?.item.secrets.service_token.id).not.to.equal(undefined);
+ });
+
+ cy.get('[placeholder="Specify host URL"').should('have.value', 'https://localhost:5000');
+ cy.getBySel(SETTINGS_OUTPUTS.NAME_INPUT).should('have.value', 'name');
+ });
+ });
+
+ describe('Form edit', () => {
+ let outputId: string;
+
+ before(() => {
+ loadRemoteESOutput().then((data) => {
+ outputId = data.item.id;
+ });
+ });
+ after(() => {
+ cleanupOutput(outputId);
+ });
+
+ it('edits the output', () => {
+ visit(`/app/fleet/settings/outputs/${outputId}`);
+
+ cy.get('[placeholder="Specify host URL"').clear().type('https://localhost:5001');
+
+ cy.getBySel(SETTINGS_SAVE_BTN).click();
+ cy.getBySel(SETTINGS_CONFIRM_MODAL_BTN).click();
+ visit(`/app/fleet/settings/outputs/${outputId}`);
+
+ cy.get('[placeholder="Specify host URL"').should('have.value', 'https://localhost:5001');
+ });
+ });
+ });
+
describe('Kafka', () => {
describe('Form validation', () => {
it('renders all form fields', () => {
@@ -301,7 +376,7 @@ describe('Outputs', () => {
cy.contains('Name is required');
cy.contains('Host is required');
cy.contains('Username is required');
- // cy.contains('Password is required');
+ cy.contains('Password is required');
cy.contains('Default topic is required');
cy.contains('Topic is required');
cy.contains(
@@ -310,7 +385,7 @@ describe('Outputs', () => {
cy.contains('Must be a key, value pair i.e. "http.response.code: 200"');
shouldDisplayError(SETTINGS_OUTPUTS.NAME_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_USERNAME_INPUT);
- // shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT); // TODO
+ shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.AUTHENTICATION_PASSWORD_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_DEFAULT_TOPIC_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_CONDITION_INPUT);
shouldDisplayError(SETTINGS_OUTPUTS_KAFKA.TOPICS_TOPIC_INPUT);
diff --git a/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts b/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts
index e1d1a3530df9a..e439961e287d7 100644
--- a/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts
+++ b/x-pack/plugins/fleet/cypress/screens/fleet_outputs.ts
@@ -84,6 +84,17 @@ export const loadESOutput = () =>
hosts: ['https://bla.co'],
});
+export const loadRemoteESOutput = () =>
+ loadOutput({
+ name: 'remote_es',
+ type: 'remote_elasticsearch',
+ is_default: false,
+ is_default_monitoring: false,
+ hosts: ['https://bla.co'],
+ secrets: { service_token: 'token' },
+ preset: 'balanced',
+ });
+
export const loadLogstashOutput = () =>
loadOutput({
name: 'ls',
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx
index d9fa988aa55cc..3023cf1397faa 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx
@@ -227,6 +227,7 @@ export const EditOutputFlyout: React.FunctionComponent =
})}
{...inputs.sslKeySecretInput.formRowProps}
onUsePlainText={onUsePlainText}
+ cancelEdit={inputs.sslKeySecretInput.cancelEdit}
>
= (props)
defaultMessage: 'Service Token',
})}
{...inputs.serviceTokenSecretInput.formRowProps}
+ cancelEdit={inputs.serviceTokenSecretInput.cancelEdit}
onUsePlainText={onUsePlainText}
>
{
const initialValue = 'initial value';
const clear = jest.fn();
const onUsePlainText = jest.fn();
+ const cancelEdit = jest.fn();
it('should switch to edit mode when the replace button is clicked', () => {
const { getByText, queryByText, container } = render(
@@ -23,6 +24,7 @@ describe('SecretFormRow', () => {
initialValue={initialValue}
clear={clear}
onUsePlainText={onUsePlainText}
+ cancelEdit={cancelEdit}
>
@@ -38,13 +40,14 @@ describe('SecretFormRow', () => {
expect(queryByText(initialValue)).not.toBeInTheDocument();
});
- it('should call the clear function when the cancel button is clicked', () => {
+ it('should call the cancelEdit function when the cancel button is clicked', () => {
const { getByText } = render(
@@ -53,12 +56,17 @@ describe('SecretFormRow', () => {
fireEvent.click(getByText('Replace Test Secret'));
fireEvent.click(getByText('Cancel Test Secret change'));
- expect(clear).toHaveBeenCalled();
+ expect(cancelEdit).toHaveBeenCalled();
});
it('should call the onUsePlainText function when the revert link is clicked', () => {
const { getByText } = render(
-
+
);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx
index fc55835557403..f483503af9e43 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_secret_form_row.tsx
@@ -29,7 +29,18 @@ export const SecretFormRow: React.FC<{
clear: () => void;
initialValue?: any;
onUsePlainText: () => void;
-}> = ({ fullWidth, error, isInvalid, children, clear, title, initialValue, onUsePlainText }) => {
+ cancelEdit: () => void;
+}> = ({
+ fullWidth,
+ error,
+ isInvalid,
+ children,
+ clear,
+ title,
+ initialValue,
+ onUsePlainText,
+ cancelEdit,
+}) => {
const hasInitialValue = initialValue !== undefined;
const [editMode, setEditMode] = useState(!initialValue);
const valueHiddenPanel = (
@@ -66,7 +77,7 @@ export const SecretFormRow: React.FC<{
{
setEditMode(false);
- clear();
+ cancelEdit();
}}
color="primary"
iconType="refresh"
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx
index 28218fed7e942..26e63803df0e7 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx
@@ -11,11 +11,11 @@ import { safeLoad } from 'js-yaml';
const toSecretValidator =
(validator: (value: string) => string[] | undefined) =>
(value: string | { id: string } | undefined) => {
- if (!value || typeof value === 'object') {
+ if (typeof value === 'object') {
return undefined;
}
- return validator(value);
+ return validator(value ?? '');
};
export function validateKafkaHosts(value: string[]) {