From dbec2f85df8501d6571b6609886bbf784b4c89ff Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 22 Feb 2023 09:05:38 -0500 Subject: [PATCH] Fleet UI: Consistent URL validation (#9806) --- changes/9538-url-validation-in-ui | 1 + .../integration/all/app/policiesflow.spec.ts | 16 ++- .../integration/all/app/settingsflow.spec.ts | 6 +- cypress/integration/all/app/software.spec.ts | 14 +-- cypress/integration/premium/admin.spec.ts | 4 +- cypress/support/commands.ts | 8 +- cypress/support/constants.ts | 6 +- .../forms/fields/InputField/InputField.jsx | 2 + .../forms/validators/valid_url/valid_url.ts | 28 +++-- frontend/interfaces/config.ts | 28 ++--- frontend/osquery_tables.json | 48 ++++---- .../IntegrationForm/IntegrationForm.tsx | 8 +- .../pages/admin/OrgSettingsPage/_styles.scss | 2 +- .../cards/FleetDesktop/FleetDesktop.tsx | 19 ++-- .../HostStatusWebhook/HostStatusWebhook.tsx | 16 ++- .../admin/OrgSettingsPage/cards/Info/Info.tsx | 2 +- .../admin/OrgSettingsPage/cards/Sso/Sso.tsx | 106 +++++++++--------- .../cards/WebAddress/WebAddress.tsx | 5 +- .../admin/OrgSettingsPage/cards/constants.ts | 1 + .../ManageAutomationsModal.tsx | 3 + .../ManageAutomationsModal.tsx | 11 +- 21 files changed, 186 insertions(+), 148 deletions(-) create mode 100644 changes/9538-url-validation-in-ui diff --git a/changes/9538-url-validation-in-ui b/changes/9538-url-validation-in-ui new file mode 100644 index 0000000000..5143ab492f --- /dev/null +++ b/changes/9538-url-validation-in-ui @@ -0,0 +1 @@ +* Update to better URL validation in UI \ No newline at end of file diff --git a/cypress/integration/all/app/policiesflow.spec.ts b/cypress/integration/all/app/policiesflow.spec.ts index f2c1079655..ec8713d0fc 100644 --- a/cypress/integration/all/app/policiesflow.spec.ts +++ b/cypress/integration/all/app/policiesflow.spec.ts @@ -55,12 +55,12 @@ const enableJiraPoliciesIntegration = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, vulnerabilities_webhook: { - destination_url: "www.foo.com/bar", + destination_url: "https://www.foo.com/bar", enable_vulnerabilities_webhook: false, }, }, @@ -115,12 +115,12 @@ const enableZendeskPoliciesIntegration = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, vulnerabilities_webhook: { - destination_url: "www.foo.com/bar", + destination_url: "https://www.foo.com/bar", enable_vulnerabilities_webhook: false, }, }, @@ -508,11 +508,15 @@ describe("Policies flow (seeded)", () => { cy.findByRole("button", { name: /manage automations/i }).click(); }); cy.getAttached(".manage-automations-modal").within(() => { + // Ensure clicking on slider after modal animation + cy.wait(300); // eslint-disable-line cypress/no-unnecessary-waiting cy.getAttached(".fleet-slider").click(); cy.getAttached(".fleet-checkbox__input").check({ force: true }); }); - cy.getAttached("#webhook-url").click().type("www.foo.com/bar"); + cy.getAttached("#webhook-url").click().type("http://www.foo.com/bar"); cy.findByRole("button", { name: /^Save$/ }).click(); + // Ensure update + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting // Confirm failing policies webhook was added successfully cy.findByText(/updated policy automations/i).should("exist"); cy.getAttached(".button-wrap").within(() => { @@ -523,6 +527,8 @@ describe("Policies flow (seeded)", () => { }); // reset slider for subsequent tests cy.getAttached(".manage-automations-modal").within(() => { + // Ensure clicking on slider after modal animation + cy.wait(300); // eslint-disable-line cypress/no-unnecessary-waiting cy.getAttached(".fleet-slider").click(); }); cy.findByRole("button", { name: /^Save$/ }).click(); diff --git a/cypress/integration/all/app/settingsflow.spec.ts b/cypress/integration/all/app/settingsflow.spec.ts index 710cee5891..9c51367cc6 100644 --- a/cypress/integration/all/app/settingsflow.spec.ts +++ b/cypress/integration/all/app/settingsflow.spec.ts @@ -194,7 +194,7 @@ describe("App settings flow", () => { // specifically targeting this one to avoid conflict // with cypress seeing multiple "metadata url" - one // in a tooltip, the other as the actual label - cy.getAttached("[for='metadataURL']") + cy.getAttached("[for='metadataUrl']") .click() .type("http://github.com/fleetdm/fleet"); @@ -223,7 +223,7 @@ describe("App settings flow", () => { "https://http.cat/100" ); - cy.getAttached("#metadataURL").should( + cy.getAttached("#metadataUrl").should( "have.value", "http://github.com/fleetdm/fleet" ); @@ -280,7 +280,7 @@ describe("App settings flow", () => { ); cy.findByText(/single sign-on options/i).click(); - cy.getAttached("#metadataURL").should( + cy.getAttached("#metadataUrl").should( "have.value", "http://github.com/fleetdm/fleet" ); diff --git a/cypress/integration/all/app/software.spec.ts b/cypress/integration/all/app/software.spec.ts index 4a98c39c8c..4583643061 100644 --- a/cypress/integration/all/app/software.spec.ts +++ b/cypress/integration/all/app/software.spec.ts @@ -55,12 +55,12 @@ const enableWebhook = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, vulnerabilities_webhook: { - destination_url: "www.foo.com/bar", + destination_url: "http://www.foo.com/bar", enable_vulnerabilities_webhook: true, }, }, @@ -115,12 +115,12 @@ const enableJiraSoftwareIntegration = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, vulnerabilities_webhook: { - destination_url: "www.foo.com/bar", + destination_url: "http://www.foo.com/bar", enable_vulnerabilities_webhook: false, }, }, @@ -175,12 +175,12 @@ const enableZendeskSoftwareIntegration = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, vulnerabilities_webhook: { - destination_url: "www.foo.com/bar", + destination_url: "http://www.foo.com/bar", enable_vulnerabilities_webhook: false, }, }, @@ -259,7 +259,7 @@ describe("Software", () => { cy.getAttached(".fleet-slider").click(); cy.getAttached("#webhook-radio-btn").next().click(); }); - cy.getAttached("#webhook-url").click().type("www.foo.com/bar"); + cy.getAttached("#webhook-url").click().type("http://www.foo.com/bar"); cy.intercept("PATCH", "/api/latest/fleet/config", enableWebhook).as( "createWebhook" ); diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index 5b9e371f23..20a927188f 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -577,13 +577,13 @@ describe("Premium tier - Global Admin user", () => { cy.getAttached("[id=transparency_url") .should("have.value", "https://fleetdm.com/transparency") .clear() - .type("example.com/transparency"); + .type("http://example.com/transparency"); cy.findByRole("button", { name: /save/i }).click(); cy.findByText(/successfully updated/i).should("exist"); cy.visit("settings/organization/fleet-desktop"); cy.getAttached("[id=transparency_url").should( "have.value", - "example.com/transparency" + "http://example.com/transparency" ); }); }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 9cda76b149..f48dea7478 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -422,15 +422,15 @@ Cypress.Commands.add("addUser", (options = {}) => { Cypress.Commands.add("addDockerHost", (team = "") => { const serverPort = new URL(Cypress.config().baseUrl).port; // Get enroll secret - let enrollSecretURL = "/api/latest/fleet/spec/enroll_secret"; + let enrollSecretUrl = "/api/latest/fleet/spec/enroll_secret"; if (team === "apples") { - enrollSecretURL = "/api/latest/fleet/teams/1/secrets"; + enrollSecretUrl = "/api/latest/fleet/teams/1/secrets"; } else if (team === "oranges") { - enrollSecretURL = "/api/latest/fleet/teams/2/secrets"; + enrollSecretUrl = "/api/latest/fleet/teams/2/secrets"; } cy.request({ - url: enrollSecretURL, + url: enrollSecretUrl, auth: { bearer: window.localStorage.getItem("FLEET::auth_token"), }, diff --git a/cypress/support/constants.ts b/cypress/support/constants.ts index ef292c814a..958ca0f946 100644 --- a/cypress/support/constants.ts +++ b/cypress/support/constants.ts @@ -79,7 +79,7 @@ const CONFIG_INTEGRATIONS_AUTOMATIONS = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, @@ -224,12 +224,12 @@ const CONFIG_INTEGRATIONS_AUTOMATIONS_DISABLED = { }, failing_policies_webhook: { enable_failing_policies_webhook: false, - destination_url: "ok.com", + destination_url: "https://www.foo.com/bar", policy_ids: [5, 10], host_batch_size: 0, }, vulnerabilities_webhook: { - destination_url: "www.foo.com/bar", + destination_url: "http://www.foo.com/bar", enable_vulnerabilities_webhook: false, }, }, diff --git a/frontend/components/forms/fields/InputField/InputField.jsx b/frontend/components/forms/fields/InputField/InputField.jsx index 5d1e148998..6182fceac0 100644 --- a/frontend/components/forms/fields/InputField/InputField.jsx +++ b/frontend/components/forms/fields/InputField/InputField.jsx @@ -32,6 +32,7 @@ class InputField extends Component { hint: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf(PropTypes.string), + PropTypes.object, ]), }; @@ -91,6 +92,7 @@ class InputField extends Component { blockAutoComplete, value, } = this.props; + const { onInputChange } = this; const shouldShowPasswordClass = type === "password"; const inputClasses = classnames(baseClass, inputClassName, { diff --git a/frontend/components/forms/validators/valid_url/valid_url.ts b/frontend/components/forms/validators/valid_url/valid_url.ts index b9d177630d..a3d55f9d56 100644 --- a/frontend/components/forms/validators/valid_url/valid_url.ts +++ b/frontend/components/forms/validators/valid_url/valid_url.ts @@ -1,13 +1,19 @@ -const URL_REGEX_PATTERN = new RegExp( - "^(https?:\\/\\/)?" + // protocol - "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name - "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address - "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path - "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string - "(\\#[-a-z\\d_]*)?$", - "i" -); // fragment locator +interface IValidUrl { + url: string; + protocol?: string; +} -export default (url: string): boolean => { - return !!URL_REGEX_PATTERN.test(url); +export default ({ url, protocol }: IValidUrl): boolean => { + try { + const newUrl = new URL(url); + if (protocol === "http") { + return newUrl.protocol === "http:" || newUrl.protocol === "https:"; + } + if (protocol === "https") { + return newUrl.protocol === "https:"; + } + return true; + } catch (e) { + return false; + } }; diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index 77f5f35071..372c2ea209 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -111,36 +111,36 @@ export interface IConfigFormData { smtpAuthenticationMethod: string; smtpAuthenticationType: string; domain: string; - smtpEnableSSLTLS: boolean; - enableStartTLS: boolean; - serverURL: string; - orgLogoURL: string; + smtpEnableSslTls: boolean; + enableStartTls: boolean; + serverUrl: string; + orgLogoUrl: string; orgName: string; smtpPassword: string; smtpPort?: number; smtpSenderAddress: string; smtpServer: string; smtpUsername: string; - verifySSLCerts: boolean; - entityID: string; - issuerURI: string; - idpImageURL: string; + verifySslCerts: boolean; + entityId: string; + issuerUri: string; + idpImageUrl: string; metadata: string; - metadataURL: string; + metadataUrl: string; idpName: string; - enableSSO: boolean; - enableSSOIDPLogin: boolean; - enableSMTP: boolean; + enableSso: boolean; + enableSsoIdpLogin: boolean; + enableSmtp: boolean; enableHostExpiry: boolean; hostExpiryWindow: number; disableLiveQuery: boolean; agentOptions: any; enableHostStatusWebhook: boolean; - hostStatusWebhookDestinationURL?: string; + hostStatusWebhookDestinationUrl?: string; hostStatusWebhookHostPercentage?: number; hostStatusWebhookDaysCount?: number; enableUsageStatistics: boolean; - transparency_url: string; + transparencyUrl: string; } export interface IConfigFeatures { diff --git a/frontend/osquery_tables.json b/frontend/osquery_tables.json index 71d9725dda..26e027c916 100644 --- a/frontend/osquery_tables.json +++ b/frontend/osquery_tables.json @@ -2760,20 +2760,20 @@ "index": false }, { - "name":"issuer2", - "description":"Certificate issuer distinguished name", - "type":"text", - "hidden":true, - "required":false, - "index":false + "name": "issuer2", + "description": "Certificate issuer distinguished name", + "type": "text", + "hidden": true, + "required": false, + "index": false }, { - "name":"subject2", - "description":"Certificate distinguished name", - "type":"text", - "hidden":true, - "required":false, - "index":false + "name": "subject2", + "description": "Certificate distinguished name", + "type": "text", + "hidden": true, + "required": false, + "index": false } ] }, @@ -8196,12 +8196,12 @@ "index": false }, { - "name":"original_filename", - "description":"(Executable files only) Original filename", - "type":"text", - "hidden":true, - "required":false, - "index":false + "name": "original_filename", + "description": "(Executable files only) Original filename", + "type": "text", + "hidden": true, + "required": false, + "index": false }, { "name": "bsd_flags", @@ -16664,12 +16664,12 @@ "index": false }, { - "name":"translated", - "description":"Indicates whether the process is running under the Rosetta Translation Environment, yes=1, no=0, error=-1.", - "type":"integer", - "hidden":false, - "required":false, - "index":false + "name": "translated", + "description": "Indicates whether the process is running under the Rosetta Translation Environment, yes=1, no=0, error=-1.", + "type": "integer", + "hidden": false, + "required": false, + "index": false } ] }, diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx index 29f3c9b5e7..43b7658eab 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/components/IntegrationForm/IntegrationForm.tsx @@ -12,6 +12,8 @@ import { import Button from "components/buttons/Button"; // @ts-ignore import InputField from "components/forms/fields/InputField"; +import validUrl from "components/forms/validators/valid_url"; + import Spinner from "components/Spinner"; const baseClass = "integration-form"; @@ -86,8 +88,8 @@ const IntegrationForm = ({ const validateForm = () => { let error = null; - if (url.slice(0, 8) !== "https://") { - error = "URL must begin with https://"; + if (url && !validUrl({ url, protocol: "https" })) { + error = `${url} is not a valid HTTPS URL`; } setUrlError(error); @@ -226,7 +228,7 @@ const IntegrationForm = ({ tooltip={ "\ To find the Jira project key, head to your project in
\ - Jira. Your project key is in URL. For example, in
\ + Jira. Your project key is located in the URL. For example, in
\ “jira.example.com/projects/JRAEXAMPLE,”
\ “JRAEXAMPLE” is your project key. \ " diff --git a/frontend/pages/admin/OrgSettingsPage/_styles.scss b/frontend/pages/admin/OrgSettingsPage/_styles.scss index c6fa3cdbec..5823bf3d7c 100644 --- a/frontend/pages/admin/OrgSettingsPage/_styles.scss +++ b/frontend/pages/admin/OrgSettingsPage/_styles.scss @@ -138,7 +138,7 @@ } &:nth-child(2) { - padding-top: 10px; + padding-top: 6px; } &--checkbox { diff --git a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx index 72a45a0d5f..0f49ce4c01 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/FleetDesktop/FleetDesktop.tsx @@ -24,27 +24,25 @@ const FleetDesktop = ({ isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { const [formData, setFormData] = useState< - Pick + Pick >({ - transparency_url: + transparencyUrl: appConfig.fleet_desktop?.transparency_url || DEFAULT_TRANSPARENCY_URL, }); const [formErrors, setFormErrors] = useState({}); const handleInputChange = ({ value }: IFormField) => { - setFormData({ transparency_url: value.toString() }); + setFormData({ transparencyUrl: value.toString() }); setFormErrors({}); }; const validateForm = () => { - const { transparency_url } = formData; + const { transparencyUrl } = formData; const errors: IAppConfigFormErrors = {}; - if (!transparency_url) { - errors.transparency_url = "Transparency URL name must be present"; - } else if (!validUrl(transparency_url)) { - errors.transparency_url = `${transparency_url} is not a valid URL`; + if (transparencyUrl && !validUrl({ url: transparencyUrl })) { + errors.transparency_url = `${transparencyUrl} is not a valid URL`; } setFormErrors(errors); @@ -55,7 +53,7 @@ const FleetDesktop = ({ const formDataForAPI: Pick = { fleet_desktop: { - transparency_url: formData.transparency_url, + transparency_url: formData.transparencyUrl, }, }; @@ -75,10 +73,11 @@ const FleetDesktop = ({ label="Custom transparency URL" onChange={handleInputChange} name="transparency_url" - value={formData.transparency_url} + value={formData.transparencyUrl} parseTarget onBlur={validateForm} error={formErrors.transparency_url} + placeholder="https://fleetdm.com/transparency" />

When an end user clicks “Transparency” in the Fleet Desktop menu, by diff --git a/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx b/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx index eb7f8c440c..8dd1bab37e 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/HostStatusWebhook/HostStatusWebhook.tsx @@ -7,6 +7,8 @@ import Checkbox from "components/forms/fields/Checkbox"; import Dropdown from "components/forms/fields/Dropdown"; // @ts-ignore import InputField from "components/forms/fields/InputField"; +import validUrl from "components/forms/validators/valid_url"; + import Modal from "components/Modal"; import { IAppConfigFormProps, @@ -32,7 +34,7 @@ const HostStatusWebhook = ({ enableHostStatusWebhook: appConfig.webhook_settings.host_status_webhook .enable_host_status_webhook || false, - hostStatusWebhookDestinationURL: + hostStatusWebhookDestinationUrl: appConfig.webhook_settings.host_status_webhook.destination_url || "", hostStatusWebhookHostPercentage: appConfig.webhook_settings.host_status_webhook.host_percentage || @@ -43,7 +45,7 @@ const HostStatusWebhook = ({ const { enableHostStatusWebhook, - hostStatusWebhookDestinationURL, + hostStatusWebhookDestinationUrl, hostStatusWebhookHostPercentage, hostStatusWebhookDaysCount, } = formData; @@ -59,8 +61,10 @@ const HostStatusWebhook = ({ const errors: IAppConfigFormErrors = {}; if (enableHostStatusWebhook) { - if (!hostStatusWebhookDestinationURL) { + if (!hostStatusWebhookDestinationUrl) { errors.destination_url = "Destination URL must be present"; + } else if (!validUrl({ url: hostStatusWebhookDestinationUrl })) { + errors.server_url = `${hostStatusWebhookDestinationUrl} is not a valid URL`; } if (!hostStatusWebhookDaysCount) { @@ -92,7 +96,7 @@ const HostStatusWebhook = ({ webhook_settings: { host_status_webhook: { enable_host_status_webhook: enableHostStatusWebhook, - destination_url: hostStatusWebhookDestinationURL, + destination_url: hostStatusWebhookDestinationUrl, host_percentage: hostStatusWebhookHostPercentage, days_count: hostStatusWebhookDaysCount, }, @@ -171,8 +175,8 @@ const HostStatusWebhook = ({ placeholder="https://server.com/example" label="Destination URL" onChange={handleInputChange} - name="hostStatusWebhookDestinationURL" - value={hostStatusWebhookDestinationURL} + name="hostStatusWebhookDestinationUrl" + value={hostStatusWebhookDestinationUrl} parseTarget onBlur={validateForm} error={formErrors.destination_url} diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx index a99315c0f5..181c72049f 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Info/Info.tsx @@ -41,7 +41,7 @@ const Info = ({ errors.org_name = "Organization name must be present"; } - if (orgLogoURL && !validUrl(orgLogoURL)) { + if (orgLogoURL && !validUrl({ url: orgLogoURL, protocol: "http" })) { errors.org_logo_url = `${orgLogoURL} is not a valid URL`; } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx index 9c69a7ae90..ae5c2321a6 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Sso/Sso.tsx @@ -16,15 +16,15 @@ import { const baseClass = "app-config-form"; interface ISsoFormData { - enableSSO?: boolean; + enableSso?: boolean; idpName?: string; - entityID?: string; - issuerURI?: string; - idpImageURL?: string; + entityId?: string; + issuerUri?: string; + idpImageUrl?: string; metadata?: string; - metadataURL?: string; - enableSSOIDPLogin?: boolean; - enableJITProvisioning?: boolean; + metadataUrl?: string; + enableSsoIdpLogin?: boolean; + enableJitProvisioning?: boolean; } const Sso = ({ @@ -34,28 +34,28 @@ const Sso = ({ isUpdatingSettings, }: IAppConfigFormProps): JSX.Element => { const [formData, setFormData] = useState({ - enableSSO: appConfig.sso_settings.enable_sso ?? false, + enableSso: appConfig.sso_settings.enable_sso ?? false, idpName: appConfig.sso_settings.idp_name ?? "", - entityID: appConfig.sso_settings.entity_id ?? "", - issuerURI: appConfig.sso_settings.issuer_uri ?? "", - idpImageURL: appConfig.sso_settings.idp_image_url ?? "", + entityId: appConfig.sso_settings.entity_id ?? "", + issuerUri: appConfig.sso_settings.issuer_uri ?? "", + idpImageUrl: appConfig.sso_settings.idp_image_url ?? "", metadata: appConfig.sso_settings.metadata ?? "", - metadataURL: appConfig.sso_settings.metadata_url ?? "", - enableSSOIDPLogin: appConfig.sso_settings.enable_sso_idp_login ?? false, - enableJITProvisioning: + metadataUrl: appConfig.sso_settings.metadata_url ?? "", + enableSsoIdpLogin: appConfig.sso_settings.enable_sso_idp_login ?? false, + enableJitProvisioning: appConfig.sso_settings.enable_jit_provisioning ?? false, }); const { - enableSSO, + enableSso, idpName, - entityID, - issuerURI, - idpImageURL, + entityId, + issuerUri, + idpImageUrl, metadata, - metadataURL, - enableSSOIDPLogin, - enableJITProvisioning, + metadataUrl, + enableSsoIdpLogin, + enableJitProvisioning, } = formData; const [formErrors, setFormErrors] = useState({}); @@ -67,20 +67,25 @@ const Sso = ({ const validateForm = () => { const errors: IAppConfigFormErrors = {}; - if (enableSSO) { - if (idpImageURL && !validUrl(idpImageURL)) { - errors.idp_image_url = `${idpImageURL} is not a valid URL`; + if (enableSso) { + if (idpImageUrl && !validUrl({ url: idpImageUrl })) { + errors.idp_image_url = `${idpImageUrl} is not a valid URL`; } - if (metadata === "" && metadataURL === "") { - errors.metadata_url = "Metadata URL must be present"; + if (!metadata) { + if (!metadataUrl) { + errors.metadata_url = "Metadata or Metadata URL must be present"; + errors.metadata = "Metadata or Metadata URL must be present"; + } else if (!validUrl({ url: metadataUrl })) { + errors.metadata_url = `${metadataUrl} is not a valid URL`; + } } - if (!entityID) { + if (!entityId) { errors.entity_id = "Entity ID must be present"; } - if (typeof entityID === "string" && entityID.length < 5) { + if (typeof entityId === "string" && entityId.length < 5) { errors.entity_id = "Entity ID must be 5 or more characters"; } @@ -94,7 +99,7 @@ const Sso = ({ useEffect(() => { validateForm(); - }, [enableSSO]); + }, [idpImageUrl, metadata, metadataUrl, entityId, idpName]); const onFormSubmit = (evt: React.MouseEvent) => { evt.preventDefault(); @@ -102,15 +107,15 @@ const Sso = ({ // Formatting of API not UI const formDataToSubmit = { sso_settings: { - entity_id: entityID?.trim(), - issuer_uri: issuerURI?.trim(), - idp_image_url: idpImageURL?.trim(), + entity_id: entityId?.trim(), + issuer_uri: issuerUri?.trim(), + idp_image_url: idpImageUrl?.trim(), metadata: metadata?.trim(), - metadata_url: metadataURL?.trim(), + metadata_url: metadataUrl?.trim(), idp_name: idpName?.trim(), - enable_sso: enableSSO, - enable_sso_idp_login: enableSSOIDPLogin, - enable_jit_provisioning: enableJITProvisioning, + enable_sso: enableSso, + enable_sso_idp_login: enableSsoIdpLogin, + enable_jit_provisioning: enableJitProvisioning, }, }; @@ -124,8 +129,8 @@ const Sso = ({

Enable single sign-on @@ -153,8 +158,8 @@ const Sso = ({ } onChange={handleInputChange} - name="entityID" - value={entityID} + name="entityId" + value={entityId} parseTarget onBlur={validateForm} error={formErrors.entity_id} @@ -165,8 +170,8 @@ const Sso = ({ @@ -175,8 +180,8 @@ const Sso = ({
@@ -205,8 +211,8 @@ const Sso = ({ } onChange={handleInputChange} - name="metadataURL" - value={metadataURL} + name="metadataUrl" + value={metadataUrl} parseTarget onBlur={validateForm} error={formErrors.metadata_url} @@ -216,8 +222,8 @@ const Sso = ({
Allow SSO login initiated by identity provider @@ -227,8 +233,8 @@ const Sso = ({
<> diff --git a/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx b/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx index 048cc5d8c6..f8105774b9 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx @@ -3,6 +3,8 @@ import React, { useState } from "react"; import Button from "components/buttons/Button"; // @ts-ignore import InputField from "components/forms/fields/InputField"; +import validUrl from "components/forms/validators/valid_url"; + import { IAppConfigFormProps, IFormField, @@ -31,9 +33,10 @@ const WebAddress = ({ const validateForm = () => { const errors: IAppConfigFormErrors = {}; - if (!serverURL) { errors.server_url = "Fleet server URL must be present"; + } else if (!validUrl({ url: serverURL, protocol: "http" })) { + errors.server_url = `${serverURL} is not a valid URL`; } setFormErrors(errors); diff --git a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts index cb163259dd..88c7464afb 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/constants.ts +++ b/frontend/pages/admin/OrgSettingsPage/cards/constants.ts @@ -15,6 +15,7 @@ export interface IFormField { } export interface IAppConfigFormErrors { + metadata?: string | null; metadata_url?: string | null; entity_id?: string | null; idp_name?: string | null; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index e25ebe14fe..3c9a875feb 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -18,6 +18,7 @@ import Dropdown from "components/forms/fields/Dropdown"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import Radio from "components/forms/fields/Radio"; +import validUrl from "components/forms/validators/valid_url"; import PreviewPayloadModal from "../PreviewPayloadModal"; import PreviewTicketModal from "../PreviewTicketModal"; @@ -192,6 +193,8 @@ const ManageAutomationsModal = ({ if (isWebhookEnabled && !destinationUrl) { newErrors.url = "Please add a destination URL"; + } else if (!validUrl({ url: destinationUrl })) { + newErrors.url = `${destinationUrl} is not a valid URL`; } else { delete newErrors.url; } diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx index f4257a82d2..594593b4b3 100644 --- a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -26,6 +26,7 @@ import Slider from "components/forms/fields/Slider"; import Radio from "components/forms/fields/Radio"; // @ts-ignore import InputField from "components/forms/fields/InputField"; +import validUrl from "components/forms/validators/valid_url"; import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook"; import useDeepEffect from "hooks/useDeepEffect"; @@ -60,8 +61,12 @@ interface IManageAutomationsModalProps { const validateWebhookURL = (url: string) => { const errors: { [key: string]: string } = {}; - if (url === "") { + if (!url) { errors.url = "Please add a destination URL"; + } else if (!validUrl({ url })) { + errors.url = `${url} is not a valid URL`; + } else { + delete errors.url; } const valid = !size(errors); @@ -186,7 +191,7 @@ const ManageAutomationsModal = ({ const handleSaveAutomation = (evt: React.MouseEvent) => { evt.preventDefault(); - const { valid: validUrl, errors: newErrors } = validateWebhookURL( + const { valid: newValidUrl, errors: newErrors } = validateWebhookURL( destinationUrl ); setErrors({ @@ -232,7 +237,7 @@ const ManageAutomationsModal = ({ return; } if (!integrationEnabled) { - if (!validUrl) { + if (!newValidUrl) { return; } // set enable_vulnerabilities_webhook to true