Files
pyqcrm/lib/ConfigLoader.py

433 lines
16 KiB
Python

"""! @brief Defines the configuration class."""
##
# @file ConfigLoader.py
#
# @brief Defines the ConfigLoader class.
#
# @section description_configloader Description
# Defines the base class for the program configuration.
# - ConfigLoader (base class)
#
# @section libraries_configloader Libraries/Modules
# - <a href="https://docs.python.org/3/library/os.html">os</a> standard library
# - Access to system specific information.
# - <a href="https://docs.python.org/3/library/urllib.html">urllib</a> Python package
# - Collects several modules for working with URLs.
# - <a href="https://pypi.org/project/toml">toml</a> Python library
# - A Python library for parsing and creating TOML.
# - <a href="https://pypi.org/project/platformdirs/">platformdirs</a> Python package
# - Determining appropriate platform-specific dirs.
# - <a href="https://docs.python.org/3/library/pathlib.html">pathlib</a> Python module
# - Offers classes representing filesystem paths with semantics appropriate for different operating systems.
# - <a href="https://docs.python.org/3/library/shutil.html">shutil</a> Python module
# - Offers a number of high-level operations on files and collections of files.
# - <a href="https://docs.python.org/3/library/base64.html">base64</a> Python standard library
# - Provides functions for encoding binary data to printable ASCII characters and decoding such encodings back to binary data.
# - <a href="https://doc.qt.io/qtforpython-6/PySide6/QtCore/QObject.html">QObject</a> PySide6 class
# - The base class of all Qt objects.
# - <a href="https://doc.qt.io/qtforpython-6/PySide6/QtCore/Slot.html">Slot</a> PySide6 function
# - A function that is called in response to a particular signal.
# - <a href="https://doc.qt.io/qtforpython-6/PySide6/QtCore/Signal.html">Signal</a> PySide6 class
# - Provides a way to declare and connect Qt signals in a pythonic way.
# - <a href="https://pycryptodome.readthedocs.io/en/latest/src/api.html">Crypto</a> Python package
# - Provides cryptographic functionalities.
# - DbManager local class
# - Provides a singleton database connection for the entire program.
# - UserManager local class
# - Provides a model to handle users.
# - PyqcrmFlags local ENUM
# - Provides ENUMS to facilitate working in the program.
# - Vermasseln local class
# - Provides encryption functionality for the program.
#
# @section notes_configloader Notes
# - Needs a database connection.
#
# @section todo_configloader TODO
# - None.
#
# @section author_configloader Author(s)
# - Created by Linuxero on 03/14/2025.
# - Modified by Linuxero on 03/14/2025.
#
# Copyright (c) 2025 Schnaxero. All rights reserved.
import toml
from platformdirs import user_config_dir
from pathlib import Path
from PySide6.QtCore import QObject, Slot, Signal
from .Vermasseln import Vermasseln
import shutil
from urllib.parse import urlparse
from .DB.DbManager import DbManager
import os
from Crypto.Random import get_random_bytes
from base64 import b64encode
from .DB.UserManager import UserManager
from .PyqcrmFlags import PyqcrmFlags
class ConfigLoader(QObject):
"""! The ConfigLoader class.
Defines the class utilized by all different parts of the program.
Handles the local configuration of the whole program.
"""
__config = None
__version = "0.1-alpha"
__check_enc_key = True
dbConnectionError = Signal(str, bool)
adminUserError = Signal(str, bool)
adminNotAvailable = Signal()
configurationReady = Signal()
backupEncryptionKey = Signal()
invalidEncryptionKey = Signal()
def __init__(self):
"""! The ConfigLoader class initializer.
"""
super().__init__()
# print(f"In {__file__} file, __init__()")
self.config_dir = user_config_dir() + '/pyqcrm'
config_dir = Path(self.config_dir)
if config_dir.exists():
self.__configLoad()
if self.__config:
self.checkEncryptionKey()
else:
config_dir.mkdir(0o750, True, True)
@Slot(dict, result = bool)
def setConfig(self, app_config):
"""! Prepares the configuration of the program.
@param app_config The configuration as a dictionary.
@return True on success, False on failure.
"""
# print(f"In {__file__} file, setConfig()")
if not self.__config:
base_conf = self.__initializeConfig()
conf = self.__checkDbConnection(app_config)
app_config = toml.dumps(app_config)
if conf:
app_config = base_conf + app_config
self.__config = toml.loads(app_config)
self.__saveConfig()
conf = self.__checkAdminUser()
if conf:
self.configurationReady.emit()
def __initializeConfig(self):
"""! Creates the initial configuration of the program.
"""
# print(f"In {__file__} file, __initializeConfig()")
self.__encrypt_key = b64encode(get_random_bytes(32)).decode("utf-8")
conf = f"[pyqcrm]\nVERSION = \"{self.__version}\"\n"
conf = conf + f"ENCRYPTION_KEY_VALID = \"No\"\n"
conf = conf + f"ENCRYPTION_KEY = \"{self.__encrypt_key}\"\n\n"
return conf
def __checkDbConnection(self, db_config):
"""! Tests for a valid database connection.
@param db_config The configuration of the database connection as a dictionary.
@return True on success, False on failure.
"""
# print(f"In {__file__} file, __checkDbConnection()")
con = DbManager(db_config['database']).getConnection()
if con:
self.dbConnectionError.emit("Connection OK", True)
return True
else:
self.dbConnectionError.emit("Connection fehlgeschlagen", False)
return False
def __saveConfig(self):
"""! Saves the configuration of the program.
"""
# print(f"In {__file__} file, saveConfig()")
try:
with open (self.config_dir + '/pyqcrm.toml', 'w') as f:
# print(self.__config)
config = Vermasseln().oscarVermasseln(toml.dumps(self.__config))
f.write(config)
except FileNotFoundError:
print("Konnte die Konfiguration nicht speichern.")
def __checkAdminUser(self):
"""! Checks for a valid admin account of the program.
@return True on success, False on failure.
"""
# print(f"In {__file__} file, __checkAdminUser()")
result = UserManager().checkAdmin()
if not result:
#if not result[0][0] == 1:
self.adminUserError.emit("Kein Admin vorhanden", False)
return False
else:
self.adminUserError.emit("Admin vorhanden", True)
return True
@Slot(dict, result= bool)
def addAdminUser(self, user_config):
"""! Adds an admin account.
@param user_config The credentials of the admin account as a dictionary.
@return True on success, False on failure.
"""
# print(f"In {__file__} file, addAdminUser()")
admin = UserManager(user_config["user"], PyqcrmFlags.ADMIN).createUser()
if not admin:
#self.adminNotAvailable.emit()
self.adminUserError.emit("Benutzername nich verfügbar", False)
else:
self.__config['pyqcrm']['ENCRYPTION_KEY_VALID'] = 'Yes'
self.__saveConfig()
self.backupEncryptionKey.emit()
return admin
@Slot(str, str)
def __saveData(self, recovery_file, recovery_password, data):
"""! Generic function to save backups to a file.
This function emits a configurationReady signal.
@param recovery_file The full path of the backup file.
@param recovery_password password to secure the backup file.
@param data The content of the backup file.
"""
# print(f"In {__file__} file, __saveData()")
local = False
rp = self.__setRecoveryPassword(recovery_password)
rf = rp[1] + data + rp[0]
rf = Vermasseln().oscarVermasseln(rf, local)
rec_file = urlparse(recovery_file)
if os.name == "nt":
rec_file = rec_file [1:]
else:
rec_file = rec_file.path + ".pyqrec"
try:
with open(rec_file, "w") as f:
f.write(rf)
self.configurationReady.emit()
except Exception as e:
print(str(e))
@Slot(str, str)
def getRecoveryKey(self, recovery_file, recovery_password):
"""! Loads the encryption key from a backup.
This function emits a configurationReady signal.
@param recovery_file The full path of the backup file.
@param recovery_password password to secure the backup file.
"""
rec_file = urlparse(recovery_file)
rec_file = rec_file.path
if os.name == "nt":
rec_file = rec_file [1:]
try:
ek = self.__parseImport(rec_file, recovery_password)
if ek:
self.__setEncryptionKey(ek)
self.configurationReady.emit()
else:
self.__invalidateEncryptionKey()
self.invalidEncryptionKey.emit()
except Exception as e:
print(str(e))
def __parseImport(self, rec_file, recovery_password):
"""! Loads the content from a backup.
@param rec_file The full path of the backup file.
@param recovery_password password used to secure the backup file.
@return The content on success, None on failure.
"""
local = False
with open(rec_file, "r") as f:
rf = f.read()
rf = Vermasseln().entschluesseln(rf, local)
ek = rf[128:]
ek = ek[:-32]
password = rf[:128]
salt = rf[-32:]
ok = self.__checkRecoveryPassword(recovery_password, password, salt)
if ok:
return ek
else:
return None
def __invalidateEncryptionKey(self):
"""! Flag the encryption key as invalid.
"""
# print(f"In {__file__} file, __invalidateEncryptionKey()")
self.__config['pyqcrm']['ENCRYPTION_KEY_VALID'] = 'No'
self.__saveConfig()
@Slot()
def checkEncryptionKey(self):
"""! Checks the validity of the encryption key.
This function emits an invalidEncryptionKey signal.
"""
# print(f"In {__file__} file, __checkEncryptionKey()")
if self.__config['pyqcrm']['ENCRYPTION_KEY_VALID'] == 'No':
self.invalidEncryptionKey.emit()
def __checkRecoveryPassword(self, recovery_password, password, salt):
"""! Generic function to save backups to a file.
This function emits a configurationReady signal.
@param recovery_password The password from the backup file.
@param password The password used when creating the backup file.
@param salt A salt to hash the password.
@return A password.
"""
# print(f"In {__file__} file, __checkRecoveryPassword()")
rp = self.__setRecoveryPassword(recovery_password, salt)
return rp[1] == password
@Slot(str, str) # todo: non local encryption
def importConfig(self, confile, password):
"""! Generic function to import configuration from a backup.
This function emits a invalidEncryptionKey signal.
@param conffile The path of the backup file.
@param password The password used when creating the backup file.
"""
confile = urlparse(confile)
confile = confile.path
if os.name == "nt":
confile = confile[1:]
try:
ek = self.__parseImport(confile, password)
if ek:
self.__config = toml.loads(ek)
self.__saveConfig()
self.__configLoad()
else:
self.invalidEncryptionKey.emit()
except Exception as e:
print(str(e))
def __configLoad(self):
"""! Loads the program configuration.
This function emits a configurationReady signal.
"""
# print(f"In {__file__} file, __configLoad()")
try:
with open (self.config_dir + '/pyqcrm.toml', 'r') as f:
config = f.read()
self.__config = toml.loads(Vermasseln().entschluesseln(config))
self.configurationReady.emit()
except FileNotFoundError:
print("Konnte die Konfiguration nicht laden.")
except TypeError:
print(f"Invalid Configuration: {__file__}")
except Exception as e:
print(str(e))
def getConfig(self):
"""! Returns the program configuration.
@return configuration as a toml file.
"""
# print(f"In {__file__} file, getConfig()")
# print(self.__config)
return self.__config
def __setRecoveryPassword(self, key, salt = None):
# print(f"In {__file__} file, __setRecoveryPassword()")
key = Vermasseln.userPasswordHash(key, salt)
return key.split("$")
@Slot(str)
def __setEncryptionKey(self, enc_key):
# print(f"In {__file__} file, __setEncryptionKey()")
self.__config['pyqcrm']['ENCRYPTION_KEY_VALID'] = 'Yes'
self.__config['pyqcrm']['ENCRYPTION_KEY'] = enc_key
self.__saveConfig()
@Slot(str, str)
def backupConfig(self, filename, password):
"""! Saves the program configuration.
@param filename the path of the backup file.
@param password the password to secure the backup.
"""
conf_file = toml.dumps(self.getConfig())
self.__saveData(filename, password, conf_file)
@Slot(dict)
def saveDbConf(self, db = None):
"""! Saves/Upates the database configuration.
@param db Database configuration as a dictionary.
"""
self.__config.update(db)
self.__saveConfig()
@Slot(result = dict)
def getDbConf(self):
"""! Loads the database configuration.
@return Database configuration as a dictionary on success, None on failure.
"""
try:
return self.__config['database']
except KeyError as ex:
print(f"Missing database configuration: {ex}")
return None
@Slot(dict)
def saveCompanyInfo(self, company = None):
"""! Saves/Upates the company information.
@param company Company configuration as a dictionary.
"""
self.__config.update(company)
self.__saveConfig()
@Slot(result = dict)
def getCompanyInfo(self):
"""! Loads the company information.
@return Company information as a dictionary on success, None on failure.
"""
try:
return self.__config['company']
except KeyError as ex:
print(f"Missing company info: {ex}")
return None
@Slot(dict)
def saveMiscConf(self, misc_conf = None):
"""! Saves/Upates the miscellaneous configuration.
@param misc_conf Extra configuration as a dictionary.
"""
self.__config.update(misc_conf)
self.__saveConfig()
@Slot(result = bool)
def systray(self):
"""! Loads the system tray configuration.
@return boolean if system tray is set to be used, False can also be returned on failure.
"""
try:
return self.__config['misc']['SYSTRAY']
except KeyError as ex:
print(f"Missing configuration: {ex}")
return False
@Slot(str, str)
def backupEncryptkey(self, filename, password):
"""! Saves/Upates the encryption key.
@param filename Path to save the key.
@param password Password to secure the backup.
"""
encrypt_key = self.__config['pyqcrm']['ENCRYPTION_KEY']
self.__saveData(filename, password, encrypt_key)