Fleet UI: Disable all live query/policy buttons when globally disabled (#14821)

This commit is contained in:
RachelElysia 2023-11-01 09:25:39 -04:00 committed by GitHub
parent ae6c387059
commit 9ad0962de2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 275 additions and 53 deletions

View File

@ -0,0 +1 @@
- If live queries are disabled globally, all UI buttons to run a live query or policy are disabled with a tooltip explanation

View File

@ -55,6 +55,7 @@ const PolicyPage = ({
isGlobalAdmin, isGlobalAdmin,
isGlobalMaintainer, isGlobalMaintainer,
isAnyTeamMaintainerOrTeamAdmin, isAnyTeamMaintainerOrTeamAdmin,
config,
} = useContext(AppContext); } = useContext(AppContext);
const { const {
lastEditedQueryBody, lastEditedQueryBody,
@ -225,7 +226,7 @@ const PolicyPage = ({
}; };
const renderLiveQueryWarning = (): JSX.Element | null => { const renderLiveQueryWarning = (): JSX.Element | null => {
if (isLiveQueryRunnable) { if (isLiveQueryRunnable || config?.server_settings.live_query_disabled) {
return null; return null;
} }

View File

@ -4,6 +4,7 @@ import { createCustomRenderer } from "test/test-utils";
import createMockPolicy from "__mocks__/policyMock"; import createMockPolicy from "__mocks__/policyMock";
import createMockUser from "__mocks__/userMock"; import createMockUser from "__mocks__/userMock";
import createMockConfig from "__mocks__/configMock";
import PolicyForm from "./PolicyForm"; import PolicyForm from "./PolicyForm";
@ -38,6 +39,7 @@ describe("PolicyForm - component", () => {
isOnGlobalTeam: true, isOnGlobalTeam: true,
isPremiumTier: true, isPremiumTier: true,
isSandboxMode: false, isSandboxMode: false,
config: createMockConfig(),
}, },
}, },
}); });
@ -93,6 +95,7 @@ describe("PolicyForm - component", () => {
isOnGlobalTeam: true, isOnGlobalTeam: true,
isPremiumTier: true, isPremiumTier: true,
isSandboxMode: false, isSandboxMode: false,
config: createMockConfig(),
}, },
}, },
}); });
@ -122,9 +125,76 @@ describe("PolicyForm - component", () => {
await user.hover(screen.getByRole("button", { name: "Save" })); await user.hover(screen.getByRole("button", { name: "Save" }));
expect( expect(container.querySelector("#save-policy-button")).toHaveTextContent(
container.querySelector("#policy-form__button-wrap--tooltip") /to save or run the policy/i
).toHaveTextContent(/to save or run the policy/i); );
});
it("disables run button with tooltip for globally disabled queries", async () => {
const render = createCustomRenderer({
context: {
policy: {
policyTeamId: undefined,
lastEditedQueryId: mockPolicy.id,
lastEditedQueryName: mockPolicy.name,
lastEditedQueryDescription: mockPolicy.description,
lastEditedQueryBody: mockPolicy.query,
lastEditedQueryResolution: mockPolicy.resolution,
lastEditedQueryCritical: mockPolicy.critical,
lastEditedQueryPlatform: undefined, // missing policy platforms
defaultPolicy: false,
setLastEditedQueryName: jest.fn(),
setLastEditedQueryDescription: jest.fn(),
setLastEditedQueryBody: jest.fn(),
setLastEditedQueryResolution: jest.fn(),
setLastEditedQueryCritical: jest.fn(),
setLastEditedQueryPlatform: jest.fn(),
},
app: {
currentUser: createMockUser(),
isGlobalObserver: false,
isGlobalAdmin: true,
isGlobalMaintainer: false,
isOnGlobalTeam: true,
isPremiumTier: true,
isSandboxMode: false,
config: createMockConfig({
server_settings: {
...createMockConfig().server_settings,
live_query_disabled: true, // Live query disabled
},
}),
},
},
});
const { container, user } = render(
<PolicyForm
policyIdForEdit={mockPolicy.id}
showOpenSchemaActionText={false}
storedPolicy={createMockPolicy()}
isStoredPolicyLoading={false}
isTeamAdmin={false}
isTeamMaintainer={false}
isTeamObserver={false}
isUpdatingPolicy={false}
onCreatePolicy={jest.fn()}
onOsqueryTableSelect={jest.fn()}
goToSelectTargets={jest.fn()}
onUpdate={jest.fn()}
onOpenSchemaSidebar={jest.fn()}
renderLiveQueryWarning={jest.fn()}
backendValidators={{}}
/>
);
expect(screen.getByRole("button", { name: "Run" })).toBeDisabled();
await user.hover(screen.getByRole("button", { name: "Run" }));
expect(container.querySelector("#run-policy-button")).toHaveTextContent(
/live queries are disabled/i
);
}); });
// TODO: Consider testing save button is disabled for a sql error // TODO: Consider testing save button is disabled for a sql error

