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, isAnyTeamMaintainerOrTeamAdmin: undefined,
isTeamObserver: undefined, isTeamObserver: undefined,
isTeamMaintainer: undefined, isTeamMaintainer: undefined,
isTeamMaintainerOrTeamAdmin: undefined,
isAnyTeamAdmin: undefined, isAnyTeamAdmin: undefined,
isTeamAdmin: undefined, isTeamAdmin: undefined,
isOnlyObserver: undefined, isOnlyObserver: undefined,
@ -93,6 +94,10 @@ const setPermissions = (user: IUser, config: IConfig, teamId = 0) => {
isTeamObserver: permissions.isTeamObserver(user, teamId), isTeamObserver: permissions.isTeamObserver(user, teamId),
isTeamMaintainer: permissions.isTeamMaintainer(user, teamId), isTeamMaintainer: permissions.isTeamMaintainer(user, teamId),
isTeamAdmin: permissions.isTeamAdmin(user, teamId), isTeamAdmin: permissions.isTeamAdmin(user, teamId),
isTeamMaintainerOrTeamAdmin: permissions.isTeamMaintainerOrTeamAdmin(
user,
teamId
),
isOnlyObserver: permissions.isOnlyObserver(user), isOnlyObserver: permissions.isOnlyObserver(user),
}; };
}; };
@ -153,6 +158,7 @@ const AppProvider = ({ children }: Props) => {
isTeamObserver: state.isTeamObserver, isTeamObserver: state.isTeamObserver,
isTeamMaintainer: state.isTeamMaintainer, isTeamMaintainer: state.isTeamMaintainer,
isTeamAdmin: state.isTeamAdmin, isTeamAdmin: state.isTeamAdmin,
isTeamMaintainerOrTeamAdmin: state.isTeamMaintainer,
isAnyTeamAdmin: state.isAnyTeamAdmin, isAnyTeamAdmin: state.isAnyTeamAdmin,
isOnlyObserver: state.isOnlyObserver, isOnlyObserver: state.isOnlyObserver,
setCurrentUser: (currentUser: IUser) => { setCurrentUser: (currentUser: IUser) => {

View file

@ -1,7 +1,8 @@
/* Conditionally renders global schedule and team schedules */ /* 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 { useDispatch, useSelector } from "react-redux";
import { AppContext } from "context/app";
import { push } from "react-router-redux"; import { push } from "react-router-redux";
// @ts-ignore // @ts-ignore
@ -44,7 +45,8 @@ const renderTable = (
allScheduledQueriesError: { name: string; reason: string }[], allScheduledQueriesError: { name: string; reason: string }[],
toggleScheduleEditorModal: () => void, toggleScheduleEditorModal: () => void,
teamId: number, teamId: number,
isTeamMaintainer: boolean isTeamMaintainerOrTeamAdmin: boolean,
isOnGlobalTeam: boolean
): JSX.Element => { ): JSX.Element => {
if (Object.keys(allScheduledQueriesError).length !== 0) { if (Object.keys(allScheduledQueriesError).length !== 0) {
return <TableDataError />; return <TableDataError />;
@ -57,16 +59,18 @@ const renderTable = (
allScheduledQueriesList={allScheduledQueriesList} allScheduledQueriesList={allScheduledQueriesList}
toggleScheduleEditorModal={toggleScheduleEditorModal} toggleScheduleEditorModal={toggleScheduleEditorModal}
teamId={teamId} teamId={teamId}
isTeamMaintainer={isTeamMaintainer} isTeamMaintainerOrTeamAdmin={isTeamMaintainerOrTeamAdmin}
isOnGlobalTeam={isOnGlobalTeam}
/> />
); );
}; };
const renderAllTeamsTable = ( const renderAllTeamsTable = (
teamId: number,
isTeamMaintainer: boolean,
allTeamsScheduledQueriesList: IGlobalScheduledQuery[], allTeamsScheduledQueriesList: IGlobalScheduledQuery[],
allTeamsScheduledQueriesError: { name: string; reason: string }[] allTeamsScheduledQueriesError: { name: string; reason: string }[],
teamId: number,
isTeamMaintainerOrTeamAdmin: boolean,
isOnGlobalTeam: boolean
): JSX.Element => { ): JSX.Element => {
if (Object.keys(allTeamsScheduledQueriesError).length > 0) { if (Object.keys(allTeamsScheduledQueriesError).length > 0) {
return <TableDataError />; return <TableDataError />;
@ -78,7 +82,8 @@ const renderAllTeamsTable = (
inheritedQueries inheritedQueries
allScheduledQueriesList={allTeamsScheduledQueriesList} allScheduledQueriesList={allTeamsScheduledQueriesList}
teamId={teamId} teamId={teamId}
isTeamMaintainer={isTeamMaintainer} isTeamMaintainerOrTeamAdmin={isTeamMaintainerOrTeamAdmin}
isOnGlobalTeam={isOnGlobalTeam}
/> />
</div> </div>
); );
@ -138,11 +143,57 @@ interface ITeamOptions {
const ManageSchedulePage = ({ const ManageSchedulePage = ({
params: { team_id }, params: { team_id },
}: ITeamSchedulesPageProps): JSX.Element => { }: ITeamSchedulesPageProps): JSX.Element => {
const teamId = parseInt(team_id, 10); let teamId = parseInt(team_id, 10);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { MANAGE_PACKS } = paths; const { MANAGE_PACKS } = paths;
const handleAdvanced = () => dispatch(push(MANAGE_PACKS)); 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(() => { useEffect(() => {
dispatch(queryActions.loadAll()); dispatch(queryActions.loadAll());
dispatch(teamActions.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 allQueries = useSelector((state: IRootState) => state.entities.queries);
const allQueriesList = Object.values(allQueries.data); const allQueriesList = Object.values(allQueries.data);
@ -197,38 +244,6 @@ const ManageSchedulePage = ({
const selectedTeam = isNaN(teamId) ? "global" : teamId; 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>( const [showInheritedQueries, setShowInheritedQueries] = useState<boolean>(
false false
); );
@ -257,6 +272,55 @@ const ManageSchedulePage = ({
setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal); setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal);
}, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]); }, [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 = ( const onRemoveScheduledQueryClick = (
selectedTableQueryIds: number[] selectedTableQueryIds: number[]
): void => { ): void => {
@ -372,21 +436,15 @@ const ManageSchedulePage = ({
[dispatch, teamId, toggleScheduleEditorModal] [dispatch, teamId, toggleScheduleEditorModal]
); );
const onChangeSelectedTeam = (selectedTeamId: number) => { if (selectedTeam === "global" && isTeamMaintainerOrTeamAdmin) {
if (isNaN(selectedTeamId)) {
dispatch(push(`${paths.MANAGE_SCHEDULE}`));
} else {
dispatch(push(`${paths.MANAGE_TEAM_SCHEDULE(selectedTeamId)}`));
}
};
if (selectedTeam === "global" && isTeamMaintainer) {
const teamMaintainerTeams = generateTeamOptionsDropdownItems(); const teamMaintainerTeams = generateTeamOptionsDropdownItems();
dispatch( if (teamMaintainerTeams.length) {
push( dispatch(
`${paths.MANAGE_TEAM_SCHEDULE(Number(teamMaintainerTeams[0].value))}` push(
) `${paths.MANAGE_TEAM_SCHEDULE(Number(teamMaintainerTeams[0].value))}`
); )
);
}
} }
return ( return (
@ -409,15 +467,7 @@ const ManageSchedulePage = ({
</div> </div>
) : ( ) : (
<div> <div>
<Dropdown {renderTitleOrDropdown()}
value={selectedTeam}
className={`${baseClass}__team-dropdown`}
options={generateTeamOptionsDropdownItems()}
searchable={false}
onChange={(newSelectedValue: number) =>
onChangeSelectedTeam(newSelectedValue)
}
/>
<div className={`${baseClass}__description`}> <div className={`${baseClass}__description`}>
{isNaN(teamId) ? ( {isNaN(teamId) ? (
<p> <p>
@ -438,7 +488,7 @@ const ManageSchedulePage = ({
{allScheduledQueriesList.length !== 0 && {allScheduledQueriesList.length !== 0 &&
allScheduledQueriesError.length !== 0 && ( allScheduledQueriesError.length !== 0 && (
<div className={`${baseClass}__action-button-container`}> <div className={`${baseClass}__action-button-container`}>
{!isTeamMaintainer && ( {!isTeamMaintainerOrTeamAdmin && (
<Button <Button
variant="inverse" variant="inverse"
onClick={handleAdvanced} onClick={handleAdvanced}
@ -465,7 +515,8 @@ const ManageSchedulePage = ({
allScheduledQueriesError, allScheduledQueriesError,
toggleScheduleEditorModal, toggleScheduleEditorModal,
teamId, teamId,
isTeamMaintainer isTeamMaintainerOrTeamAdmin,
isOnGlobalTeam || false
)} )}
</div> </div>
{/* must use ternary for NaN */} {/* must use ternary for NaN */}
@ -497,10 +548,11 @@ const ManageSchedulePage = ({
) : null} ) : null}
{showInheritedQueries && {showInheritedQueries &&
renderAllTeamsTable( renderAllTeamsTable(
teamId,
isTeamMaintainer,
allTeamsScheduledQueriesList, allTeamsScheduledQueriesList,
allTeamsScheduledQueriesError allTeamsScheduledQueriesError,
teamId,
isTeamMaintainerOrTeamAdmin,
isOnGlobalTeam || false
)} )}
{showScheduleEditorModal && ( {showScheduleEditorModal && (
<ScheduleEditorModal <ScheduleEditorModal

View file

@ -33,7 +33,8 @@ interface IScheduleListWrapperProps {
toggleScheduleEditorModal?: () => void; toggleScheduleEditorModal?: () => void;
teamId: number; teamId: number;
inheritedQueries?: boolean; inheritedQueries?: boolean;
isTeamMaintainer: boolean; isTeamMaintainerOrTeamAdmin: boolean;
isOnGlobalTeam: boolean;
} }
interface IRootState { interface IRootState {
entities: { entities: {
@ -55,7 +56,8 @@ const ScheduleListWrapper = ({
onEditScheduledQueryClick, onEditScheduledQueryClick,
teamId, teamId,
inheritedQueries, inheritedQueries,
isTeamMaintainer, isTeamMaintainerOrTeamAdmin,
isOnGlobalTeam,
}: IScheduleListWrapperProps): JSX.Element => { }: IScheduleListWrapperProps): JSX.Element => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { MANAGE_PACKS } = paths; const { MANAGE_PACKS } = paths;
@ -70,28 +72,34 @@ const ScheduleListWrapper = ({
<div className={`${noScheduleClass}__inner-text`}> <div className={`${noScheduleClass}__inner-text`}>
<h2>You don&apos;t have any queries scheduled.</h2> <h2>You don&apos;t have any queries scheduled.</h2>
<p> <p>
{!isTeamMaintainer {isOnGlobalTeam &&
? "Schedule a query, or go to your osquery packs via the 'Advanced' button." "Schedule a query, or go to your osquery packs via the 'Advanced' button."}
: "Schedule a query to run on hosts assigned to this team."} {isTeamMaintainerOrTeamAdmin &&
"Schedule a query to run on hosts assigned to this team."}
{!isOnGlobalTeam &&
!isTeamMaintainerOrTeamAdmin &&
"There are no scheduled queries assigned to this team."}
</p> </p>
<div className={`${noScheduleClass}__-cta-buttons`}> {(isOnGlobalTeam || isTeamMaintainerOrTeamAdmin) && (
<Button <div className={`${noScheduleClass}__-cta-buttons`}>
variant="brand"
className={`${noScheduleClass}__schedule-button`}
onClick={toggleScheduleEditorModal}
>
Schedule a query
</Button>
{!isTeamMaintainer && (
<Button <Button
variant="inverse" variant="brand"
onClick={handleAdvanced} className={`${noScheduleClass}__schedule-button`}
className={`${baseClass}__advanced-button`} onClick={toggleScheduleEditorModal}
> >
Advanced Schedule a query
</Button> </Button>
)} {isOnGlobalTeam && (
</div> <Button
variant="inverse"
onClick={handleAdvanced}
className={`${baseClass}__advanced-button`}
>
Advanced
</Button>
)}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -44,6 +44,14 @@ const isTeamAdmin = (user: IUser | null, teamId: number | null): boolean => {
return userTeamRole === "admin"; 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 // This checks against all teams
const isAnyTeamMaintainer = (user: IUser): boolean => { const isAnyTeamMaintainer = (user: IUser): boolean => {
if (!isOnGlobalTeam(user)) { if (!isOnGlobalTeam(user)) {
@ -95,6 +103,7 @@ export default {
isOnGlobalTeam, isOnGlobalTeam,
isTeamObserver, isTeamObserver,
isTeamMaintainer, isTeamMaintainer,
isTeamMaintainerOrTeamAdmin,
isAnyTeamMaintainer, isAnyTeamMaintainer,
isAnyTeamMaintainerOrTeamAdmin, isAnyTeamMaintainerOrTeamAdmin,
isTeamAdmin, isTeamAdmin,