diff --git a/changes/issue-5950-transparency-url b/changes/issue-5950-transparency-url new file mode 100644 index 0000000000..c00ae30179 --- /dev/null +++ b/changes/issue-5950-transparency-url @@ -0,0 +1 @@ +* Enable UI for Fleet Premium licensees to set custom transparancy url for Fleet Desktop \ No newline at end of file diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index 9ed5fdc8b4..9c02a3479a 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -696,6 +696,15 @@ describe( cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD); cy.visit("/settings/users"); }); + it("hides access to Fleet Desktop settings", () => { + cy.visit("settings/organization"); + cy.getAttached(".app-settings__form-nav-list").within(() => { + cy.findByText(/organization info/i).should("exist"); + cy.findByText(/fleet desktop/i).should("not.exist"); + }); + cy.visit("settings/organization/fleet-desktop"); + cy.findAllByText(/access denied/i).should("exist"); + }); it("hides access team settings", () => { cy.findByText(/teams/i).should("not.exist"); }); diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index 449afbf96b..2b4064d7ba 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -682,6 +682,26 @@ describe("Premium tier - Global Admin user", () => { cy.findByLabelText(/password/i).should("exist"); }); }); + it("allows access to Fleet Desktop settings", () => { + cy.visit("settings/organization"); + cy.getAttached(".app-settings__form-nav-list").within(() => { + cy.findByText(/organization info/i).should("exist"); + cy.findByText(/fleet desktop/i) + .should("exist") + .click(); + }); + cy.getAttached("[id=transparency_url") + .should("have.value", "https://fleetdm.com/transparency") + .clear() + .type("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" + ); + }); }); describe("User profile page", () => { it("renders elements according to role-based access controls", () => { diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index 9050943566..d11b0b534c 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -72,6 +72,10 @@ export default PropTypes.shape({ }), }); +export interface IFleetDesktopSettings { + transparency_url: string; +} + export interface IConfigFormData { smtpAuthenticationMethod: string; smtpAuthenticationType: string; @@ -105,6 +109,7 @@ export interface IConfigFormData { hostStatusWebhookHostPercentage?: number; hostStatusWebhookDaysCount?: number; enableUsageStatistics: boolean; + transparency_url: string; } export interface IConfig { @@ -162,6 +167,7 @@ export interface IConfig { expiration: string; note: string; }; + fleet_desktop: IFleetDesktopSettings; vulnerabilities: { databases_path: string; periodicity: number; diff --git a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx index 88c0aa72a2..a8dc559564 100644 --- a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx +++ b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useContext, useState, useEffect } from "react"; +import { useErrorHandler } from "react-error-boundary"; import { useQuery } from "react-query"; import { Params } from "react-router/lib/Router"; import { Link } from "react-router"; @@ -19,6 +20,7 @@ import AgentOptions from "./cards/Agents"; import HostStatusWebhook from "./cards/HostStatusWebhook"; import Statistics from "./cards/Statistics"; import Advanced from "./cards/Advanced"; +import FleetDesktop from "./cards/FleetDesktop"; interface IAppSettingsPageProps { params: Params; @@ -29,8 +31,10 @@ export const baseClass = "app-settings"; const AppSettingsPage = ({ params: { section: sectionTitle }, }: IAppSettingsPageProps): JSX.Element => { + const { isFreeTier, isPremiumTier, setConfig } = useContext(AppContext); const { renderFlash } = useContext(NotificationContext); - const { setConfig } = useContext(AppContext); + + const handlePageError = useErrorHandler(); const [activeSection, setActiveSection] = useState("info"); @@ -50,7 +54,7 @@ const AppSettingsPage = ({ }; const onFormSubmit = useCallback( - (formData: IConfig) => { + (formData: Partial) => { if (!appConfig) { return false; } @@ -87,10 +91,13 @@ const AppSettingsPage = ({ ); useEffect(() => { + if (isFreeTier && sectionTitle === "fleet-desktop") { + handlePageError({ status: 403 }); + } if (sectionTitle) { setActiveSection(sectionTitle); } - }, [sectionTitle]); + }, [isFreeTier, sectionTitle]); const renderSection = () => { if (!isLoading && appConfig) { @@ -123,6 +130,13 @@ const AppSettingsPage = ({ {activeSection === "advanced" && ( )} + {isPremiumTier && activeSection === "fleet-desktop" && ( + + )} ); } @@ -208,6 +222,18 @@ const AppSettingsPage = ({ Usage statistics + {isPremiumTier && ( +
  • + + Fleet Desktop + +
  • + )}
  • { + const [formData, setFormData] = useState< + Pick + >({ + transparency_url: + appConfig.fleet_desktop?.transparency_url || DEFAULT_TRANSPARENCY_URL, + }); + + const [formErrors, setFormErrors] = useState({}); + + const handleInputChange = ({ value }: IFormField) => { + setFormData({ transparency_url: value.toString() }); + setFormErrors({}); + }; + + const validateForm = () => { + const { transparency_url } = 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`; + } + + setFormErrors(errors); + }; + + const onFormSubmit = (evt: React.MouseEvent) => { + evt.preventDefault(); + + const formDataForAPI: Pick = { + fleet_desktop: { + transparency_url: formData.transparency_url, + }, + }; + + handleSubmit(formDataForAPI); + }; + + if (!isPremiumTier) { + return <>; + } + + return ( +
    +
    +

    Fleet Desktop

    +
    + +

    + When an end user clicks “Transparency” in the Fleet Desktop menu, by + default they are taken to{" "} + + {" "} + https://fleetdm.com/transparency{" "} + open new tab + {" "} + . You can override the URL to take them to a resource of your + choice. +

    +
    +
    + +
    + ); +}; + +export default FleetDesktop; diff --git a/frontend/pages/admin/AppSettingsPage/cards/FleetDesktop/index.ts b/frontend/pages/admin/AppSettingsPage/cards/FleetDesktop/index.ts new file mode 100644 index 0000000000..a928d3485a --- /dev/null +++ b/frontend/pages/admin/AppSettingsPage/cards/FleetDesktop/index.ts @@ -0,0 +1 @@ +export { default } from "./FleetDesktop"; diff --git a/frontend/pages/admin/AppSettingsPage/cards/constants.ts b/frontend/pages/admin/AppSettingsPage/cards/constants.ts index 5ce5d0684b..194ae254dc 100644 --- a/frontend/pages/admin/AppSettingsPage/cards/constants.ts +++ b/frontend/pages/admin/AppSettingsPage/cards/constants.ts @@ -1,7 +1,10 @@ import { IConfig } from "interfaces/config"; +export const DEFAULT_TRANSPARENCY_URL = "https://fleetdm.com/transparency"; + export interface IAppConfigFormProps { appConfig: IConfig; + isPremiumTier?: boolean; handleSubmit: any; } @@ -26,6 +29,7 @@ export interface IAppConfigFormErrors { destination_url?: string | null; host_expiry_window?: string | null; agent_options?: string | null; + transparency_url?: string | null; } export const authMethodOptions = [ diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 25159adb6b..10a418d918 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -18,6 +18,7 @@ export default { ADMIN_SETTINGS_HOST_STATUS_WEBHOOK: `${URL_PREFIX}/settings/organization/host-status-webhook`, ADMIN_SETTINGS_STATISTICS: `${URL_PREFIX}/settings/organization/statistics`, ADMIN_SETTINGS_ADVANCED: `${URL_PREFIX}/settings/organization/advanced`, + ADMIN_SETTINGS_FLEET_DESKTOP: `${URL_PREFIX}/settings/organization/fleet-desktop`, ALL_PACKS: `${URL_PREFIX}/packs/all`, EDIT_PACK: (packId: number): string => { return `${URL_PREFIX}/packs/${packId}/edit`;