fleet/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx

457 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useContext, useEffect, useState } from "react";
import { Link } from "react-router";
import { useQuery } from "react-query";
import { useDispatch } from "react-redux";
import { noop } from "lodash";
2021-08-30 23:02:53 +00:00
// @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import PATHS from "router/paths";
2021-08-30 23:02:53 +00:00
import { IPolicy } from "interfaces/policy";
import { ITeam } from "interfaces/team";
import { IUser } from "interfaces/user";
2021-08-30 23:02:53 +00:00
import { AppContext } from "context/app";
import fleetQueriesAPI from "services/entities/queries";
import globalPoliciesAPI from "services/entities/global_policies";
import teamsAPI from "services/entities/teams";
import teamPoliciesAPI from "services/entities/team_policies";
2021-08-30 23:02:53 +00:00
import { inMilliseconds, secondsToHms } from "fleet/helpers";
import sortUtils from "utilities/sort";
import permissionsUtils from "utilities/permissions";
2021-08-30 23:02:53 +00:00
import TableDataError from "components/TableDataError";
2021-08-30 23:02:53 +00:00
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
import IconToolTip from "components/IconToolTip";
2021-08-30 23:02:53 +00:00
import PoliciesListWrapper from "./components/PoliciesListWrapper";
import RemovePoliciesModal from "./components/RemovePoliciesModal";
import TeamsDropdown from "./components/TeamsDropdown";
2021-08-30 23:02:53 +00:00
const baseClass = "manage-policies-page";
const DOCS_LINK =
"https://fleetdm.com/docs/deploying/configuration#osquery-policy-update-interval";
2021-08-30 23:02:53 +00:00
2021-10-02 00:35:13 +00:00
const renderInheritedPoliciesButtonText = (
showPolicies: boolean,
policies: IPolicy[]
) => {
const count = policies.length;
return `${showPolicies ? "Hide" : "Show"} ${count} inherited ${
count > 1 ? "policies" : "policy"
}`;
};
const ManagePolicyPage = (managePoliciesPageProps: {
router: any;
location: any;
}): JSX.Element => {
const { location, router } = managePoliciesPageProps;
2021-08-30 23:02:53 +00:00
const dispatch = useDispatch();
const {
config,
currentUser,
isAnyTeamMaintainerOrTeamAdmin,
isGlobalAdmin,
isGlobalMaintainer,
isOnGlobalTeam,
isFreeTier,
isPremiumTier,
} = useContext(AppContext);
2021-08-30 23:02:53 +00:00
const { isTeamMaintainer, isTeamAdmin } = permissionsUtils;
const canAddOrRemovePolicy = (user: IUser | null, teamId: number | null) =>
isGlobalAdmin ||
isGlobalMaintainer ||
isTeamMaintainer(user, teamId) ||
isTeamAdmin(user, teamId);
const { data: teams } = useQuery(["teams"], () => teamsAPI.loadAll({}), {
enabled: !!isPremiumTier,
select: (data) => data.teams,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
const { data: fleetQueries } = useQuery(
["fleetQueries"],
() => fleetQueriesAPI.loadAll(),
{
select: (data) => data.queries,
refetchOnMount: false,
refetchOnWindowFocus: false,
}
);
// ===== local state
const [globalPolicies, setGlobalPolicies] = useState<IPolicy[] | never[]>([]);
const [isLoadingGlobalPolicies, setIsLoadingGlobalPolicies] = useState(true);
const [isGlobalPoliciesError, setIsGlobalPoliciesError] = useState(false);
const [teamPolicies, setTeamPolicies] = useState<IPolicy[] | never[]>([]);
const [isLoadingTeamPolicies, setIsLoadingTeamPolicies] = useState(true);
const [isTeamPoliciesError, setIsTeamPoliciesError] = useState(false);
const [userTeams, setUserTeams] = useState<ITeam[] | never[] | null>(null);
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(
parseInt(location?.query?.team_id, 10) || null
);
const [selectedPolicyIds, setSelectedPolicyIds] = useState<
number[] | never[]
>([]);
2021-08-30 23:02:53 +00:00
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
const [showInheritedPolicies, setShowInheritedPolicies] = useState(false);
2021-08-30 23:02:53 +00:00
const [updateInterval, setUpdateInterval] = useState<string>(
"osquery policy update interval"
2021-08-30 23:02:53 +00:00
);
// ===== local state
2021-08-30 23:02:53 +00:00
const getGlobalPolicies = useCallback(async () => {
setIsLoadingGlobalPolicies(true);
setIsGlobalPoliciesError(false);
let result;
2021-08-30 23:02:53 +00:00
try {
result = await globalPoliciesAPI
.loadAll()
.then((response) => response.policies);
setGlobalPolicies(result);
2021-08-30 23:02:53 +00:00
} catch (error) {
console.log(error);
setIsGlobalPoliciesError(true);
2021-08-30 23:02:53 +00:00
} finally {
setIsLoadingGlobalPolicies(false);
2021-08-30 23:02:53 +00:00
}
return result;
}, []);
2021-08-30 23:02:53 +00:00
const getTeamPolicies = useCallback(async (teamId) => {
setIsLoadingTeamPolicies(true);
setIsTeamPoliciesError(false);
let result;
2021-08-30 23:02:53 +00:00
try {
result = await teamPoliciesAPI
.loadAll(teamId)
.then((response) => response.policies);
setTeamPolicies(result);
2021-08-30 23:02:53 +00:00
} catch (error) {
console.log(error);
setIsTeamPoliciesError(true);
} finally {
setIsLoadingTeamPolicies(false);
2021-08-30 23:02:53 +00:00
}
return result;
}, []);
2021-08-30 23:02:53 +00:00
const getPolicies = useCallback(
(teamId) => {
return teamId ? getTeamPolicies(teamId) : getGlobalPolicies();
},
[getGlobalPolicies, getTeamPolicies]
);
2021-08-30 23:02:53 +00:00
2021-10-02 00:35:13 +00:00
const handleChangeSelectedTeam = (id: number) => {
const { MANAGE_POLICIES } = PATHS;
const path = id ? `${MANAGE_POLICIES}?team_id=${id}` : MANAGE_POLICIES;
router.replace(path);
setShowInheritedPolicies(false);
setSelectedPolicyIds([]);
};
2021-08-30 23:02:53 +00:00
const toggleRemovePoliciesModal = () =>
2021-08-30 23:02:53 +00:00
setShowRemovePoliciesModal(!showRemovePoliciesModal);
const toggleShowInheritedPolicies = () =>
setShowInheritedPolicies(!showInheritedPolicies);
const onRemovePoliciesClick = (selectedTableIds: number[]): void => {
toggleRemovePoliciesModal();
setSelectedPolicyIds(selectedTableIds);
};
const onRemovePoliciesSubmit = async () => {
try {
const request = selectedTeamId
? teamPoliciesAPI.destroy(selectedTeamId, selectedPolicyIds)
: globalPoliciesAPI.destroy(selectedPolicyIds);
2021-08-30 23:02:53 +00:00
await request.then(() => {
2021-08-30 23:02:53 +00:00
dispatch(
renderFlash(
"success",
`Successfully removed ${
selectedPolicyIds?.length === 1 ? "policy" : "policies"
2021-08-30 23:02:53 +00:00
}.`
)
);
});
} catch {
dispatch(
renderFlash(
"error",
`Unable to remove ${
selectedPolicyIds?.length === 1 ? "policy" : "policies"
}. Please try again.`
)
);
} finally {
toggleRemovePoliciesModal();
getPolicies(selectedTeamId);
}
};
2021-08-30 23:02:53 +00:00
// Sort list of teams the current user has permission to access and set as userTeams.
useEffect(() => {
if (isPremiumTier) {
let unsortedTeams: ITeam[] | null = null;
if (isOnGlobalTeam && teams) {
unsortedTeams = teams;
} else if (!isOnGlobalTeam && currentUser?.teams) {
unsortedTeams = currentUser.teams;
}
if (unsortedTeams !== null) {
2021-10-02 00:35:13 +00:00
const sortedTeams = unsortedTeams.sort((a, b) =>
sortUtils.caseInsensitiveAsc(a.name, b.name)
2021-08-30 23:02:53 +00:00
);
setUserTeams(sortedTeams);
2021-08-30 23:02:53 +00:00
}
}
}, [currentUser, isOnGlobalTeam, isPremiumTier, teams]);
// Watch the location url and parse team param to set selectedTeamId.
// Note 0 is used as the id for the "All teams" option.
// Null case is used to represent no valid id has been selected.
useEffect(() => {
let teamId: number | null = parseInt(location?.query?.team_id, 10) || 0;
// If the team id does not match one in the user teams list,
// we use a default value and change call change handler
// to update url params with the default value.
// We return early to guard against potential invariant condition.
if (userTeams && !userTeams.find((t) => t.id === teamId)) {
if (isOnGlobalTeam) {
// For global users, default to zero (i.e. all teams).
if (teamId !== 0) {
handleChangeSelectedTeam(0);
return;
}
} else {
// For non-global users, default to the first team in the list.
// If there is no default team, set teamId to null so that getPolicies
// API request will not be triggered.
teamId = userTeams[0]?.id || null;
if (teamId) {
handleChangeSelectedTeam(teamId);
return;
}
}
}
// Null case must be distinguished from 0 (which is used as the id for the "All teams" option)
// so a falsiness check cannot be used here. Null case here allows us to skip API call
// that would be triggered on a change to selectedTeamId.
teamId !== null && setSelectedTeamId(teamId);
2021-10-02 00:35:13 +00:00
}, [isOnGlobalTeam, location, userTeams]);
// Watch for selected team changes and call getPolicies to make new policies API request.
useEffect(() => {
// Null case must be distinguished from 0 (which is used as the id for the "All teams" option)
// so a falsiness check cannot be used here. Null case here allows us to skip API call.
if (selectedTeamId !== null) {
if (isOnGlobalTeam || isAnyTeamMaintainerOrTeamAdmin) {
getGlobalPolicies();
}
if (selectedTeamId) {
getTeamPolicies(selectedTeamId);
}
}
2021-10-02 00:35:13 +00:00
}, [
getGlobalPolicies,
getTeamPolicies,
isAnyTeamMaintainerOrTeamAdmin,
2021-10-02 00:35:13 +00:00
isOnGlobalTeam,
selectedTeamId,
]);
// Pull osquery policy update interval value from config, reformat, and set as updateInterval.
useEffect(() => {
if (config) {
const { osquery_policy: interval } = config;
interval &&
setUpdateInterval(secondsToHms(inMilliseconds(interval) / 1000));
}
}, [config]);
2021-08-30 23:02:53 +00:00
2021-10-02 00:35:13 +00:00
// If the user is free tier or if there is no selected team, we show the default description.
// We also want to check selectTeamId for the null case so that we don't render the element prematurely.
const showDefaultDescription =
isFreeTier || (isPremiumTier && !selectedTeamId && selectedTeamId !== null);
// If there aren't any policies of if there are loading errors, we don't show the update interval info banner.
// We also want to check selectTeamId for the null case so that we don't render the element prematurely.
const showInfoBanner =
(selectedTeamId && !isTeamPoliciesError && !!teamPolicies?.length) ||
(!selectedTeamId &&
selectedTeamId !== null &&
!isGlobalPoliciesError &&
!!globalPolicies?.length);
// If there aren't any policies of if there are loading errors, we don't show the inherited policies button.
const showInheritedPoliciesButton =
!!selectedTeamId && !!globalPolicies?.length && !isGlobalPoliciesError;
2021-10-02 00:35:13 +00:00
const selectedTeamData = userTeams?.find(
(team) => selectedTeamId === team.id
);
2021-08-30 23:02:53 +00:00
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper body-wrap`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<div className={`${baseClass}__title`}>
{isFreeTier && <h1>Policies</h1>}
{isPremiumTier &&
userTeams !== null &&
selectedTeamId !== null && (
<TeamsDropdown
currentUserTeams={userTeams}
onChange={handleChangeSelectedTeam}
selectedTeam={selectedTeamId}
/>
)}
</div>
2021-08-30 23:02:53 +00:00
</div>
</div>
{canAddOrRemovePolicy(currentUser, selectedTeamId) && (
2021-08-30 23:02:53 +00:00
<div className={`${baseClass}__action-button-container`}>
<Link
to={PATHS.NEW_POLICY}
className={`${baseClass}__add-policy-link`}
2021-08-30 23:02:53 +00:00
>
Add a policy
</Link>
2021-08-30 23:02:53 +00:00
</div>
)}
</div>
<div className={`${baseClass}__description`}>
{isPremiumTier && !!selectedTeamId && (
2021-08-30 23:02:53 +00:00
<p>
Add additional policies for <b>all hosts assigned to this team</b>
2021-08-30 23:02:53 +00:00
.
</p>
)}
2021-10-02 00:35:13 +00:00
{showDefaultDescription && (
<p>
Add policies for <b>all of your hosts</b> to see which pass your
organizations standards.{" "}
</p>
)}
2021-10-02 00:35:13 +00:00
</div>
{!!updateInterval && showInfoBanner && (
<InfoBanner className={`${baseClass}__sandbox-info`}>
<p>
Your policies are checked every <b>{updateInterval.trim()}</b>.{" "}
{isGlobalAdmin && (
<span>
Check out the Fleet documentation on{" "}
<a href={DOCS_LINK} target="_blank" rel="noreferrer">
<b>how to edit this frequency</b>
</a>
.
</span>
)}
</p>
</InfoBanner>
)}
<div>
{!!selectedTeamId &&
(isTeamPoliciesError ? (
<TableDataError />
) : (
<PoliciesListWrapper
policiesList={teamPolicies}
isLoading={isLoadingTeamPolicies}
onRemovePoliciesClick={onRemovePoliciesClick}
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
)}
selectedTeamData={selectedTeamData}
/>
))}
{!selectedTeamId &&
(isGlobalPoliciesError ? (
<TableDataError />
) : (
<PoliciesListWrapper
policiesList={globalPolicies}
isLoading={isLoadingGlobalPolicies}
onRemovePoliciesClick={onRemovePoliciesClick}
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
)}
selectedTeamData={selectedTeamData}
/>
))}
2021-08-30 23:02:53 +00:00
</div>
2021-10-02 00:35:13 +00:00
{showInheritedPoliciesButton && (
<span>
<Button
variant="unstyled"
className={`${showInheritedPolicies ? "upcarat" : "rightcarat"}
${baseClass}__inherited-policies-button`}
2021-10-02 00:35:13 +00:00
onClick={toggleShowInheritedPolicies}
>
{renderInheritedPoliciesButtonText(
showInheritedPolicies,
globalPolicies
)}
</Button>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>All teams policies are checked <br/> for this teams hosts.</p></center>\
"
2021-10-02 00:35:13 +00:00
}
/>
</div>
2021-10-02 00:35:13 +00:00
</span>
)}
{showInheritedPoliciesButton && showInheritedPolicies && (
<div className={`${baseClass}__inherited-policies-table`}>
<PoliciesListWrapper
isLoading={isLoadingGlobalPolicies}
policiesList={globalPolicies}
onRemovePoliciesClick={noop}
resultsTitle="policies"
canAddOrRemovePolicy={canAddOrRemovePolicy(
currentUser,
selectedTeamId
)}
tableType="inheritedPolicies"
selectedTeamData={selectedTeamData}
2021-10-02 00:35:13 +00:00
/>
</div>
)}
2021-08-30 23:02:53 +00:00
{showRemovePoliciesModal && (
<RemovePoliciesModal
onCancel={toggleRemovePoliciesModal}
onSubmit={onRemovePoliciesSubmit}
/>
)}
</div>
</div>
);
};
export default ManagePolicyPage;