View File

@ -6,6 +6,7 @@ import ReactTooltip from "react-tooltip";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { size } from "lodash"; import { size } from "lodash";
import classnames from "classnames"; import classnames from "classnames";
import { COLORS } from "styles/var/colors";
import { addGravatarUrlToResource } from "utilities/helpers"; import { addGravatarUrlToResource } from "utilities/helpers";
import { AppContext } from "context/app"; import { AppContext } from "context/app";
@ -117,6 +118,7 @@ const PolicyForm = ({
isOnGlobalTeam, isOnGlobalTeam,
isPremiumTier, isPremiumTier,
isSandboxMode, isSandboxMode,
config,
} = useContext(AppContext); } = useContext(AppContext);
const debounceSQL = useDebouncedCallback((sql: string) => { const debounceSQL = useDebouncedCallback((sql: string) => {
@ -152,6 +154,8 @@ const PolicyForm = ({
!policyIdForEdit && !policyIdForEdit &&
DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName); DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName);
const disabledLiveQuery = config?.server_settings.live_query_disabled;
useEffect(() => { useEffect(() => {
if (isNewTemplatePolicy) { if (isNewTemplatePolicy) {
setCompatiblePlatforms(lastEditedQueryBody); setCompatiblePlatforms(lastEditedQueryBody);
@ -550,7 +554,7 @@ const PolicyForm = ({
<span <span
className={`${baseClass}__button-wrap--tooltip`} className={`${baseClass}__button-wrap--tooltip`}
data-tip data-tip
data-for={`${baseClass}__button-wrap--tooltip`} data-for="save-policy-button"
data-tip-disable={!isEditMode || isAnyPlatformSelected} data-tip-disable={!isEditMode || isAnyPlatformSelected}
> >
<Button <Button
@ -567,8 +571,8 @@ const PolicyForm = ({
className={`${baseClass}__button-wrap--tooltip`} className={`${baseClass}__button-wrap--tooltip`}
place="bottom" place="bottom"
effect="solid" effect="solid"
id={`${baseClass}__button-wrap--tooltip`} id="save-policy-button"
backgroundColor="#3e4771" backgroundColor={COLORS["tooltip-bg"]}
> >
Select the platform(s) this Select the platform(s) this
<br /> <br />
@ -581,14 +585,18 @@ const PolicyForm = ({
<span <span
className={`${baseClass}__button-wrap--tooltip`} className={`${baseClass}__button-wrap--tooltip`}
data-tip data-tip
data-for={`${baseClass}__button-wrap--tooltip`} data-for="run-policy-button"
data-tip-disable={!isEditMode || isAnyPlatformSelected} data-tip-disable={
(!isEditMode || isAnyPlatformSelected) && !disabledLiveQuery
}
> >
<Button <Button
className={`${baseClass}__run`} className={`${baseClass}__run`}
variant="blue-green" variant="blue-green"
onClick={goToSelectTargets} onClick={goToSelectTargets}
disabled={isEditMode && !isAnyPlatformSelected} disabled={
(isEditMode && !isAnyPlatformSelected) || disabledLiveQuery
}
> >
Run Run
</Button> </Button>
@ -597,14 +605,19 @@ const PolicyForm = ({
className={`${baseClass}__button-wrap--tooltip`} className={`${baseClass}__button-wrap--tooltip`}
place="bottom" place="bottom"
effect="solid" effect="solid"
id={`${baseClass}__button-wrap--tooltip`} id="run-policy-button"
backgroundColor="#3e4771" backgroundColor={COLORS["tooltip-bg"]}
data-html
> >
Select the platform(s) this {disabledLiveQuery ? (
<br /> <>Live queries are disabled in organization settings</>
policy will be checked on ) : (
<br /> <>
to save or run the policy. Select the platform(s) this <br />
policy will be checked on <br />
to save or run the policy.
</>
)}
</ReactTooltip> </ReactTooltip>
</div> </div>
</form> </form>

View File

@ -2,6 +2,8 @@ import React, { useContext, useState, useEffect } from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router"; import { InjectedRouter, Params } from "react-router/lib/Router";
import { useErrorHandler } from "react-error-boundary"; import { useErrorHandler } from "react-error-boundary";
import ReactTooltip from "react-tooltip";
import { COLORS } from "styles/var/colors";
import PATHS from "router/paths"; import PATHS from "router/paths";
import { AppContext } from "context/app"; import { AppContext } from "context/app";
@ -171,6 +173,7 @@ const QueryDetailsPage = ({
const isApiError = storedQueryError || queryReportError; const isApiError = storedQueryError || queryReportError;
const isClipped = const isClipped =
(queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT; (queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT;
const disabledLiveQuery = config?.server_settings.live_query_disabled;
const renderHeader = () => { const renderHeader = () => {
const canEditQuery = const canEditQuery =
@ -216,15 +219,33 @@ const QueryDetailsPage = ({
<div <div
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--new-query`} className={`${baseClass}__button-wrap ${baseClass}__button-wrap--new-query`}
> >
<Button <div
className={`${baseClass}__run`} data-tip
variant="blue-green" data-for="live-query-button"
onClick={() => { // Tooltip shows when live queries are globally disabled
queryId && router.push(PATHS.LIVE_QUERY(queryId)); data-tip-disable={!disabledLiveQuery}
}}
> >
Live query <Button
</Button> className={`${baseClass}__run`}
variant="blue-green"
onClick={() => {
queryId && router.push(PATHS.LIVE_QUERY(queryId));
}}
disabled={disabledLiveQuery}
>
Live query
</Button>
</div>
<ReactTooltip
className="live-query-button-tooltip"
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="live-query-button"
data-html
>
Live queries are disabled in organization settings
</ReactTooltip>
</div> </div>
)} )}
</div> </div>

View File

@ -68,6 +68,7 @@ const EditQueryPage = ({
isAnyTeamMaintainerOrTeamAdmin, isAnyTeamMaintainerOrTeamAdmin,
isObserverPlus, isObserverPlus,
isAnyTeamObserverPlus, isAnyTeamObserverPlus,
config,
} = useContext(AppContext); } = useContext(AppContext);
const { const {
selectedOsqueryTable, selectedOsqueryTable,
@ -293,7 +294,7 @@ const EditQueryPage = ({
}; };
const renderLiveQueryWarning = (): JSX.Element | null => { const renderLiveQueryWarning = (): JSX.Element | null => {
if (isLiveQueryRunnable) { if (isLiveQueryRunnable || config?.server_settings.live_query_disabled) {
return null; return null;
} }

View File

@ -4,6 +4,7 @@ import { createCustomRenderer } from "test/test-utils";
import createMockQuery from "__mocks__/queryMock"; import createMockQuery from "__mocks__/queryMock";
import createMockUser from "__mocks__/userMock"; import createMockUser from "__mocks__/userMock";
import createMockConfig from "__mocks__/configMock";
import EditQueryForm from "./EditQueryForm"; import EditQueryForm from "./EditQueryForm";
@ -51,6 +52,7 @@ describe("EditQueryForm - component", () => {
isOnGlobalTeam: true, isOnGlobalTeam: true,
isPremiumTier: true, isPremiumTier: true,
isSandboxMode: false, isSandboxMode: false,
config: createMockConfig(),
}, },
}, },
}); });
@ -80,6 +82,76 @@ describe("EditQueryForm - component", () => {
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
}); });
it("disables live query button for globally disabled live queries", async () => {
const render = createCustomRenderer({
context: {
query: {
lastEditedQueryId: mockQuery.id,
lastEditedQueryName: "", // missing query name
lastEditedQueryDescription: mockQuery.description,
lastEditedQueryBody: mockQuery.query,
lastEditedQueryObserverCanRun: mockQuery.observer_can_run,
lastEditedQueryFrequency: mockQuery.interval,
lastEditedQueryPlatforms: mockQuery.platform,
lastEditedQueryMinOsqueryVersion: mockQuery.min_osquery_version,
lastEditedQueryLoggingType: mockQuery.logging,
setLastEditedQueryName: jest.fn(),
setLastEditedQueryDescription: jest.fn(),
setLastEditedQueryBody: jest.fn(),
setLastEditedQueryObserverCanRun: jest.fn(),
setLastEditedQueryFrequency: jest.fn(),
setLastEditedQueryPlatforms: jest.fn(),
setLastEditedQueryMinOsqueryVersion: jest.fn(),
setLastEditedQueryLoggingType: jest.fn(),
},
app: {
currentUser: createMockUser(),
isGlobalObserver: false,
isGlobalAdmin: true,
isGlobalMaintainer: false,
isOnGlobalTeam: true,
isPremiumTier: true,
isSandboxMode: false,
config: createMockConfig({
server_settings: {
...createMockConfig().server_settings,
live_query_disabled: true, // Live query disabled
},
}),
},
},
});
const { container, user } = render(
<EditQueryForm
router={mockRouter}
queryIdForEdit={1}
apiTeamIdForQuery={1}
teamNameForQuery={"Apples"}
showOpenSchemaActionText
storedQuery={createMockQuery({ name: "Mock query" })}
isStoredQueryLoading={false}
isQuerySaving={false}
isQueryUpdating={false}
onSubmitNewQuery={jest.fn()}
onOsqueryTableSelect={jest.fn()}
onUpdate={jest.fn()}
onOpenSchemaSidebar={jest.fn()}
renderLiveQueryWarning={jest.fn()}
backendValidators={{}}
showConfirmSaveChangesModal={false}
setShowConfirmSaveChangesModal={jest.fn()}
/>
);
expect(screen.getByRole("button", { name: "Live query" })).toBeDisabled();
await user.hover(screen.getByRole("button", { name: "Live query" }));
expect(container.querySelector("#live-query-button")).toHaveTextContent(
/live queries are disabled/i
);
});
// TODO: Consider testing save button is disabled for a sql error // TODO: Consider testing save button is disabled for a sql error
// Trickiness is in modifying react-ace using react-testing library // Trickiness is in modifying react-ace using react-testing library
}); });

View File

@ -161,10 +161,12 @@ const EditQueryForm = ({
isGlobalMaintainer, isGlobalMaintainer,
isObserverPlus, isObserverPlus,
isAnyTeamObserverPlus, isAnyTeamObserverPlus,
config,
} = useContext(AppContext); } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext); const { renderFlash } = useContext(NotificationContext);
const savedQueryMode = !!queryIdForEdit; const savedQueryMode = !!queryIdForEdit;
const disabledLiveQuery = config?.server_settings.live_query_disabled;
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
// NOTE: SaveQueryModal is only being used to create a new query in this component. // NOTE: SaveQueryModal is only being used to create a new query in this component.
// It's easy to confuse with other names like promptSaveQuery, promptSaveAsNewQuery, etc., // It's easy to confuse with other names like promptSaveQuery, promptSaveAsNewQuery, etc.,
@ -589,18 +591,36 @@ const EditQueryForm = ({
<div <div
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--new-query`} className={`${baseClass}__button-wrap ${baseClass}__button-wrap--new-query`}
> >
<Button <div
className={`${baseClass}__run`} data-tip
variant="blue-green" data-for="live-query-button"
onClick={() => { // Tooltip shows when live queries are globally disabled
router.push( data-tip-disable={!disabledLiveQuery}
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
}}
> >
Live query <Button
</Button> className={`${baseClass}__run`}
variant="blue-green"
onClick={() => {
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
}}
disabled={disabledLiveQuery}
>
Live query
</Button>
</div>
<ReactTooltip
className={`live-query-button-tooltip`}
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="live-query-button"
data-html
>
Live queries are disabled in organization settings
</ReactTooltip>
</div> </div>
)} )}
</form> </form>
@ -808,18 +828,36 @@ const EditQueryForm = ({
</div> </div>
</> </>
)} )}
<Button <div
className={`${baseClass}__run`} data-tip
variant="blue-green" data-for="live-query-button"
onClick={() => { // Tooltip shows when live queries are globally disabled
router.push( data-tip-disable={!disabledLiveQuery}
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
}}
> >
Live query <Button
</Button> className={`${baseClass}__run`}
variant="blue-green"
onClick={() => {
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId)
);
}}
disabled={disabledLiveQuery}
>
Live query
</Button>
</div>
<ReactTooltip
className={`live-query-button-tooltip`}
place="top"
effect="solid"
backgroundColor={COLORS["tooltip-bg"]}
id="live-query-button"
data-html
>
Live queries are disabled in organization settings
</ReactTooltip>
</div> </div>
</form> </form>
{showSaveQueryModal && ( {showSaveQueryModal && (

View File

@ -59,6 +59,7 @@ const RunQueryPage = ({
isAnyTeamMaintainerOrTeamAdmin, isAnyTeamMaintainerOrTeamAdmin,
isObserverPlus, isObserverPlus,
isAnyTeamObserverPlus, isAnyTeamObserverPlus,
config,
} = useContext(AppContext); } = useContext(AppContext);
const { const {
selectedQueryTargets, selectedQueryTargets,
@ -90,6 +91,15 @@ const RunQueryPage = ({
const [targetsTotalCount, setTargetsTotalCount] = useState(0); const [targetsTotalCount, setTargetsTotalCount] = useState(0);
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true); const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
const disabledLiveQuery = config?.server_settings.live_query_disabled;
// Reroute users out of live flow when live queries are globally disabled
if (disabledLiveQuery) {
queryId
? router.push(PATHS.QUERY(queryId))
: router.push(PATHS.NEW_QUERY());
}
// disabled on page load so we can control the number of renders // disabled on page load so we can control the number of renders
// else it will re-populate the context on occasion // else it will re-populate the context on occasion
const { data: storedQuery } = useQuery< const { data: storedQuery } = useQuery<
@ -155,11 +165,6 @@ const RunQueryPage = ({
}); });
}, [targetedLabels, targetedHosts, targetedTeams]); }, [targetedLabels, targetedHosts, targetedTeams]);
console.log(
"LiveQueryPage.tsx: selectedQueryTargetsByType",
selectedQueryTargetsByType
);
// Updates title that shows up on browser tabs // Updates title that shows up on browser tabs
useEffect(() => { useEffect(() => {
// e.g., Run live query | Discover TLS certificates | Fleet for osquery // e.g., Run live query | Discover TLS certificates | Fleet for osquery