mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
Allow team admins to manage scheduled queries (#2738)
This commit is contained in:
parent
45c5e29ca0
commit
e50ca4ece7
5 changed files with 168 additions and 92 deletions
1
changes/fix-2687-team-admin-scheduled-queries
Normal file
1
changes/fix-2687-team-admin-scheduled-queries
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Bug fix: Allow Team Admins to manage scheduled queries
|
||||
|
|
@ -51,6 +51,7 @@ const initialState = {
|
|||
isAnyTeamMaintainerOrTeamAdmin: undefined,
|
||||
isTeamObserver: undefined,
|
||||
isTeamMaintainer: undefined,
|
||||
isTeamMaintainerOrTeamAdmin: undefined,
|
||||
isAnyTeamAdmin: undefined,
|
||||
isTeamAdmin: undefined,
|
||||
isOnlyObserver: undefined,
|
||||
|
|
@ -93,6 +94,10 @@ const setPermissions = (user: IUser, config: IConfig, teamId = 0) => {
|
|||
isTeamObserver: permissions.isTeamObserver(user, teamId),
|
||||
isTeamMaintainer: permissions.isTeamMaintainer(user, teamId),
|
||||
isTeamAdmin: permissions.isTeamAdmin(user, teamId),
|
||||
isTeamMaintainerOrTeamAdmin: permissions.isTeamMaintainerOrTeamAdmin(
|
||||
user,
|
||||
teamId
|
||||
),
|
||||
isOnlyObserver: permissions.isOnlyObserver(user),
|
||||
};
|
||||
};
|
||||
|
|
@ -153,6 +158,7 @@ const AppProvider = ({ children }: Props) => {
|
|||
isTeamObserver: state.isTeamObserver,
|
||||
isTeamMaintainer: state.isTeamMaintainer,
|
||||
isTeamAdmin: state.isTeamAdmin,
|
||||
isTeamMaintainerOrTeamAdmin: state.isTeamMaintainer,
|
||||
isAnyTeamAdmin: state.isAnyTeamAdmin,
|
||||
isOnlyObserver: state.isOnlyObserver,
|
||||
setCurrentUser: (currentUser: IUser) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/* Conditionally renders global schedule and team schedules */
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import React, { useState, useCallback, useEffect, useContext } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { push } from "react-router-redux";
|
||||
// @ts-ignore
|
||||
|
|
@ -44,7 +45,8 @@ const renderTable = (
|
|||
allScheduledQueriesError: { name: string; reason: string }[],
|
||||
toggleScheduleEditorModal: () => void,
|
||||
teamId: number,
|
||||
isTeamMaintainer: boolean
|
||||
isTeamMaintainerOrTeamAdmin: boolean,
|
||||
isOnGlobalTeam: boolean
|
||||
): JSX.Element => {
|
||||
if (Object.keys(allScheduledQueriesError).length !== 0) {
|
||||
return <TableDataError />;
|
||||
|
|
@ -57,16 +59,18 @@ const renderTable = (
|
|||
allScheduledQueriesList={allScheduledQueriesList}
|
||||
toggleScheduleEditorModal={toggleScheduleEditorModal}
|
||||
teamId={teamId}
|
||||
isTeamMaintainer={isTeamMaintainer}
|
||||
isTeamMaintainerOrTeamAdmin={isTeamMaintainerOrTeamAdmin}
|
||||
isOnGlobalTeam={isOnGlobalTeam}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAllTeamsTable = (
|
||||
teamId: number,
|
||||
isTeamMaintainer: boolean,
|
||||
allTeamsScheduledQueriesList: IGlobalScheduledQuery[],
|
||||
allTeamsScheduledQueriesError: { name: string; reason: string }[]
|
||||
allTeamsScheduledQueriesError: { name: string; reason: string }[],
|
||||
teamId: number,
|
||||
isTeamMaintainerOrTeamAdmin: boolean,
|
||||
isOnGlobalTeam: boolean
|
||||
): JSX.Element => {
|
||||
if (Object.keys(allTeamsScheduledQueriesError).length > 0) {
|
||||
return <TableDataError />;
|
||||
|
|
@ -78,7 +82,8 @@ const renderAllTeamsTable = (
|
|||
inheritedQueries
|
||||
allScheduledQueriesList={allTeamsScheduledQueriesList}
|
||||
teamId={teamId}
|
||||
isTeamMaintainer={isTeamMaintainer}
|
||||
isTeamMaintainerOrTeamAdmin={isTeamMaintainerOrTeamAdmin}
|
||||
isOnGlobalTeam={isOnGlobalTeam}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -138,11 +143,57 @@ interface ITeamOptions {
|
|||
const ManageSchedulePage = ({
|
||||
params: { team_id },
|
||||
}: ITeamSchedulesPageProps): JSX.Element => {
|
||||
const teamId = parseInt(team_id, 10);
|
||||
let teamId = parseInt(team_id, 10);
|
||||
const dispatch = useDispatch();
|
||||
const { MANAGE_PACKS } = paths;
|
||||
const handleAdvanced = () => dispatch(push(MANAGE_PACKS));
|
||||
|
||||
const { currentUser, isOnGlobalTeam } = useContext(AppContext);
|
||||
|
||||
const isTeamMaintainerOrTeamAdmin = (() => {
|
||||
return !!permissionUtils.isTeamMaintainerOrTeamAdmin(currentUser, teamId);
|
||||
})();
|
||||
|
||||
const onChangeSelectedTeam = (selectedTeamId: number) => {
|
||||
if (isNaN(selectedTeamId)) {
|
||||
dispatch(push(`${paths.MANAGE_SCHEDULE}`));
|
||||
} else {
|
||||
dispatch(push(`${paths.MANAGE_TEAM_SCHEDULE(selectedTeamId)}`));
|
||||
}
|
||||
};
|
||||
|
||||
const loadFirstMaintainerOrAdminTeam = () => {
|
||||
if (currentUser) {
|
||||
const adminOrMaintainerTeam = currentUser.teams.find((team) => {
|
||||
return team.role === "admin" || team.role === "maintainer"
|
||||
? team.id
|
||||
: null;
|
||||
});
|
||||
if (adminOrMaintainerTeam) {
|
||||
teamId = adminOrMaintainerTeam.id;
|
||||
onChangeSelectedTeam(teamId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin && !teamId) {
|
||||
loadFirstMaintainerOrAdminTeam();
|
||||
}
|
||||
|
||||
if (!isOnGlobalTeam && !isTeamMaintainerOrTeamAdmin && teamId) {
|
||||
if (currentUser) {
|
||||
const canLoadTeam = currentUser.teams.find((team) => {
|
||||
return (
|
||||
(team.role === "admin" || team.role === "maintainer") &&
|
||||
team.id === teamId
|
||||
);
|
||||
});
|
||||
if (!canLoadTeam) {
|
||||
loadFirstMaintainerOrAdminTeam();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(queryActions.loadAll());
|
||||
dispatch(teamActions.loadAll());
|
||||
|
|
@ -163,10 +214,6 @@ const ManageSchedulePage = ({
|
|||
}
|
||||
);
|
||||
|
||||
const isTeamMaintainer = useSelector((state: IRootState): boolean => {
|
||||
return permissionUtils.isAnyTeamMaintainer(state.auth.user);
|
||||
});
|
||||
|
||||
const allQueries = useSelector((state: IRootState) => state.entities.queries);
|
||||
const allQueriesList = Object.values(allQueries.data);
|
||||
|
||||
|
|
@ -197,38 +244,6 @@ const ManageSchedulePage = ({
|
|||
|
||||
const selectedTeam = isNaN(teamId) ? "global" : teamId;
|
||||
|
||||
const generateTeamOptionsDropdownItems = (): ITeamOptions[] => {
|
||||
const teamOptions: ITeamOptions[] = [];
|
||||
|
||||
if (isTeamMaintainer) {
|
||||
user.teams.forEach((team) => {
|
||||
if (team.role === "maintainer") {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
value: "global",
|
||||
});
|
||||
|
||||
allTeamsList.forEach((team) => {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return teamOptions;
|
||||
};
|
||||
|
||||
const [showInheritedQueries, setShowInheritedQueries] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
|
|
@ -257,6 +272,55 @@ const ManageSchedulePage = ({
|
|||
setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal);
|
||||
}, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]);
|
||||
|
||||
const generateTeamOptionsDropdownItems = (): ITeamOptions[] => {
|
||||
const teamOptions: ITeamOptions[] = [];
|
||||
|
||||
if (isTeamMaintainerOrTeamAdmin) {
|
||||
user.teams.forEach((team) => {
|
||||
if (team.role === "admin" || team.role === "maintainer") {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (isOnGlobalTeam) {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: "All teams",
|
||||
value: "global",
|
||||
});
|
||||
|
||||
allTeamsList.forEach((team) => {
|
||||
teamOptions.push({
|
||||
disabled: false,
|
||||
label: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return teamOptions;
|
||||
};
|
||||
|
||||
const renderTitleOrDropdown = (): JSX.Element => {
|
||||
const dropDownOptions = generateTeamOptionsDropdownItems();
|
||||
return dropDownOptions.length === 1 ? (
|
||||
<h1>{dropDownOptions[0].label}</h1>
|
||||
) : (
|
||||
<Dropdown
|
||||
value={selectedTeam}
|
||||
className={`${baseClass}__team-dropdown`}
|
||||
options={dropDownOptions}
|
||||
searchable={false}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
onChangeSelectedTeam(newSelectedValue)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onRemoveScheduledQueryClick = (
|
||||
selectedTableQueryIds: number[]
|
||||
): void => {
|
||||
|
|
@ -372,21 +436,15 @@ const ManageSchedulePage = ({
|
|||
[dispatch, teamId, toggleScheduleEditorModal]
|
||||
);
|
||||
|
||||
const onChangeSelectedTeam = (selectedTeamId: number) => {
|
||||
if (isNaN(selectedTeamId)) {
|
||||
dispatch(push(`${paths.MANAGE_SCHEDULE}`));
|
||||
} else {
|
||||
dispatch(push(`${paths.MANAGE_TEAM_SCHEDULE(selectedTeamId)}`));
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedTeam === "global" && isTeamMaintainer) {
|
||||
if (selectedTeam === "global" && isTeamMaintainerOrTeamAdmin) {
|
||||
const teamMaintainerTeams = generateTeamOptionsDropdownItems();
|
||||
dispatch(
|
||||
push(
|
||||
`${paths.MANAGE_TEAM_SCHEDULE(Number(teamMaintainerTeams[0].value))}`
|
||||
)
|
||||
);
|
||||
if (teamMaintainerTeams.length) {
|
||||
dispatch(
|
||||
push(
|
||||
`${paths.MANAGE_TEAM_SCHEDULE(Number(teamMaintainerTeams[0].value))}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -409,15 +467,7 @@ const ManageSchedulePage = ({
|
|||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Dropdown
|
||||
value={selectedTeam}
|
||||
className={`${baseClass}__team-dropdown`}
|
||||
options={generateTeamOptionsDropdownItems()}
|
||||
searchable={false}
|
||||
onChange={(newSelectedValue: number) =>
|
||||
onChangeSelectedTeam(newSelectedValue)
|
||||
}
|
||||
/>
|
||||
{renderTitleOrDropdown()}
|
||||
<div className={`${baseClass}__description`}>
|
||||
{isNaN(teamId) ? (
|
||||
<p>
|
||||
|
|
@ -438,7 +488,7 @@ const ManageSchedulePage = ({
|
|||
{allScheduledQueriesList.length !== 0 &&
|
||||
allScheduledQueriesError.length !== 0 && (
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
{!isTeamMaintainer && (
|
||||
{!isTeamMaintainerOrTeamAdmin && (
|
||||
<Button
|
||||
variant="inverse"
|
||||
onClick={handleAdvanced}
|
||||
|
|
@ -465,7 +515,8 @@ const ManageSchedulePage = ({
|
|||
allScheduledQueriesError,
|
||||
toggleScheduleEditorModal,
|
||||
teamId,
|
||||
isTeamMaintainer
|
||||
isTeamMaintainerOrTeamAdmin,
|
||||
isOnGlobalTeam || false
|
||||
)}
|
||||
</div>
|
||||
{/* must use ternary for NaN */}
|
||||
|
|
@ -497,10 +548,11 @@ const ManageSchedulePage = ({
|
|||
) : null}
|
||||
{showInheritedQueries &&
|
||||
renderAllTeamsTable(
|
||||
teamId,
|
||||
isTeamMaintainer,
|
||||
allTeamsScheduledQueriesList,
|
||||
allTeamsScheduledQueriesError
|
||||
allTeamsScheduledQueriesError,
|
||||
teamId,
|
||||
isTeamMaintainerOrTeamAdmin,
|
||||
isOnGlobalTeam || false
|
||||
)}
|
||||
{showScheduleEditorModal && (
|
||||
<ScheduleEditorModal
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ interface IScheduleListWrapperProps {
|
|||
toggleScheduleEditorModal?: () => void;
|
||||
teamId: number;
|
||||
inheritedQueries?: boolean;
|
||||
isTeamMaintainer: boolean;
|
||||
isTeamMaintainerOrTeamAdmin: boolean;
|
||||
isOnGlobalTeam: boolean;
|
||||
}
|
||||
interface IRootState {
|
||||
entities: {
|
||||
|
|
@ -55,7 +56,8 @@ const ScheduleListWrapper = ({
|
|||
onEditScheduledQueryClick,
|
||||
teamId,
|
||||
inheritedQueries,
|
||||
isTeamMaintainer,
|
||||
isTeamMaintainerOrTeamAdmin,
|
||||
isOnGlobalTeam,
|
||||
}: IScheduleListWrapperProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { MANAGE_PACKS } = paths;
|
||||
|
|
@ -70,28 +72,34 @@ const ScheduleListWrapper = ({
|
|||
<div className={`${noScheduleClass}__inner-text`}>
|
||||
<h2>You don't have any queries scheduled.</h2>
|
||||
<p>
|
||||
{!isTeamMaintainer
|
||||
? "Schedule a query, or go to your osquery packs via the 'Advanced' button."
|
||||
: "Schedule a query to run on hosts assigned to this team."}
|
||||
{isOnGlobalTeam &&
|
||||
"Schedule a query, or go to your osquery packs via the 'Advanced' button."}
|
||||
{isTeamMaintainerOrTeamAdmin &&
|
||||
"Schedule a query to run on hosts assigned to this team."}
|
||||
{!isOnGlobalTeam &&
|
||||
!isTeamMaintainerOrTeamAdmin &&
|
||||
"There are no scheduled queries assigned to this team."}
|
||||
</p>
|
||||
<div className={`${noScheduleClass}__-cta-buttons`}>
|
||||
<Button
|
||||
variant="brand"
|
||||
className={`${noScheduleClass}__schedule-button`}
|
||||
onClick={toggleScheduleEditorModal}
|
||||
>
|
||||
Schedule a query
|
||||
</Button>
|
||||
{!isTeamMaintainer && (
|
||||
{(isOnGlobalTeam || isTeamMaintainerOrTeamAdmin) && (
|
||||
<div className={`${noScheduleClass}__-cta-buttons`}>
|
||||
<Button
|
||||
variant="inverse"
|
||||
onClick={handleAdvanced}
|
||||
className={`${baseClass}__advanced-button`}
|
||||
variant="brand"
|
||||
className={`${noScheduleClass}__schedule-button`}
|
||||
onClick={toggleScheduleEditorModal}
|
||||
>
|
||||
Advanced
|
||||
Schedule a query
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{isOnGlobalTeam && (
|
||||
<Button
|
||||
variant="inverse"
|
||||
onClick={handleAdvanced}
|
||||
className={`${baseClass}__advanced-button`}
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ const isTeamAdmin = (user: IUser | null, teamId: number | null): boolean => {
|
|||
return userTeamRole === "admin";
|
||||
};
|
||||
|
||||
const isTeamMaintainerOrTeamAdmin = (
|
||||
user: IUser | null,
|
||||
teamId: number | null
|
||||
): boolean => {
|
||||
const userTeamRole = user?.teams.find((team) => team.id === teamId)?.role;
|
||||
return userTeamRole === "admin" || userTeamRole === "maintainer";
|
||||
};
|
||||
|
||||
// This checks against all teams
|
||||
const isAnyTeamMaintainer = (user: IUser): boolean => {
|
||||
if (!isOnGlobalTeam(user)) {
|
||||
|
|
@ -95,6 +103,7 @@ export default {
|
|||
isOnGlobalTeam,
|
||||
isTeamObserver,
|
||||
isTeamMaintainer,
|
||||
isTeamMaintainerOrTeamAdmin,
|
||||
isAnyTeamMaintainer,
|
||||
isAnyTeamMaintainerOrTeamAdmin,
|
||||
isTeamAdmin,
|
||||
|
|
|
|||
Loading…
Reference in a new issue