fleet/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
Lucas Manuel Rodriguez 964f85b174
Amend policy creation and spec (for proprietary query), and add update APIs (#2890)
* Amend policy creation (proprietary query), add update APIs

* Fix Datastore.SavePolicy bug (and add tests)

* Add integration tests for new policy APIs

* Add author email

* Add activities

* Push breaking changes for return policy fields

* WIP

* Add integration test for host policies

* Make more improvements to policy representation

* Improve upgrade code (from PR review comments)

* PR changes

* Revert activities for policies

* Use *uint instead of uint for queryID, use fleet.PolicyPayload

* Filter out other schemas

* New policy flow (#2922)

* created new policy flow -- no API connection

* added api props

* fixed prop name

* lint fixes

* removed unused modal; fixed style

* name, desc icons; created global components

* lint fixes

* ignoring certain files and lines for prettier

* Update frontend/pages/policies/PolicyPage/PolicyPage.tsx

* Make policy names unique across deployment

* Amend upgrade script

* Fix migration for unique names

* Do not deduplicate but instead rename policies

Co-authored-by: Martavis Parker <47053705+martavis@users.noreply.github.com>
2021-11-24 14:16:42 -03:00

457 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
// @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import PATHS from "router/paths";
import { IPolicy } from "interfaces/policy";
import { ITeam } from "interfaces/team";
import { IUser } from "interfaces/user";
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";
import { inMilliseconds, secondsToHms } from "fleet/helpers";
import sortUtils from "utilities/sort";
import permissionsUtils from "utilities/permissions";
import TableDataError from "components/TableDataError";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
import IconToolTip from "components/IconToolTip";
import PoliciesListWrapper from "./components/PoliciesListWrapper";
import RemovePoliciesModal from "./components/RemovePoliciesModal";
import TeamsDropdown from "./components/TeamsDropdown";
const baseClass = "manage-policies-page";
const DOCS_LINK =
"https://fleetdm.com/docs/deploying/configuration#osquery-policy-update-interval";
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;
const dispatch = useDispatch();
const {
config,
currentUser,
isAnyTeamMaintainerOrTeamAdmin,
isGlobalAdmin,
isGlobalMaintainer,
isOnGlobalTeam,
isFreeTier,
isPremiumTier,
} = useContext(AppContext);
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[]
>([]);
const [showRemovePoliciesModal, setShowRemovePoliciesModal] = useState(false);
const [showInheritedPolicies, setShowInheritedPolicies] = useState(false);
const [updateInterval, setUpdateInterval] = useState<string>(
"osquery policy update interval"
);
// ===== local state
const getGlobalPolicies = useCallback(async () => {
setIsLoadingGlobalPolicies(true);
setIsGlobalPoliciesError(false);
let result;
try {
result = await globalPoliciesAPI
.loadAll()
.then((response) => response.policies);
setGlobalPolicies(result);
} catch (error) {
console.log(error);
setIsGlobalPoliciesError(true);
} finally {
setIsLoadingGlobalPolicies(false);
}
return result;
}, []);
const getTeamPolicies = useCallback(async (teamId) => {
setIsLoadingTeamPolicies(true);
setIsTeamPoliciesError(false);
let result;
try {
result = await teamPoliciesAPI
.loadAll(teamId)
.then((response) => response.policies);
setTeamPolicies(result);
} catch (error) {
console.log(error);
setIsTeamPoliciesError(true);
} finally {
setIsLoadingTeamPolicies(false);
}
return result;
}, []);
const getPolicies = useCallback(
(teamId) => {
return teamId ? getTeamPolicies(teamId) : getGlobalPolicies();
},
[getGlobalPolicies, getTeamPolicies]
);
const handleChangeSelectedTeam = (id: number) => {
const { MANAGE_POLICIES } = PATHS;
const path = id ? `${MANAGE_POLICIES}?team_id=${id}` : MANAGE_POLICIES;
router.replace(path);
setShowInheritedPolicies(false);
setSelectedPolicyIds([]);
};
const toggleRemovePoliciesModal = () =>
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);
await request.then(() => {
dispatch(
renderFlash(
"success",
`Successfully removed ${
selectedPolicyIds?.length === 1 ? "policy" : "policies"
}.`
)
);
});
} catch {
dispatch(
renderFlash(
"error",
`Unable to remove ${
selectedPolicyIds?.length === 1 ? "policy" : "policies"
}. Please try again.`
)
);
} finally {
toggleRemovePoliciesModal();
getPolicies(selectedTeamId);
}
};
// 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) {
const sortedTeams = unsortedTeams.sort((a, b) =>
sortUtils.caseInsensitiveAsc(a.name, b.name)
);
setUserTeams(sortedTeams);
}
}
}, [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);
}, [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);
}
}
}, [
getGlobalPolicies,
getTeamPolicies,
isAnyTeamMaintainerOrTeamAdmin,
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]);
// 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;
const selectedTeamData = userTeams?.find(
(team) => selectedTeamId === team.id
);
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>
</div>
</div>
{canAddOrRemovePolicy(currentUser, selectedTeamId) && (
<div className={`${baseClass}__action-button-container`}>
<Link
to={PATHS.NEW_POLICY}
className={`${baseClass}__add-policy-link`}
>
Add a policy
</Link>
</div>
)}
</div>
<div className={`${baseClass}__description`}>
{isPremiumTier && !!selectedTeamId && (
<p>
Add additional policies for <b>all hosts assigned to this team</b>
.
</p>
)}
{showDefaultDescription && (
<p>
Add policies for <b>all of your hosts</b> to see which pass your
organizations standards.{" "}
</p>
)}
</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}
/>
))}
</div>
{showInheritedPoliciesButton && (
<span>
<Button
variant="unstyled"
className={`${showInheritedPolicies ? "upcarat" : "rightcarat"}
${baseClass}__inherited-policies-button`}
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>\
"
}
/>
</div>
</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}
/>
</div>
)}
{showRemovePoliciesModal && (
<RemovePoliciesModal
onCancel={toggleRemovePoliciesModal}
onSubmit={onRemovePoliciesSubmit}
/>
)}
</div>
</div>
);
};
export default ManagePolicyPage;