From 4bb1e0c7729d5ec7b6d4cb1f21ff4dfd4f71b5b0 Mon Sep 17 00:00:00 2001 From: Roula O'Regan Date: Fri, 19 Nov 2021 19:07:15 -0800 Subject: [PATCH] Limit non job owners interaction Check user permissions before allowing a user to "eat, kill, retry" jobs that they do not own. Allow an override in "File -> Enable Job Interaction". This gets saved to the config file and is disabled by default. --- cuegui/cuegui/Constants.py | 2 + cuegui/cuegui/MainWindow.py | 34 ++++- cuegui/cuegui/MenuActions.py | 261 +++++++++++++++++++++++------------ cuegui/cuegui/Utils.py | 23 +++ 4 files changed, 230 insertions(+), 90 deletions(-) diff --git a/cuegui/cuegui/Constants.py b/cuegui/cuegui/Constants.py index 13b511317..b25ae0e50 100644 --- a/cuegui/cuegui/Constants.py +++ b/cuegui/cuegui/Constants.py @@ -137,6 +137,8 @@ def __packaged_version(): EMAIL_BODY_PREFIX = __config.get('email.body_prefix') EMAIL_BODY_SUFFIX = __config.get('email.body_suffix') EMAIL_DOMAIN = __config.get('email.domain') +USER_CONFIRM_RESTART = "You must restart for this action to take effect, close window?: " +USER_INTERACTION_PERMISSIONS = "You do not have permissions to {0} {1} owned by {2}" GITHUB_CREATE_ISSUE_URL = __config.get('links.issue.create') URL_USERGUIDE = __config.get('links.user_guide') diff --git a/cuegui/cuegui/MainWindow.py b/cuegui/cuegui/MainWindow.py index 787458ea2..1cbb7cd6b 100644 --- a/cuegui/cuegui/MainWindow.py +++ b/cuegui/cuegui/MainWindow.py @@ -68,6 +68,7 @@ def __init__(self, app_name, app_version, window_name, parent = None): self.name = window_name else: self.name = self.windows_names[0] + self.__isEnabled = bool(int(QtGui.qApp.settings.value("EnableJobInteraction", 0))) # Provides a location for widgets to the right of the menu menuLayout = QtWidgets.QHBoxLayout() @@ -195,6 +196,20 @@ def __createMenus(self): self.windowMenu = self.menuBar().addMenu("&Window") self.helpMenu = self.menuBar().addMenu("&Help") + if self.__isEnabled is False: + # Menu Bar: File -> Enable Job Interaction + enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Enable Job Interaction', self) + enableJobInteraction.setStatusTip('Enable Job Interaction') + enableJobInteraction.triggered.connect(self.__enableJobInteraction) + self.fileMenu.addAction(enableJobInteraction) + # allow user to disable the job interaction + else: + # Menu Bar: File -> Disable Job Interaction + enableJobInteraction = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Disable Job Interaction', self) + enableJobInteraction.setStatusTip('Disable Job Interaction') + enableJobInteraction.triggered.connect(self.__enableJobInteraction) + self.fileMenu.addAction(enableJobInteraction) + # Menu Bar: File -> Close Window close = QtWidgets.QAction(QtGui.QIcon('icons/exit.png'), '&Close Window', self) close.setStatusTip('Close Window') @@ -454,9 +469,26 @@ def __revertLayout(self): result = QtWidgets.QMessageBox.question( self, "Restart required ", - "You must restart for this action to take effect, close window?: ", + cuegui.Constants.USER_CONFIRM_RESTART, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) if result == QtWidgets.QMessageBox.Yes: self.settings.setValue("RevertLayout", True) self.__windowCloseApplication() + + def __enableJobInteraction(self): + """ Enable/Disable user job interaction """ + result = QtWidgets.QMessageBox.question( + self, + "Job Interaction Settings ", + cuegui.Constants.USER_CONFIRM_RESTART, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + + if result == QtWidgets.QMessageBox.Yes: + # currently not enabled, user wants to enable + if self.__isEnabled is False: + self.settings.setValue("EnableJobInteraction", 1) + self.__windowCloseApplication() + else: + self.settings.setValue("EnableJobInteraction", 0) + self.__windowCloseApplication() diff --git a/cuegui/cuegui/MenuActions.py b/cuegui/cuegui/MenuActions.py index 75e88e73b..30b5c9d8e 100644 --- a/cuegui/cuegui/MenuActions.py +++ b/cuegui/cuegui/MenuActions.py @@ -373,6 +373,7 @@ def resume(self, rpcObjects=None): def kill(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) + permissions = False if jobs: msg = ("Are you sure you want to kill these jobs?\n\n" "** Note: This will stop all running frames and " @@ -381,8 +382,17 @@ def kill(self, rpcObjects=None): if cuegui.Utils.questionBoxYesNo(self._caller, "Kill jobs?", msg, [job.data.name for job in jobs]): for job in jobs: - job.kill(reason=DEFAULT_JOB_KILL_REASON) - self.killDependents(jobs) + # check permissions + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", + "job(s)", + job.username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + job.kill(reason=DEFAULT_JOB_KILL_REASON) + permissions = True + if permissions: + self.killDependents(jobs) self._update() def killDependents(self, jobs): @@ -453,7 +463,14 @@ def eatDead(self, rpcObjects=None): "Eat all DEAD frames in selected jobs?", [job.data.name for job in jobs]): for job in jobs: - job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) + # check permissions + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat dead", + "job", + job.username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + job.eatFrames(state=[opencue.compiled_proto.job_pb2.DEAD]) self._update() autoEatOn_info = ["Enable auto eating", None, "eat"] @@ -480,12 +497,19 @@ def autoEatOff(self, rpcObjects=None): def retryDead(self, rpcObjects=None): jobs = self._getOnlyJobObjects(rpcObjects) if jobs: + # check permissions if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", "Retry all DEAD frames in selected jobs?", [job.data.name for job in jobs]): for job in jobs: - job.retryFrames( - state=[opencue.compiled_proto.job_pb2.DEAD]) + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry dead", + "job", + job.username()) + cuegui.showErrorMessageBox(msg) + else: + job.retryFrames( + state=[opencue.compiled_proto.job_pb2.DEAD]) self._update() dropExternalDependencies_info = ["Drop External Dependencies", None, "kill"] @@ -778,48 +802,73 @@ def setTags(self, rpcObjects=None): def kill(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Kill ALL frames in selected layers?", - [layer.data.name for layer in layers]): - for layer in layers: - layer.kill(reason=DEFAULT_FRAME_KILL_REASON) - self._update() + #check permissions + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Kill ALL frames in selected layers?", + [layer.data.name for layer in layers]): + for layer in layers: + layer.kill(reason=DEFAULT_FRAME_KILL_REASON) + self._update() eat_info = ["&Eat", None, "eat"] def eat(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Eat ALL frames in selected layers?", - [layer.data.name for layer in layers]): - for layer in layers: - layer.eat() - self._update() + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Eat ALL frames in selected layers?", + [layer.data.name for layer in layers]): + for layer in layers: + layer.eat() + self._update() retry_info = ["&Retry", None, "retry"] def retry(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Retry ALL frames in selected layers?", - [layer.data.name for layer in layers]): - for layer in layers: - layer.retry() - self._update() + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Retry ALL frames in selected layers?", + [layer.data.name for layer in layers]): + for layer in layers: + layer.retry() + self._update() retryDead_info = ["Retry dead frames", None, "retry"] def retryDead(self, rpcObjects=None): layers = self._getOnlyLayerObjects(rpcObjects) if layers: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Retry all DEAD frames in selected layers?", - [layer.data.name for layer in layers]): - layers[-1].parent().retryFrames(layer=[layer.data.name for layer in layers], - state=[opencue.api.job_pb2.DEAD]) - self._update() + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry dead", + "layers", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Retry all DEAD frames in selected layers?", + [layer.data.name for layer in layers]): + layers[-1].parent().retryFrames(layer=[layer.data.name for layer in layers], + state=[opencue.api.job_pb2.DEAD]) + self._update() markdone_info = ["Mark done", None, "markdone"] @@ -1042,10 +1091,17 @@ def retry(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: job = self._getSource() - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", "Retry selected frames?", names): - job.retryFrames(name=names) - self._update() + # check permissions + if not cuegui.Utils.isPermissible(job): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("retry", + "frames", + job.username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", "Retry selected frames?", names): + job.retryFrames(name=names) + self._update() previewMain_info = ["Preview Main", None, "previewMain"] @@ -1079,23 +1135,37 @@ def previewAovs(self, rpcObjects=None): def eat(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Eat selected frames?", - names): - self._getSource().eatFrames(name=names) - self._update() + #check permissions + print(self._getSource()) + if not cuegui.Utils.isPermissible(self._getSource()): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Eat selected frames?", + names): + self._getSource().eatFrames(name=names) + self._update() kill_info = ["&Kill", None, "kill"] def kill(self, rpcObjects=None): names = [frame.data.name for frame in self._getOnlyFrameObjects(rpcObjects)] if names: - if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", - "Kill selected frames?", - names): - self._getSource().killFrames(reason=DEFAULT_FRAME_KILL_REASON, - name=names) - self._update() + if not cuegui.Utils.isPermissible(self._getSource(), self): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("kill", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo(self._caller, "Confirm", + "Kill selected frames?", + names): + self._getSource().killFrames(reason=DEFAULT_FRAME_KILL_REASON, + name=names) + self._update() markAsWaiting_info = ["Mark as &waiting", None, "configure"] @@ -1199,47 +1269,53 @@ def eatandmarkdone(self, rpcObjects=None): frames = self._getOnlyFrameObjects(rpcObjects) if frames: frameNames = [frame.data.name for frame in frames] + #check permissions + if not cuegui.Utils.isPermissible(self._getSource(), self): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat and mark done", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", + "Eat and Mark done all selected frames?\n" + "(Drops any dependencies that are waiting on these frames)\n\n" + "If a frame is part of a layer that will now only contain\n" + "eaten or succeeded frames, any dependencies on the\n" + "layer will be dropped as well.", + frameNames): + + # Mark done the layers to drop their dependencies if the layer is done + + if len(frames) == 1: + # Since only a single frame selected, check if layer is only one frame + layer = opencue.api.findLayer(self._getSource().data.name, + frames[0].data.layer_name) + if layer.data.layer_stats.total_frames == 1: + # Single frame selected of single frame layer, mark done and eat it all + layer.eat() + layer.markdone() - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", - "Eat and Mark done all selected frames?\n" - "(Drops any dependencies that are waiting on these frames)\n\n" - "If a frame is part of a layer that will now only contain\n" - "eaten or succeeded frames, any dependencies on the\n" - "layer will be dropped as well.", - frameNames): - - # Mark done the layers to drop their dependencies if the layer is done - - if len(frames) == 1: - # Since only a single frame selected, check if layer is only one frame - layer = opencue.api.findLayer(self._getSource().data.name, - frames[0].data.layer_name) - if layer.data.layer_stats.total_frames == 1: - # Single frame selected of single frame layer, mark done and eat it all - layer.eat() - layer.markdone() + self._update() + return - self._update() - return + self._getSource().eatFrames(name=frameNames) + self._getSource().markdoneFrames(name=frameNames) - self._getSource().eatFrames(name=frameNames) - self._getSource().markdoneFrames(name=frameNames) + # Warning: The below assumes that eaten frames are desired to be markdone - # Warning: The below assumes that eaten frames are desired to be markdone - - # Wait for the markDoneFrames to be processed, then drop the dependencies on - # the layer if all frames are done. - layerNames = [frame.data.layer_name for frame in frames] - time.sleep(1) - for layer in self._getSource().getLayers(): - if layer.data.name in layerNames: - if ( - layer.data.layer_stats.eaten_frames + - layer.data.layer_stats.succeeded_frames == - layer.data.layer_stats.total_frames): - layer.markdone() - self._update() + # Wait for the markDoneFrames to be processed, then drop the dependencies on + # the layer if all frames are done. + layerNames = [frame.data.layer_name for frame in frames] + time.sleep(1) + for layer in self._getSource().getLayers(): + if layer.data.name in layerNames: + if ( + layer.data.layer_stats.eaten_frames + + layer.data.layer_stats.succeeded_frames == + layer.data.layer_stats.total_frames): + layer.markdone() + self._update() class ShowActions(AbstractActions): @@ -1672,14 +1748,21 @@ def view(self, rpcObjects=None): def kill(self, rpcObjects=None): procs = self._getOnlyProcObjects(rpcObjects) if procs: - if cuegui.Utils.questionBoxYesNo( - self._caller, "Confirm", "Kill selected frames?", - ["%s -> %s @ %s" % (proc.data.job_name, proc.data.frame_name, proc.data.name) - for proc in procs]): - for proc in procs: - self.cuebotCall(proc.kill, - "Kill Proc %s Failed" % proc.data.name) - self._update() + print(self._getSource()) + if not cuegui.Utils.isPermissible(self._getSource(), self): + msg = cuegui.Constants.USER_INTERACTION_PERMISSIONS.format("eat and mark done", + "frames", + self._getSource().username()) + cuegui.Utils.showErrorMessageBox(msg) + else: + if cuegui.Utils.questionBoxYesNo( + self._caller, "Confirm", "Kill selected frames?", + ["%s -> %s @ %s" % (proc.data.job_name, proc.data.frame_name, proc.data.name) + for proc in procs]): + for proc in procs: + self.cuebotCall(proc.kill, + "Kill Proc %s Failed" % proc.data.name) + self._update() unbook_info = ["Unbook", None, "eject"] diff --git a/cuegui/cuegui/Utils.py b/cuegui/cuegui/Utils.py index 5b88345fd..da8b596d2 100644 --- a/cuegui/cuegui/Utils.py +++ b/cuegui/cuegui/Utils.py @@ -35,6 +35,7 @@ from qtpy import QtCore from qtpy import QtGui from qtpy import QtWidgets +import getpass import six import opencue @@ -671,3 +672,25 @@ def byteConversion(amount, btype): for _ in range(n): _bytes *= 1024 return _bytes + + +def isPermissible(jobObject): + """ + Validate if the current user has the correct permissions to perform + the action + + :param userName: jobObject + :ptype userName: Opencue Job Object + :return: + """ + hasPermissions = False + # Case 1. Check if current user is the job owner + currentUser = getpass.getuser() + if currentUser.lower() == jobObject.username().lower(): + hasPermissions = True + + # Case 2. Check if "Enable not owned Job Interactions" is Enabled + if bool(int(QtGui.qApp.settings.value("EnableJobInteraction", 0))): + hasPermissions = True + + return hasPermissions