Add team policies to new policy flow UI (#3116)

This commit is contained in:
gillespi314 2021-11-29 13:50:58 -06:00 committed by GitHub
parent 036093874d
commit a51225f3a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 109 additions and 45 deletions

View file

@ -11,34 +11,44 @@ type Props = {
};
type InitialStateType = {
selectedOsqueryTable: IOsqueryTable;
lastEditedQueryName: string;
lastEditedQueryDescription: string;
lastEditedQueryBody: string;
setLastEditedQueryName: (value: string) => void;
setLastEditedQueryDescription: (value: string) => void;
setLastEditedQueryBody: (value: string) => void;
policyTeamId: number;
setPolicyTeamId: (id: number) => void;
selectedOsqueryTable: IOsqueryTable;
setSelectedOsqueryTable: (tableName: string) => void;
};
const initialState = {
selectedOsqueryTable: find(osqueryTables, { name: "users" }),
lastEditedQueryName: DEFAULT_POLICY.name,
lastEditedQueryDescription: DEFAULT_POLICY.description,
lastEditedQueryBody: DEFAULT_POLICY.query,
setLastEditedQueryName: () => null,
setLastEditedQueryDescription: () => null,
setLastEditedQueryBody: () => null,
policyTeamId: 0,
setPolicyTeamId: () => null,
selectedOsqueryTable: find(osqueryTables, { name: "users" }),
setSelectedOsqueryTable: () => null,
};
const actions = {
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
SET_POLICY_TEAM_ID: "SET_POLICY_TEAM_ID",
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
};
const reducer = (state: any, action: any) => {
switch (action.type) {
case actions.SET_POLICY_TEAM_ID:
return {
...state,
policyTeamId: action.id,
};
case actions.SET_SELECTED_OSQUERY_TABLE:
return {
...state,
@ -71,7 +81,6 @@ const PolicyProvider = ({ children }: Props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = {
selectedOsqueryTable: state.selectedOsqueryTable,
lastEditedQueryName: state.lastEditedQueryName,
lastEditedQueryDescription: state.lastEditedQueryDescription,
lastEditedQueryBody: state.lastEditedQueryBody,
@ -93,6 +102,11 @@ const PolicyProvider = ({ children }: Props) => {
lastEditedQueryBody,
});
},
policyTeamId: state.policyTeamId,
setPolicyTeamId: (id: number) => {
dispatch({ type: actions.SET_POLICY_TEAM_ID, id });
},
selectedOsqueryTable: state.selectedOsqueryTable,
setSelectedOsqueryTable: (tableName: string) => {
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
},

View file

@ -16,4 +16,5 @@ export interface IPolicyFormData {
description?: string | number | boolean | any[] | undefined;
name?: string | number | boolean | any[] | undefined;
query?: string | number | boolean | any[] | undefined;
team_id?: number;
}

View file

@ -14,6 +14,7 @@ import { ITeam } from "interfaces/team";
import { IUser } from "interfaces/user";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import fleetQueriesAPI from "services/entities/queries";
import globalPoliciesAPI from "services/entities/global_policies";
@ -67,6 +68,8 @@ const ManagePolicyPage = (managePoliciesPageProps: {
isPremiumTier,
} = useContext(AppContext);
const { setPolicyTeamId } = useContext(PolicyContext);
const { isTeamMaintainer, isTeamAdmin } = permissionsUtils;
const canAddOrRemovePolicy = (user: IUser | null, teamId: number | null) =>
isGlobalAdmin ||
@ -74,12 +77,16 @@ const ManagePolicyPage = (managePoliciesPageProps: {
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: teams } = useQuery<{ teams: ITeam[] }, Error, ITeam[]>(
["teams"],
() => teamsAPI.loadAll({}),
{
enabled: !!isPremiumTier,
select: (data) => data.teams,
refetchOnMount: false,
refetchOnWindowFocus: false,
}
);
const { data: fleetQueries } = useQuery(
["fleetQueries"],
@ -161,6 +168,7 @@ const ManagePolicyPage = (managePoliciesPageProps: {
router.replace(path);
setShowInheritedPolicies(false);
setSelectedPolicyIds([]);
setPolicyTeamId(id);
};
const toggleRemovePoliciesModal = () =>

View file

@ -8,6 +8,7 @@ import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { QUERIES_PAGE_STEPS, DEFAULT_POLICY } from "utilities/constants";
import globalPoliciesAPI from "services/entities/global_policies"; // @ts-ignore
import teamPoliciesAPI from "services/entities/team_policies"; // @ts-ignore
import hostAPI from "services/entities/hosts"; // @ts-ignore
import { IPolicyFormData, IPolicy } from "interfaces/policy";
import { ITarget } from "interfaces/target";
@ -108,7 +109,9 @@ const PolicyPage = ({
const {
mutateAsync: createPolicy,
} = useMutation((formData: IPolicyFormData) =>
globalPoliciesAPI.create(formData)
formData.team_id
? teamPoliciesAPI.create(formData)
: globalPoliciesAPI.create(formData)
);
useEffect(() => {

View file

@ -13,11 +13,11 @@ export interface INewPolicyModalProps {
baseClass: string;
queryValue: string;
onCreatePolicy: (formData: IPolicyFormData) => void;
setIsSaveModalOpen: (isOpen: boolean) => void;
setIsNewPolicyModalOpen: (isOpen: boolean) => void;
}
const validatePolicyName = (name: string) => {
const errors: { [key: string]: any } = {};
const errors: { [key: string]: string } = {};
if (!name) {
errors.name = "Policy name must be present";
@ -31,11 +31,11 @@ const NewPolicyModal = ({
baseClass,
queryValue,
onCreatePolicy,
setIsSaveModalOpen,
}: INewPolicyModalProps) => {
setIsNewPolicyModalOpen,
}: INewPolicyModalProps): JSX.Element => {
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [errors, setErrors] = useState<{ [key: string]: any }>({});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
useDeepEffect(() => {
if (name) {
@ -43,7 +43,7 @@ const NewPolicyModal = ({
}
}, [name]);
const handleUpdate = (evt: React.MouseEvent<HTMLButtonElement>) => {
const handleSavePolicy = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
const { valid, errors: newErrors } = validatePolicyName(name);
@ -59,12 +59,12 @@ const NewPolicyModal = ({
query: queryValue,
});
setIsSaveModalOpen(false);
setIsNewPolicyModalOpen(false);
}
};
return (
<Modal title={"Save policy"} onExit={() => setIsSaveModalOpen(false)}>
<Modal title={"Save policy"} onExit={() => setIsNewPolicyModalOpen(false)}>
<form className={`${baseClass}__save-modal-form`} autoComplete="off">
<InputField
name="name"
@ -89,7 +89,7 @@ const NewPolicyModal = ({
>
<Button
className={`${baseClass}__btn`}
onClick={() => setIsSaveModalOpen(false)}
onClick={() => setIsNewPolicyModalOpen(false)}
variant="text-link"
>
Cancel
@ -98,7 +98,7 @@ const NewPolicyModal = ({
className={`${baseClass}__btn`}
type="button"
variant="brand"
onClick={handleUpdate}
onClick={handleSavePolicy}
>
Save policy
</Button>

View file

@ -16,7 +16,6 @@ import Avatar from "components/Avatar";
import FleetAce from "components/FleetAce"; // @ts-ignore
import validateQuery from "components/forms/validators/validate_query";
import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import Spinner from "components/Spinner"; // @ts-ignore
import InputField from "components/forms/fields/InputField";
import NewPolicyModal from "../NewPolicyModal";
@ -42,7 +41,7 @@ interface IPolicyFormProps {
}
const validateQuerySQL = (query: string) => {
const errors: { [key: string]: any } = {};
const errors: { [key: string]: string } = {};
const { error: queryError, valid: queryValid } = validateQuery(query);
if (!queryValid) {
@ -66,8 +65,10 @@ const PolicyForm = ({
renderLiveQueryWarning,
}: IPolicyFormProps): JSX.Element => {
const isEditMode = !!policyIdForEdit;
const [errors, setErrors] = useState<{ [key: string]: any }>({});
const [isSaveModalOpen, setIsSaveModalOpen] = useState<boolean>(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [isNewPolicyModalOpen, setIsNewPolicyModalOpen] = useState<boolean>(
false
);
const [showQueryEditor, setShowQueryEditor] = useState<boolean>(false);
const [compatiblePlatforms, setCompatiblePlatforms] = useState<string[]>([]);
const [isEditingName, setIsEditingName] = useState<boolean>(false);
@ -135,7 +136,7 @@ const PolicyForm = ({
});
};
const promptSaveQuery = (forceNew = false) => (
const handleSavePolicy = (forceNew = false) => (
evt: React.MouseEvent<HTMLButtonElement>
) => {
evt.preventDefault();
@ -160,7 +161,7 @@ const PolicyForm = ({
if (valid) {
if (!isEditMode || forceNew) {
setIsSaveModalOpen(true);
setIsNewPolicyModalOpen(true);
} else {
onUpdate({
name: lastEditedQueryName,
@ -174,6 +175,8 @@ const PolicyForm = ({
setIsEditingName(false);
setIsEditingDescription(false);
}
return null;
};
const renderAuthor = (): JSX.Element | null => {
@ -221,6 +224,8 @@ const PolicyForm = ({
} else if (compatiblePlatforms[0] === "None") {
return "No platforms (check your query for invalid tables or tables that are supported on different platforms)";
}
return null;
};
const displayFormattedPlatforms = compatiblePlatforms.map((string) => {
@ -422,7 +427,7 @@ const PolicyForm = ({
onChange={(sqlString: string) => {
setLastEditedQueryBody(sqlString);
}}
handleSubmit={promptSaveQuery}
handleSubmit={handleSavePolicy}
/>
{renderPlatformCompatibility()}
{renderLiveQueryWarning()}
@ -444,7 +449,7 @@ const PolicyForm = ({
<Button
className={`${baseClass}__save`}
variant="brand"
onClick={promptSaveQuery()}
onClick={handleSavePolicy()}
disabled={
isAnyTeamMaintainerOrTeamAdmin &&
!hasTeamMaintainerPermissions
@ -480,12 +485,12 @@ const PolicyForm = ({
</Button>
</div>
</form>
{isSaveModalOpen && (
{isNewPolicyModalOpen && (
<NewPolicyModal
baseClass={baseClass}
queryValue={lastEditedQueryBody}
onCreatePolicy={onCreatePolicy}
setIsSaveModalOpen={setIsSaveModalOpen}
setIsNewPolicyModalOpen={setIsNewPolicyModalOpen}
/>
)}
</>

View file

@ -2,9 +2,9 @@ import React, { useContext, useEffect } from "react";
import { Link } from "react-router";
import { useDispatch } from "react-redux";
import { InjectedRouter } from "react-router/lib/Router";
import { UseMutateAsyncFunction } from "react-query";
import globalPoliciesAPI from "services/entities/global_policies";
import teamPoliciesAPI from "services/entities/team_policies";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy"; // @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
@ -24,7 +24,7 @@ interface IQueryEditorProps {
storedPolicyError: any;
showOpenSchemaActionText: boolean;
isStoredPolicyLoading: boolean;
createPolicy: UseMutateAsyncFunction<any, unknown, IPolicyFormData, unknown>;
createPolicy: (formData: IPolicyFormData) => Promise<any>;
onOsqueryTableSelect: (tableName: string) => void;
goToSelectTargets: () => void;
onOpenSchemaSidebar: () => void;
@ -44,7 +44,7 @@ const QueryEditor = ({
goToSelectTargets,
onOpenSchemaSidebar,
renderLiveQueryWarning,
}: IQueryEditorProps) => {
}: IQueryEditorProps): JSX.Element | null => {
const dispatch = useDispatch();
const { currentUser } = useContext(AppContext);
@ -54,6 +54,7 @@ const QueryEditor = ({
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryBody,
policyTeamId,
} = useContext(PolicyContext);
useEffect(() => {
@ -67,9 +68,22 @@ const QueryEditor = ({
}
}, []);
const onSavePolicyFormSubmit = debounce(async (formData: IPolicyFormData) => {
const onCreatePolicy = debounce(async (formData: IPolicyFormData) => {
// TODO: The approach taken with selectedTeamId context works in most cases. Howeve, the context
// will reset to global if page is refreshed. This will cause bugs where a global policy gets
// created when the user intended a team policy. For non-gloabl users, request will fail but the
// erorr is opaque and would require them to navigate back to the manage policies page to select
// a team and start over, in which case it might be better to intercept the unauthorized errors
// and redirect to the manage policies page (unless we have added a means to select a team on
// the edit/create policy form itself).
if (policyTeamId) {
formData.team_id = policyTeamId;
}
try {
const { policy }: { policy: IPolicy } = await createPolicy(formData);
const policy: IPolicy = await createPolicy(formData).then(
(data) => data.policy
);
router.push(PATHS.EDIT_POLICY(policy));
dispatch(renderFlash("success", "Policy created!"));
} catch (createError) {
@ -94,8 +108,20 @@ const QueryEditor = ({
lastEditedQueryBody,
});
const updateAPIRequest = () => {
// storedPolicy.team_id is used for existing policies because selectedTeamId is subject to change
const team_id = storedPolicy?.team_id;
return team_id
? teamPoliciesAPI.update(policyIdForEdit, {
...updatedPolicy,
team_id,
})
: globalPoliciesAPI.update(policyIdForEdit, updatedPolicy);
};
try {
await globalPoliciesAPI.update(policyIdForEdit, updatedPolicy);
await updateAPIRequest();
dispatch(renderFlash("success", "Policy updated!"));
} catch (updateError) {
console.error(updateError);
@ -121,7 +147,7 @@ const QueryEditor = ({
<span>Back to policies</span>
</Link>
<PolicyForm
onCreatePolicy={onSavePolicyFormSubmit}
onCreatePolicy={onCreatePolicy}
goToSelectTargets={goToSelectTargets}
onOsqueryTableSelect={onOsqueryTableSelect}
onUpdate={onUpdatePolicy}

View file

@ -4,13 +4,10 @@ import endpoints from "fleet/endpoints";
import { IPolicyFormData } from "interfaces/policy";
export default {
create: (data: number | IPolicyFormData) => {
// TODO: How does the frontend need to support legacy policies?
create: (data: IPolicyFormData) => {
const { GLOBAL_POLICIES } = endpoints;
if (typeof data === "number") {
return sendRequest("POST", GLOBAL_POLICIES, { query_id: data });
}
return sendRequest("POST", GLOBAL_POLICIES, data);
},
destroy: (ids: number[]) => {

View file

@ -1,14 +1,24 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import sendRequest from "services";
import endpoints from "fleet/endpoints";
import { IPolicyFormData } from "interfaces/policy";
// const endpoints = { TEAMS: "/v1/fleet/team" };
export default {
create: (team_id: number, query_id: number) => {
// TODO: How does the frontend need to support legacy policies?
create: (data: IPolicyFormData) => {
const { name, description, query, team_id } = data;
const { TEAMS } = endpoints;
const path = `${TEAMS}/${team_id}/policies`;
return sendRequest("POST", path, { query_id });
return sendRequest("POST", path, { name, description, query });
},
update: (id: number, data: IPolicyFormData) => {
const { name, description, query, team_id } = data;
const { TEAMS } = endpoints;
const path = `${TEAMS}/${team_id}/policies/${id}`;
return sendRequest("PATCH", path, { name, description, query });
},
destroy: (team_id: number, ids: number[]) => {
const { TEAMS } = endpoints;