Merge pull request #13 from novakivana/pwd-protection

Pwd protection
This commit is contained in:
Simon Gregor Ebner
2020-03-10 11:12:02 +01:00
committed by GitHub
6 changed files with 290 additions and 30 deletions
+41 -3
View File
@@ -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 <configuration>
```
Password can be added in the same call using the option
* `-p (--password) <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-protect -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
```
+1
View File
@@ -24,6 +24,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
+89 -18
View File
@@ -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"
}
]
}
+51 -5
View File
@@ -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."
+8 -4
View File
@@ -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)
Executable
+100
View File
@@ -0,0 +1,100 @@
#!/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
import sys
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 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:
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
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
argsParse = argparse.ArgumentParser(description='Example: pylauncher-protect -r menus/menu.json -p *****')
argsParse.add_argument('configuration',
help="menu/configuration file")
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
if args.password == None:
# Ask for password
password = input("Enter password: ")
else:
password = args.password
# 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__':
main()