diff --git a/MythicReactUI/src/components/MythicComponents/TaskDeleteDialog.js b/MythicReactUI/src/components/MythicComponents/TaskDeleteDialog.js new file mode 100644 index 000000000..10e8b5c1e --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/TaskDeleteDialog.js @@ -0,0 +1,80 @@ +import React, {useState} from 'react'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import MythicTextField from '../../MythicComponents/MythicTextField'; +import {useQuery, gql, useMutation} from '@apollo/client'; +import LinearProgress from '@mui/material/LinearProgress'; + +const deleteTaskByPkMutation = gql` +mutation deleteTask($task_id: Int!) { + delete_task_by_pk(id: $task_id) { + id + } +} +`; + + +const getStatusQuery= gql` +query getStatusQuery ($task_id: Int!) { + task_by_pk(id: $task_id) { + status + commentOperator { + username + } + id + } +} +`; + +export function TaskDeleteDialog(props) { + const [status, setStatus] = useState(""); + const { loading, error } = useQuery(getStatusQuery, { + variables: {task_id: props.task_id}, + onCompleted: data => { + //setStatus(data.task_by_pk.status) + setStatus("Warning: This will delete Task!"); + }, + fetchPolicy: "network-only" + }); + + const [deleteTask] = useMutation(deleteTaskByPkMutation,{ + variables: {task_id: props.task_id} + + }); + + if (loading) { + return ; + } + if (error) { + console.error(error); + return
Error!
; + } + const onCommitSubmit = () => { + deleteTask({variables: {task_id: props.task_id}}); + props.onClose(); + } + const onChange = (name, value, error) => { + setStatus(value); + } + + return ( + + Cancel Task + + Are you sure? + + + + + + + + ); +} + diff --git a/MythicReactUI/src/components/MythicComponents/TaskDisplayContainer.js b/MythicReactUI/src/components/MythicComponents/TaskDisplayContainer.js new file mode 100644 index 000000000..14924b5e8 --- /dev/null +++ b/MythicReactUI/src/components/MythicComponents/TaskDisplayContainer.js @@ -0,0 +1,529 @@ +import React, {useEffect} from 'react'; +import { copyStringToClipboard } from '../../utilities/Clipboard'; +import GetAppIcon from '@mui/icons-material/GetApp'; +import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; +import {ResponseDisplay, ResponseDisplayConsole} from './ResponseDisplay'; +import RateReviewOutlinedIcon from '@mui/icons-material/RateReviewOutlined'; +import { MythicDialog } from '../../MythicComponents/MythicDialog'; +import {TaskCommentDialog} from './TaskCommentDialog'; +import {ViewEditTagsDialog} from '../../MythicComponents/MythicTag'; +import {useTheme} from '@mui/material/styles'; +import LockIcon from '@mui/icons-material/Lock'; +import LockOpenIcon from '@mui/icons-material/LockOpen'; +import {TaskOpsecDialog} from './TaskOpsecDialog'; +import BlockIcon from '@mui/icons-material/Block'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import {TaskDeleteDialog} from './TaskDeleteDialog'; +import {TaskViewParametersDialog} from './TaskViewParametersDialog'; +import {TaskViewStdoutStderrDialog} from './TaskViewStdoutStderrDialog'; +import {snackActions} from '../../utilities/Snackbar'; +import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined'; +import CodeIcon from '@mui/icons-material/Code'; +import KeyboardIcon from '@mui/icons-material/Keyboard'; +import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber'; +import {TaskTokenDialog} from './TaskTokenDialog'; +import Grid from '@mui/material/Grid'; +import ReplayIcon from '@mui/icons-material/Replay'; +import {gql, useMutation, useLazyQuery } from '@apollo/client'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; +import { faExternalLinkAlt, faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons'; +import SearchIcon from '@mui/icons-material/Search'; +import SpeedDial from '@mui/material/SpeedDial'; +import SpeedDialIcon from '@mui/material/SpeedDialIcon'; +import SpeedDialAction from '@mui/material/SpeedDialAction'; +import { Backdrop } from '@mui/material'; +import {downloadFileFromMemory} from '../../utilities/Clipboard'; +import InsertPhotoIcon from '@mui/icons-material/InsertPhoto'; +import html2canvas from 'html2canvas'; +import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen'; +import CodeOffIcon from '@mui/icons-material/CodeOff'; +import SettingsTwoToneIcon from '@mui/icons-material/SettingsTwoTone'; + +const ReissueTaskMutationGQL = gql` +mutation reissueTaskMutation($task_id: Int!){ + reissue_task(task_id: $task_id){ + status + error + } +} +`; +const ReissueTaskHandlerMutationGQL = gql` +mutation reissueTaskHandlerMutation($task_id: Int!){ + reissue_task_handler(task_id: $task_id){ + status + error + } +} +`; +const getAllResponsesLazyQuery = gql` +query subResponsesQuery($task_id: Int!) { + response(where: {task_id: {_eq: $task_id}}, order_by: {id: asc}) { + id + response: response_text + } +}`; + +export const TaskDisplayContainer = ({task, me}) => { + const [viewBrowserScript, setViewBrowserScript] = React.useState(true); + const [commandID, setCommandID] = React.useState(0); + const [searchOutput, setSearchOutput] = React.useState(false); + const [selectAllOutput, setSelectAllOutput] = React.useState(false); + const responseRef = React.useRef(null); + useEffect( () => { + setCommandID(task.command === null ? 0 : task.command.id); + }, [task.command]); + const toggleViewBrowserScript = React.useCallback( () => { + setViewBrowserScript(!viewBrowserScript); + }, [viewBrowserScript]); + const toggleSelectAllOutput = React.useCallback( () => { + setSelectAllOutput(!selectAllOutput); + }, [selectAllOutput]); + const toggleOpenSearch = React.useCallback( () => { + setSearchOutput(!searchOutput); + }, [searchOutput]); + + return ( + <> + + + + + + + + ); +} +export const TaskDisplayContainerFlat = ({task, me}) => { + const [viewBrowserScript, setViewBrowserScript] = React.useState(true); + const [commandID, setCommandID] = React.useState(0); + const [searchOutput, setSearchOutput] = React.useState(false); + const [selectAllOutput, setSelectAllOutput] = React.useState(false); + const responseRef = React.useRef(null); + useEffect( () => { + setCommandID(task.command === null ? 0 : task.command.id); + setSearchOutput(false); + setSelectAllOutput(false); + setViewBrowserScript(true); + }, [task.command, task.id]); + const toggleViewBrowserScript = React.useCallback( () => { + setViewBrowserScript(!viewBrowserScript); + }, [viewBrowserScript]); + const toggleSelectAllOutput = React.useCallback( () => { + setSelectAllOutput(!selectAllOutput); + }, [selectAllOutput]); + const toggleOpenSearch = React.useCallback( () => { + setSearchOutput(!searchOutput); + }, [searchOutput]); + + return ( +
+ + +
+ + ) +} +export const TaskDisplayContainerConsole = ({task, me}) => { + const [viewBrowserScript, setViewBrowserScript] = React.useState(true); + const [commandID, setCommandID] = React.useState(0); + const [searchOutput, setSearchOutput] = React.useState(false); + const [selectAllOutput, setSelectAllOutput] = React.useState(false); + const responseRef = React.useRef(null); + useEffect( () => { + setCommandID(task.command === null ? 0 : task.command.id); + }, [task.command]); + const toggleViewBrowserScript = React.useCallback( () => { + setViewBrowserScript(!viewBrowserScript); + }, [viewBrowserScript]); + const toggleSelectAllOutput = React.useCallback( () => { + setSelectAllOutput(!selectAllOutput); + }, [selectAllOutput]); + const toggleOpenSearch = React.useCallback( () => { + setSearchOutput(!searchOutput); + }, [searchOutput]); + + return ( + <> + + + + ); +} + +// the base64 decode function to handle unicode was pulled from the following stack overflow post +// https://stackoverflow.com/a/30106551 +function b64DecodeUnicode(str) { + // Going backwards: from bytestream, to percent-encoding, to original string. + //console.log("decoding", str); + try{ + return decodeURIComponent(window.atob(str)); + }catch(error){ + //console.log("Failed to base64 decode response", error) + return atob(str); + } +} +const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput, + toggleOpenSearch, taskData, viewAllOutput, me, + responseRef, style, fabStyle, viewBrowserScript}) => { + const theme = useTheme(); + const [task, setTask] = React.useState(taskData || {}); + const [openSpeedDial, setOpenSpeedDial] = React.useState(false); + const [openTaskTagDialog, setOpenTaskTagDialog] = React.useState(false); + const [openCommentDialog, setOpenCommentDialog] = React.useState(false); + const [openParametersDialog, setOpenParametersDialog] = React.useState(false); + const [openTokenDialog, setOpenTokenDialog] = React.useState(false); + const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false); + const [openDeleteTaskDialog, setOpenDeleteTaskDialog] = React.useState(false); + const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"}); + const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, { + fetchPolicy: "network-only", + onCompleted: (data) => { + const output = data.response.reduce( (prev, cur) => { + return prev + b64DecodeUnicode(cur.response); + }, b64DecodeUnicode("")); + downloadFileFromMemory(output, "task_" + task.id + ".txt"); + }, + onError: (data) => { + + } + }); + React.useEffect( () => { + setTask(taskData); + }, [taskData.id, taskData.token, taskData.original_params, taskData.opsec_pre_blocked, taskData.opsec_pre_bypassed, taskData.opsec_post_blocked, taskData.opsec_post_bypassed]) + const onDownloadResponses = () => { + downloadResponses({variables: {task_id: task.id}}); + setOpenSpeedDial(false); + }; + const copyToClipboard = () => { + let result = copyStringToClipboard(task.original_params); + if(result){ + snackActions.success("Copied text!"); + }else{ + snackActions.error("Failed to copy text"); + } + setOpenSpeedDial(false); + }; + const [reissueTask] = useMutation(ReissueTaskMutationGQL, { + onCompleted: data => { + if(data.reissue_task.status === "success"){ + snackActions.success("Successfully re-issued task to Mythic"); + }else{ + snackActions.error("Failed to re-issue task to Mythic: " + data.reissue_task.error); + } + }, + onError: data => { + console.log(data); + snackActions.error("Failed to re-issue task: " + data); + } + }); + const [reissueTaskHandler] = useMutation(ReissueTaskHandlerMutationGQL, { + onCompleted: data => { + if(data.reissue_task_handler.status === "success"){ + snackActions.success("Successfully resubmitted task for handling"); + }else{ + snackActions.warning("Failed to resubmit task for handling: " + data.reissue_task_handler.error); + } + + }, + onError: data => { + console.log(data); + snackActions.error("Error resubmitting task for handling: " + data); + } + }); + const onDownloadImageClickPng = () => { + // we calculate a transform for the nodes so that all nodes are visible + // we then overwrite the transform of the `.react-flow__viewport` element + // with the style option of the html-to-image library + snackActions.info("Saving image to png..."); + (async () => { + const canvas = await html2canvas(responseRef.current); + const image = canvas.toDataURL("image/png", 1.0); + const fakeLink = window.document.createElement("a"); + fakeLink.style = "display:none;"; + fakeLink.download = "task_output.png"; + + fakeLink.href = image; + + document.body.appendChild(fakeLink); + fakeLink.click(); + document.body.removeChild(fakeLink); + + fakeLink.remove(); + + })(); + }; + const onReissueTask = () => { + reissueTask({variables: {task_id: task.id}}); + } + const onReissueTaskHandler = () => { + reissueTaskHandler({variables: {task_id: task.id}}); + } + return ( + + {setOpenSpeedDial(false);}} style={{zIndex: 2, position: "absolute"}}/> + {openTaskTagDialog ? + ({setOpenTaskTagDialog(false);}} + innerDialog={{setOpenTaskTagDialog(false);}} />} + />) : null} + {openCommentDialog ? + ({setOpenCommentDialog(false);}} + innerDialog={{setOpenCommentDialog(false);}} />} + />) : null + } + {openParametersDialog ? + ({setOpenParametersDialog(false);}} + innerDialog={{setOpenParametersDialog(false);}} />} + />) : null + } + {openTokenDialog ? + ({setOpenTokenDialog(false);}} + innerDialog={{setOpenTokenDialog(false);}} />} + />) : null + } + {openOpsecDialog.open ? + ({setOpenOpsecDialog({...openOpsecDialog, open: false});}} + innerDialog={{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />} + />) : null + } + {openDeleteTaskDialog ? + ({setOpenDeleteTaskDialog(false);}} + innerDialog={{setOpenDeleteTaskDialog(false);}} />} + />) : null + } + + {openStdoutStderrDialog ? + ({setOpenStdoutStderrDialog(false);}} + innerDialog={{setOpenStdoutStderrDialog(false);}} />} + />) : null + } + } + style={{...style}} + onClick={()=>{setOpenSpeedDial(!openSpeedDial)}} + FabProps={{...fabStyle, color: "secondary", size: "small", sx: {minHeight: "30px", height: "30px", width: "30px"}}} + open={openSpeedDial} + direction="right" + > + : } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Toggle BrowserScript"} + onClick={() => {toggleViewBrowserScript();setOpenSpeedDial(false);}} + /> + : } + arrow + tooltipPlacement={"top"} + tooltipTitle={viewAllOutput ? "View Paginated Output" : "View All Output"} + onClick={() => {toggleSelectAllOutput();setOpenSpeedDial(false);}} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Search Output"} + onClick={() => {toggleOpenSearch();setOpenSpeedDial(false);}} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Download output"} + onClick={onDownloadResponses} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Download screenshot of output"} + onClick={onDownloadImageClickPng} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Edit Tags"} + onClick={()=>{setOpenTaskTagDialog(true);setOpenSpeedDial(false);}} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Open Task in New Window"} + onClick={()=> {window.open('/new/task/' + task.display_id, "_blank")}} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Copy original params to clipboard"} + onClick={copyToClipboard} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Edit Comment"} + onClick={()=>{setOpenCommentDialog(true);setOpenSpeedDial(false);}} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"View All Parameters"} + onClick={()=>{setOpenParametersDialog(true);setOpenSpeedDial(false);}} + /> + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"View Stdout/Stderr of Task"} + onClick={()=>{setOpenStdoutStderrDialog(true);setOpenSpeedDial(false);}} + /> + {task.opsec_pre_blocked === null ? null : ( task.opsec_pre_bypassed === false ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Submit OPSEC PreCheck Bypass Request"} + onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});setOpenSpeedDial(false);}} + /> + ): ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"View OPSEC PreCheck Data"} + onClick={()=>{setOpenOpsecDialog({open: true, view: "pre"});setOpenSpeedDial(false);}} + /> + ) + ) + } + {task.opsec_post_blocked === null ? null : ( task.opsec_post_bypassed === false ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Submit OPSEC PostCheck Bypass Request"} + onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});setOpenSpeedDial(false);}} + /> + ): ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"View OPSEC PostCheck Data"} + onClick={()=>{setOpenOpsecDialog({open: true, view: "post"});setOpenSpeedDial(false);}} + /> + ) + ) + } + {task.status.toLowerCase().includes("submitted") ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Delete Task Before Submission"} + onClick={() => {setOpenDeleteTaskDialog(true);setOpenSpeedDial(false);}} + /> + ) : ( + task.status.toLowerCase().includes("delegating tasks") ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Delete Task Before Submission"} + onClick={() => {setOpenDeleteTaskDialog(true);setOpenSpeedDial(false);}} + /> + ) : null )} + {task.token === null ? null : ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"View Token Information"} + onClick={()=>{setOpenTokenDialog(true);setOpenSpeedDial(false);}} + /> + )} + {task.status.toLowerCase().includes("error: container") ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Resubmit Tasking"} + onClick={onReissueTask} + /> + ) : null} + {task.status.toLowerCase().includes("error: task") ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Resubmit Task Handler"} + onClick={onReissueTaskHandler} + /> + ):null} + + + + ) +} diff --git a/MythicReactUI/src/components/pages/Callbacks/TaskDeleteDialog.js b/MythicReactUI/src/components/pages/Callbacks/TaskDeleteDialog.js new file mode 100644 index 000000000..c18ceb94c --- /dev/null +++ b/MythicReactUI/src/components/pages/Callbacks/TaskDeleteDialog.js @@ -0,0 +1,103 @@ +import React, {useState} from 'react'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import MythicTextField from '../../MythicComponents/MythicTextField'; +import {useQuery, gql, useMutation} from '@apollo/client'; +import LinearProgress from '@mui/material/LinearProgress'; + +const deleteTaskByPkMutation = gql` +mutation createTask($callback_display_id: Int!, $task_id: String!, $parent_task_id: Int) { + createTask( + callback_id: $callback_display_id, + command: "clear", + params: $task_id, + token_id: null, + is_interactive_task: false, + interactive_task_type: null, + parent_task_id: $parent_task_id, + tasking_location: "scripting", + files: null){ + id + } +} +`; + + +const getStatusQuery= gql` +query getStatusQuery ($task_id: Int!) { + task_by_pk(id: $task_id) { + status + commentOperator { + username + } + id + display_id + agent_task_id + callback_id + parent_task_id + callback { + display_id + } + } +} +`; + +export function TaskDeleteDialog(props) { + const [status, setStatus] = useState(""); + const [callback_display_id, setCallbackID] = useState(""); + const [task_id, setTaskID] = useState(""); + const [parent_task_id, setParentTaskID] = useState(""); + const { loading, error } = useQuery(getStatusQuery, { + variables: {task_id: props.task_id}, + onCompleted: data => { + setCallbackID(data.task_by_pk.callback.display_id); + setTaskID(data.task_by_pk.display_id.toString()); + setParentTaskID(data.task_by_pk.parent_task_id); + + setStatus("Warning: This will 'clear' the task with display id of " + data.task_by_pk.display_id); + }, + fetchPolicy: "network-only" + }); + + const [deleteTask] = useMutation(deleteTaskByPkMutation,{ + variables: {callback_display_id: callback_display_id , task_id: task_id, parent_task_id: parent_task_id} + + }); + + if (loading) { + return ; + } + if (error) { + console.error(error); + return
Error!
; + } + const onCommitSubmit = () => { + //alert(callback_display_id + ":" + props.task_id + ":" + parent_task_id); + deleteTask({variables: {callback_display_id: callback_display_id , task_id: task_id, parent_task_id: parent_task_id}}); + props.onClose(); + } + const onChange = (name, value, error) => { + setStatus(value); + } + + return ( + + Cancel Task + + Are you sure? + + + + + + + + ); +} + diff --git a/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js b/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js index 4fff859e7..4e7270556 100644 --- a/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js +++ b/MythicReactUI/src/components/pages/Callbacks/TaskDisplayContainer.js @@ -11,6 +11,8 @@ import {useTheme} from '@mui/material/styles'; import LockIcon from '@mui/icons-material/Lock'; import LockOpenIcon from '@mui/icons-material/LockOpen'; import {TaskOpsecDialog} from './TaskOpsecDialog'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import {TaskDeleteDialog} from './TaskDeleteDialog'; import {TaskViewParametersDialog} from './TaskViewParametersDialog'; import {TaskViewStdoutStderrDialog} from './TaskViewStdoutStderrDialog'; import {snackActions} from '../../utilities/Snackbar'; @@ -220,6 +222,7 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput const [openParametersDialog, setOpenParametersDialog] = React.useState(false); const [openTokenDialog, setOpenTokenDialog] = React.useState(false); const [openStdoutStderrDialog, setOpenStdoutStderrDialog] = React.useState(false); + const [openDeleteTaskDialog, setOpenDeleteTaskDialog] = React.useState(false); const [openOpsecDialog, setOpenOpsecDialog] = React.useState({open: false, view: "pre"}); const [downloadResponses] = useLazyQuery(getAllResponsesLazyQuery, { fetchPolicy: "network-only", @@ -336,6 +339,12 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput innerDialog={{setOpenOpsecDialog({...openOpsecDialog, open: false});}} />} />) : null } + {openDeleteTaskDialog ? + ({setOpenDeleteTaskDialog(false);}} + innerDialog={{setOpenDeleteTaskDialog(false);}} />} + />) : null + } {openStdoutStderrDialog ? (} + arrow + tooltipPlacement={"top"} + tooltipTitle={"Delete Task Before Submission"} + onClick={() => {setOpenDeleteTaskDialog(true);setOpenSpeedDial(false);}} + /> + ) : ( + task.status.toLowerCase().includes("delegating tasks") ? ( + } + arrow + tooltipPlacement={"top"} + tooltipTitle={"Delete Task Before Submission"} + onClick={() => {setOpenDeleteTaskDialog(true);setOpenSpeedDial(false);}} + /> + ) : null )} {task.token === null ? null : ( } @@ -498,4 +525,4 @@ const SpeedDialDisplayGeneric = ({toggleViewBrowserScript, toggleSelectAllOutput ) -} \ No newline at end of file +} diff --git a/hasura-docker/metadata/tables.yaml b/hasura-docker/metadata/tables.yaml index 59aa46959..0841c173a 100644 --- a/hasura-docker/metadata/tables.yaml +++ b/hasura-docker/metadata/tables.yaml @@ -6405,6 +6405,22 @@ check: {} set: comment_operator_id: x-hasura-User-Id + delete_permissions: + - role: mythic_admin + permission: + filter: + operator_id: + _eq: X-Hasura-User-Id + - role: operation_admin + permission: + filter: + operator_id: + _eq: X-Hasura-User-Id + - role: operator + permission: + filter: + operator_id: + _eq: X-Hasura-User-Id - table: name: taskartifact schema: public