Allow team admins to manage scheduled queries (#2738)

This commit is contained in:
Luke Heath 2021-10-28 16:23:23 -05:00 committed by GitHub
parent 45c5e29ca0
commit e50ca4ece7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 168 additions and 92 deletions

View file

@ -0,0 +1 @@
* Bug fix: Allow Team Admins to manage scheduled queries

View file

@ -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) => {

View file

@ -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

View file

@ -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&apos;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>

View file

@ -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,