From f08b24be1165092eb1574994f17576ee847c1c78 Mon Sep 17 00:00:00 2001 From: novak_i Date: Tue, 25 Feb 2020 03:06:31 -0500 Subject: [PATCH 1/6] Implement password protection --- conda-recipe/meta.yaml | 1 + src/launcher.py | 56 +++++++++++++++++++++++++++++--- src/launcher_model.py | 12 ++++--- src/protect.py | 72 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 src/protect.py diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index a68cf50..7a02048 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -22,6 +22,7 @@ build: entry_points: - pylauncher = pylauncher.launcher:main - pylauncher-convert = pylauncher.convert.convert:main + - pylauncher-protect = pylauncher.protect:main about: home: https://github.psi.ch/projects/COS/repos/pylauncher/browse diff --git a/src/launcher.py b/src/launcher.py index 59b8c0d..ea9fc48 100755 --- a/src/launcher.py +++ b/src/launcher.py @@ -5,19 +5,19 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# ---------python 2/3 compatibility imports--------- - import platform import argparse import copy import enum import shlex import subprocess +import hashlib +import sys from PyQt5 import QtCore from PyQt5.QtCore import Qt from PyQt5.QtGui import QDesktopServices, QIcon, QCursor, QKeySequence -from PyQt5.QtWidgets import QMainWindow, QMenu, QWidgetAction, QLineEdit, QWidget, QHBoxLayout, QToolButton, QVBoxLayout, QCheckBox, QAction, QLabel, QPushButton, QApplication +from PyQt5.QtWidgets import QMainWindow, QMenu, QWidgetAction, QLineEdit, QWidget, QHBoxLayout, QToolButton, QVBoxLayout, QCheckBox, QAction, QLabel, QPushButton, QApplication, QInputDialog, QMessageBox from .launcher_model import * @@ -30,6 +30,35 @@ def stringContains(string, substring, caseSensitive): else: return substring.lower() in string.lower() +def convertPwdToHash(password): + m = hashlib.md5() + m.update(password.encode()) + return m.hexdigest() + +def verifyPassword(object, password): + passwordInput = showPasswordDialog(object) + if passwordInput is not None: + convertedPwd = convertPwdToHash(passwordInput) + if convertedPwd == password: + return True + else: + showWrongPasswordDialog(object) + return False +def showPasswordDialog(object): + + password, ok = QInputDialog.getText(object.window(), + 'Password', + 'Enter password:') + if ok: + return password + return None + +def showWrongPasswordDialog(parent): + messageBox = QMessageBox(parent.window()) + messageBox.setText("Wrong password") + messageBox.setStandardButtons(QMessageBox.Ok) + returnValue = messageBox.exec() + class SearchOptions(enum.Enum): @@ -78,6 +107,7 @@ class LauncherWindow(QMainWindow): theme_base) self.menuModel = self.buildMenuModel(rootFilePath) + self.setWindowTitle(self.menuModel.main_title.text) # QMainWindow has predefined layout. Content should be in the central # widget. Create widget with a QVBoxLayout and set it as central. @@ -117,6 +147,10 @@ class LauncherWindow(QMainWindow): if self.use_sbox: self.searchInput.setMouseTracking(True) + if self.menuModel.password is not None: + if not verifyPassword(self, self.menuModel.password): + sys.exit(-1) + def setNewView(self, rootMenuFile, text=None): """Rebuild launcher from new config file. @@ -128,6 +162,7 @@ class LauncherWindow(QMainWindow): del self.menuModel self.menuModel = self.buildMenuModel(rootMenuFile) + if text: self.setWindowTitle(text) else: @@ -246,6 +281,7 @@ class LauncherMenu(QMenu): candidate = self while type(candidate) != LauncherWindow: candidate = candidate.parent() + print(candidate) return candidate @@ -948,6 +984,7 @@ class LauncherCmdButton(LauncherNamedButton): def __init__(self, itemModel, sectionTitle=None, parent=None): LauncherNamedButton.__init__(self, itemModel, sectionTitle, parent) self.cmd = itemModel.cmd + self.pwd = itemModel.pwd self.clicked.connect(self.executeCmd) toolTip = "" @@ -971,16 +1008,25 @@ class LauncherCmdButton(LauncherNamedButton): cb.setText(self.cmd, mode=cb.Clipboard) - def executeCmd(self): + def executeCmd(self, itemModel): """ Run specified command as a separate process Runs commands from the same environment ($PATH) as the launcher was started. Apart from "bash" it aboarts scripts without shebang on first line (strictly). """ + self.parent().hideAll() # When done hide all popuped menus try: - subprocess.Popen(shlex.split(self.cmd)) + if self.pwd is not None: + """passwordInput = showPasswordDialog(self) + if passwordInput is not None: + convertedPwd = convertPwdToHash(passwordInput) + if convertedPwd != self.pwd: + showWrongPasswordDialog() + return""" + if verifyPassword(self, self.pwd): + subprocess.Popen(shlex.split(self.cmd)) except OSError: warn_msg = "Command \"" + self.cmd + "\" cannot be executed. " + \ "Wrong path or bad/no interpreter." diff --git a/src/launcher_model.py b/src/launcher_model.py index 2340232..bf6a164 100644 --- a/src/launcher_model.py +++ b/src/launcher_model.py @@ -4,17 +4,17 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# ---------python 2/3 compatibility imports--------- import sys from urllib.request import urlopen from urllib.parse import urljoin from urllib.error import URLError -# ------end of python 2/3 compatibility imports----- import os import re import json import logging +import hashlib +import sys def join_launcher_path(base, file): # In case file is absolute path ora full url, base will be ignored @@ -63,6 +63,7 @@ class launcher_menu_model(object): """ def __init__(self, parent, menu_file_path, level, launcher_cfg): + self.password = None self.menu_items = list() self.parent = parent self.level = level @@ -87,6 +88,8 @@ class launcher_menu_model(object): if 0 == self.level: self.flags = menu.get("flags", dict()) + self.password = menu.get("password", None) + main_title_item = menu.get("menu-title", dict()) self.main_title = launcher_main_title_item( main_title_item, @@ -182,7 +185,7 @@ class launcher_menu_model(object): sys.exit() def __repr__(self): - s = "{} (nelm: {})\n".format(self.main_title, len(self.menu_items)) + s = "{} (nelm: {}) {}\n".format(self.main_title, len(self.menu_items), self.password) tabs = "\t" *self.level strings = list(map(repr,self.menu_items)) strings = [tabs + str for str in strings] @@ -258,6 +261,7 @@ class launcher_cmd_item(launcher_menu_model_item): self.cmd = self.cmd.format(**params) + self.pwd = parent.password class launcher_sub_menu_item(launcher_menu_model_item): @@ -302,4 +306,4 @@ class launcher_title_item(launcher_menu_model_item): """Text menu separator.""" def __init__(self, parent, item): - launcher_menu_model_item.__init__(self, parent, item) + launcher_menu_model_item.__init__(self, parent, item) \ No newline at end of file diff --git a/src/protect.py b/src/protect.py new file mode 100644 index 0000000..1b8ebbf --- /dev/null +++ b/src/protect.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +import os +import re +import json +import logging +import hashlib +import argparse +import collections + +from .launcher_model import * + +def loadJson(filePath): + try: + with open(filePath) as json_file: + data = json.load(json_file, object_pairs_hook=collections.OrderedDict) + return data + except json.decoder.JSONDecodeError as error: + error_msg = "File \"" + filePath + "\" is empty. " \ + "Json cannot be loaded." + logging.error(error_msg) + except IOError as error: + error_msg = "Json cannot be loaded from file \"" + filePath + "\". " \ + "Wrong path." + logging.error(error_msg) + + sys.exit(-1) + +def addPassword(root, password): + + # Append password to json at the beginning + root["password"]=password + root.move_to_end('password', last=False) + return root + +def hashPassword(password): + m = hashlib.md5() + m.update(password.encode()) + return m.hexdigest() + +def saveFile(jsonWithPwd, filename): + with open(filename, 'w') as outfile: + json.dump(jsonWithPwd, outfile, indent=4) + +def main(): + """ Main logic """ + + # Parse input arguments + argsPars = argparse.ArgumentParser() + argsPars.add_argument('configuration', + help="menu/configuration file") + args = argsPars.parse_args() + + # Ask for password + password = input("Enter password: ") + + # Load json from file into json object + cfg_model = loadJson(args.configuration) + + # Add password to json structure + jsonWithPwd = addPassword(cfg_model, hashPassword(password)) + + # Save json back to the original file + saveFile(jsonWithPwd, args.configuration) + +# Start program here +if __name__ == '__main__': + main() \ No newline at end of file From 8de651de09d3916a71983b7bb6af1f32f47af7de Mon Sep 17 00:00:00 2001 From: novak_i Date: Tue, 25 Feb 2020 16:31:26 -0500 Subject: [PATCH 2/6] Add recursive option to password protection --- src/protect.py | 57 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 14 deletions(-) mode change 100644 => 100755 src/protect.py diff --git a/src/protect.py b/src/protect.py old mode 100644 new mode 100755 index 1b8ebbf..51e6225 --- a/src/protect.py +++ b/src/protect.py @@ -11,8 +11,7 @@ import logging import hashlib import argparse import collections - -from .launcher_model import * +import sys def loadJson(filePath): try: @@ -30,6 +29,30 @@ def loadJson(filePath): sys.exit(-1) +def processFile(filePath, password, recursive): + data = loadJson(filePath) + dirname = os.path.dirname(filePath) + protected = addPassword(data, hashPassword(password)) + saveFile(protected, filePath) + if recursive: + files = findAllFiles(data) + for file in files: + print(os.path.realpath(os.path.join(dirname, file))) + processFile(os.path.join(dirname, file), password, recursive) + +def findAllFiles(root): + fileList = [] + for key in root: + if key == 'file': + fileList.append(root[key]) + elif isinstance(root[key], list): + for element in root[key]: + if isinstance(element, dict): + fileList += findAllFiles(element) + elif isinstance(root[key], dict): + fileList += findAllFiles(root[key]) + return fileList + def addPassword(root, password): # Append password to json at the beginning @@ -50,22 +73,28 @@ def main(): """ Main logic """ # Parse input arguments - argsPars = argparse.ArgumentParser() - argsPars.add_argument('configuration', + argsParse = argparse.ArgumentParser(description='Example: pylauncher-protect -r menus/menu.json -p *****') + argsParse.add_argument('configuration', help="menu/configuration file") - args = argsPars.parse_args() - - # Ask for password - password = input("Enter password: ") - - # Load json from file into json object - cfg_model = loadJson(args.configuration) + argsParse.add_argument('--password', '-p', + help="password to be added to json file, if not provided user is prompted to enter it") + argsParse.add_argument('--recursive', '-r', + help="add recursively to all files referenced in json", action='store_true') + args = argsParse.parse_args() # Add password to json structure - jsonWithPwd = addPassword(cfg_model, hashPassword(password)) + if args.password == None: + # Ask for password + password = input("Enter password: ") + else: + password = args.password - # Save json back to the original file - saveFile(jsonWithPwd, args.configuration) + # Get current dir and create path to json file + cwd = os.getcwd() + jsonFilePath = os.path.join(cwd, args.configuration) + + # Add password to file and, if recursive, to all the files within + processFile(jsonFilePath, password, args.recursive) # Start program here if __name__ == '__main__': From 4cfe71bee3d26432cd7b7919e4c5f4ef2dc63585 Mon Sep 17 00:00:00 2001 From: novak_i Date: Sun, 1 Mar 2020 15:04:47 -0500 Subject: [PATCH 3/6] Update readme file --- Readme.md | 44 ++++++++++++++- examples/menus2/menu_5.json | 107 ++++++++++++++++++++++++++++++------ src/protect.py | 1 - 3 files changed, 130 insertions(+), 22 deletions(-) diff --git a/Readme.md b/Readme.md index 48ae598..6f88658 100644 --- a/Readme.md +++ b/Readme.md @@ -40,8 +40,39 @@ optional arguments: _Note:_ `--position` - 0 0 is on the top left, -1 -1 is on the lower right. +Password can be added to JSON configuration file(s) as follows: +```bash +pylauncher-protect +``` +Password can be added in the same call using the option + +* `-p (--password) ` + +If not, the user will be prompted to enter it. +Password can be added to all the configuration file referenced within configuration file and any files referenced in the referenced file and so on using the option + +* `-r (--recursive) ` + +For all available options and detailed help run + +```bash +~$ pylauncher -h +usage: pylauncher-protect [-h] [--password PASSWORD] [--recursive] configuration +Example: pylauncher-protect -r menus/menu.json -p ***** + +positional arguments: + configuration menu/configuration file + +optional arguments: + -h, --help show this help message and exit + --password PASSWORD, -p PASSWORD + password to be added to json file, if not provided + user is prompted to enter it + --recursive, -r add recursively to all files referenced in json + + ## Configuration -Launcher menus are defined via JSON configuration file(s). On top level, the configuration of the menu is divided in the following 3 sections: +Launcher menus are defined via JSON configuration file(s). On top level, the configuration of the menu is divided in the following 4 sections: * __menu-title__ - An optional section to set the menu title. If no title is specified a file name is used instead. @@ -78,8 +109,13 @@ Launcher menus are defined via JSON configuration file(s). On top level, the con } ] ``` +* __password__ - Hash of a password guarding a section of menu and all submenus. Main section to define launcher items. The type of each item is defined with the `type` property. All supported types with available parameters are described in the next section. -An detailed example can be found at [examples/menus/menu_example.json](examples/menus/menu_example.json). +```json +"password": "5f4dcc3b5aa765d61d8327deb882cf99" +``` + +A detailed example can be found at [examples/menus/menu_example.json](examples/menus/menu_example.json). ### Menu Items @@ -91,7 +127,7 @@ For any menu item the two optional parameters __help_link__ and __tip__ can be s #### Styles -The appearance of a menu item can be customized via styles and themes. Therefore each menu item has following 2 optional paramters: +The appearance of a menu item can be customized via styles and themes. Therefore each menu item has following 2 optional parameters: * __style__ - Enables very flexible customization with [QSS](http://doc.qt.io/qt-4.8/stylesheet-syntax.html) syntax * __theme__ - Enables customization using one of the predefined themes. How to define a theme is described in section [Stylesheet](#stylesheet). However we strongly discourage the use of theme on a per menu item basis. @@ -295,6 +331,7 @@ LauncherDetachButton{ } ``` + # Installation ## Anaconda Anaconda comes with all required packages for __pylauncher__. To install the package use @@ -343,3 +380,4 @@ _Note:_ To be able to build the Anaconda package you need to have the `patchelf` conda create -n build_environment python patchelf source activate build_environment ``` + diff --git a/examples/menus2/menu_5.json b/examples/menus2/menu_5.json index 9cd1110..cf1517a 100644 --- a/examples/menus2/menu_5.json +++ b/examples/menus2/menu_5.json @@ -1,24 +1,95 @@ { - "menu-title": - {"text": "F_L1","theme": "green", "style": "color: #FF3388"}, + "password": "5f4dcc3b5aa765d61d8327deb882cf99", + "menu-title": { + "text": "F_L1", + "theme": "green", + "style": "color: #FF3388" + }, "file-choice": [ - {"text": "Load F_L2", "file": "../menus/menu_2.json"} + { + "text": "Load F_L2", + "file": "../menus/menu_2.json" + } ], "menu": [ - {"type": "title", "text": "General"}, - {"type": "cmd", "text": "Who is online", "command": "xeyes", "tip": "List of currently connected users.", "help-link": "http://www.google.com"}, - {"type": "title", "text": "HighLevel Applications", "theme": "green"}, - {"type": "cmd", "text": "ScreenMonitorTool", "command": "ls", "theme": "green", "style": "color: #FF3388"}, - {"type": "cmd", "text": "QE Measurement", "command": "ls -l", "theme": "green"}, - {"type": "separator"}, - {"type": "title", "text": "Utilities", "theme": "green"}, - {"type": "cmd", "text": "Remote network access control by control room", "command": "ls"}, - {"type": "menu", "text": "Strip-tool", "file": "menu_10.json"}, - {"type": "caqtdm", "text": "CA-screen", "macros": "SYS=TEST", "panel": "screen.ui" }, - {"type": "medm", "text": "MEDM-screen", "macros": "SYS=TEST", "panel": "screen.adl" }, - {"type": "cmd", "text": "Test", "command": "pep -mc S10BC02-MBND100:I-SET"}, - {"type": "cmd", "text": "Test 2", "command": "pylauncher -h"}, - {"type": "pep", "text": "Pep type test (.prc)", "panel": "S_TEST_REMOTECTRL_GUDE_SF-CTRL-TEST01.prc"}, - {"type": "pep", "text": "Pep type test (.prc)", "param": "-mc S10BC02-MBND100:I-SET"} + { + "type": "title", + "text": "General" + }, + { + "type": "cmd", + "text": "Who is online", + "command": "xeyes", + "tip": "List of currently connected users.", + "help-link": "http://www.google.com" + }, + { + "type": "title", + "text": "HighLevel Applications", + "theme": "green" + }, + { + "type": "cmd", + "text": "ScreenMonitorTool", + "command": "ls", + "theme": "green", + "style": "color: #FF3388" + }, + { + "type": "cmd", + "text": "QE Measurement", + "command": "ls -l", + "theme": "green" + }, + { + "type": "separator" + }, + { + "type": "title", + "text": "Utilities", + "theme": "green" + }, + { + "type": "cmd", + "text": "Remote network access control by control room", + "command": "ls" + }, + { + "type": "menu", + "text": "Strip-tool", + "file": "menu_10.json" + }, + { + "type": "caqtdm", + "text": "CA-screen", + "macros": "SYS=TEST", + "panel": "screen.ui" + }, + { + "type": "medm", + "text": "MEDM-screen", + "macros": "SYS=TEST", + "panel": "screen.adl" + }, + { + "type": "cmd", + "text": "Test", + "command": "pep -mc S10BC02-MBND100:I-SET" + }, + { + "type": "cmd", + "text": "Test 2", + "command": "pylauncher -h" + }, + { + "type": "pep", + "text": "Pep type test (.prc)", + "panel": "S_TEST_REMOTECTRL_GUDE_SF-CTRL-TEST01.prc" + }, + { + "type": "pep", + "text": "Pep type test (.prc)", + "param": "-mc S10BC02-MBND100:I-SET" + } ] } \ No newline at end of file diff --git a/src/protect.py b/src/protect.py index 51e6225..ba15f9a 100755 --- a/src/protect.py +++ b/src/protect.py @@ -37,7 +37,6 @@ def processFile(filePath, password, recursive): if recursive: files = findAllFiles(data) for file in files: - print(os.path.realpath(os.path.join(dirname, file))) processFile(os.path.join(dirname, file), password, recursive) def findAllFiles(root): From 9cbf00883080841de1f41afe882b893d822639fa Mon Sep 17 00:00:00 2001 From: novak_i Date: Sun, 1 Mar 2020 15:06:53 -0500 Subject: [PATCH 4/6] Minor update readme file --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 6f88658..4760120 100644 --- a/Readme.md +++ b/Readme.md @@ -56,7 +56,7 @@ Password can be added to all the configuration file referenced within configurat For all available options and detailed help run ```bash -~$ pylauncher -h +~$ pylauncher-protect -h usage: pylauncher-protect [-h] [--password PASSWORD] [--recursive] configuration Example: pylauncher-protect -r menus/menu.json -p ***** From 52a57e811a23e92d7f90721ac1ab7c2a5ab98755 Mon Sep 17 00:00:00 2001 From: novak_i Date: Sun, 1 Mar 2020 15:08:02 -0500 Subject: [PATCH 5/6] Minor update readme file --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 4760120..25a4a05 100644 --- a/Readme.md +++ b/Readme.md @@ -69,7 +69,7 @@ optional arguments: password to be added to json file, if not provided user is prompted to enter it --recursive, -r add recursively to all files referenced in json - +``` ## Configuration Launcher menus are defined via JSON configuration file(s). On top level, the configuration of the menu is divided in the following 4 sections: From d16acd6b05ab214675221d87acf59ed362e77e0f Mon Sep 17 00:00:00 2001 From: novak_i Date: Sun, 1 Mar 2020 15:08:44 -0500 Subject: [PATCH 6/6] Minor update readme file --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 25a4a05..1cccc81 100644 --- a/Readme.md +++ b/Readme.md @@ -51,7 +51,7 @@ Password can be added in the same call using the option If not, the user will be prompted to enter it. Password can be added to all the configuration file referenced within configuration file and any files referenced in the referenced file and so on using the option -* `-r (--recursive) ` +* `-r (--recursive)` For all available options and detailed help run