diff --git a/.idea/revpicommander.iml b/.idea/revpicommander.iml index bcd0f63..0d0b203 100644 --- a/.idea/revpicommander.iml +++ b/.idea/revpicommander.iml @@ -4,6 +4,7 @@ + diff --git a/MANIFEST.in b/MANIFEST.in index 979d6ef..53cd4a6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,5 +9,4 @@ include README.md include requirements.txt include setup.iss include setup.py -include stdeb.cfg include translate.pro diff --git a/requirements.txt b/requirements.txt index 5b11c16..571a1be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ revpimodio2>=2.5.6 zeroconf>=0.24.4 setuptools>=65.6.3 wheel -paramiko>=2.12.0 \ No newline at end of file +paramiko>=2.12.0 +keyring>=23.13.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 1292d52..d4263f7 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ setup( include_package_data=True, install_requires=[ + "keyring", "PyQt5", "revpimodio2", "zeroconf" diff --git a/src/revpicommander/helper.py b/src/revpicommander/helper.py index ce7a02e..2808edc 100644 --- a/src/revpicommander/helper.py +++ b/src/revpicommander/helper.py @@ -20,7 +20,7 @@ from paramiko.ssh_exception import AuthenticationException from . import proginit as pi from .ssh_tunneling.server import SSHLocalTunnel -from .sshauth import SSHAuth, SSHAuthType +from .sshauth import SSHAuth settings = QtCore.QSettings("revpimodio.org", "RevPiCommander") """Global application settings.""" @@ -304,11 +304,11 @@ class ConnectionManager(QtCore.QThread): self.xml_funcs.clear() self.xml_mode = -1 - def pyload_connect(self, settings: RevPiSettings, parent=None) -> bool: + def pyload_connect(self, revpi_settings: RevPiSettings, parent=None) -> bool: """ Create a new connection from settings object. - :param settings: Revolution Pi saved connection settings + :param revpi_settings: Revolution Pi saved connection settings :param parent: Qt parent window for dialog positioning :return: True, if the connection was successfully established """ @@ -322,10 +322,16 @@ class ConnectionManager(QtCore.QThread): socket.setdefaulttimeout(2) - if settings.ssh_use_tunnel: + if revpi_settings.ssh_use_tunnel: while True: - diag_ssh_auth = SSHAuth(SSHAuthType.PASS, parent) - diag_ssh_auth.username = settings.ssh_user + diag_ssh_auth = SSHAuth( + revpi_settings.ssh_user, + "{0}.{1}_{2}".format( + settings.applicationName(), + settings.organizationName(), + revpi_settings.internal_id), + parent, + ) if not diag_ssh_auth.exec() == QtWidgets.QDialog.Accepted: self._clear_settings() return False @@ -333,14 +339,15 @@ class ConnectionManager(QtCore.QThread): ssh_user = diag_ssh_auth.username ssh_pass = diag_ssh_auth.password ssh_tunnel_server = SSHLocalTunnel( - settings.port, - settings.address, - settings.ssh_port + revpi_settings.port, + revpi_settings.address, + revpi_settings.ssh_port ) try: ssh_tunnel_port = ssh_tunnel_server.connect_by_credentials(ssh_user, ssh_pass) break except AuthenticationException: + diag_ssh_auth.remove_saved_password() QtWidgets.QMessageBox.critical( parent, self.tr("Error"), self.tr( "The combination of username and password was rejected from the SSH server.\n\n" @@ -360,7 +367,7 @@ class ConnectionManager(QtCore.QThread): sp = ServerProxy("http://127.0.0.1:{0}".format(ssh_tunnel_port)) else: - sp = ServerProxy("http://{0}:{1}".format(settings.address, settings.port)) + sp = ServerProxy("http://{0}:{1}".format(revpi_settings.address, revpi_settings.port)) # Load values and test connection to Revolution Pi try: @@ -371,7 +378,7 @@ class ConnectionManager(QtCore.QThread): pi.logger.exception(e) self.connection_error_observed.emit(str(e)) - if not settings.ssh_use_tunnel: + if not revpi_settings.ssh_use_tunnel: # todo: Change message, that user can use ssh QtWidgets.QMessageBox.critical( parent, self.tr("Error"), self.tr( @@ -386,19 +393,19 @@ class ConnectionManager(QtCore.QThread): return False - self.settings = settings + self.settings = revpi_settings self.ssh_pass = ssh_pass self.pyload_version = pyload_version self.xml_funcs = xml_funcs self.xml_mode = xml_mode with self._lck_cli: - socket.setdefaulttimeout(settings.timeout) + socket.setdefaulttimeout(revpi_settings.timeout) self.ssh_tunnel_server = ssh_tunnel_server self._cli = sp self._cli_connect.put_nowait(( - "127.0.0.1" if settings.ssh_use_tunnel else settings.address, - ssh_tunnel_port if settings.ssh_use_tunnel else settings.port + "127.0.0.1" if revpi_settings.ssh_use_tunnel else revpi_settings.address, + ssh_tunnel_port if revpi_settings.ssh_use_tunnel else revpi_settings.port )) self.connection_established.emit() diff --git a/src/revpicommander/locale/revpicommander_de.qm b/src/revpicommander/locale/revpicommander_de.qm index 77e78b0..a8afc7e 100644 Binary files a/src/revpicommander/locale/revpicommander_de.qm and b/src/revpicommander/locale/revpicommander_de.qm differ diff --git a/src/revpicommander/locale/revpicommander_de.ts b/src/revpicommander/locale/revpicommander_de.ts index f26bef3..a154800 100644 --- a/src/revpicommander/locale/revpicommander_de.ts +++ b/src/revpicommander/locale/revpicommander_de.ts @@ -101,62 +101,62 @@ Nicht gespeicherte Änderunen gehen verloren ConnectionManager - + SIMULATING SIMULATION - + NOT CONNECTED NICHT VERBUNDEN - + SERVER ERROR SERVER FEHLER - + RUNNING LÄUFT - + PLC FILE NOT FOUND SPS PROGRAMM NICHT GEFUNDEN - + NOT RUNNING (NO STATUS) LÄUFT NICHT (KEIN STATUS) - + PROGRAM KILLED PROGRAMM GETÖTET - + PROGRAM TERMED PROGRAMM BEENDET - + NOT RUNNING LÄUFT NICHT - + FINISHED WITH CODE {0} BEENDET MIT CODE {0} - + Error Fehler - + The combination of username and password was rejected from the SSH server. Try again. @@ -165,7 +165,7 @@ Try again. Bitte erneut versuchen. - + Could not establish a SSH connection to server: {0} @@ -174,7 +174,7 @@ Bitte erneut versuchen. {0} - + Can not connect to RevPi XML-RPC Service! This could have the following reasons: The RevPi is not online, the XML-RPC service is not running / bind to localhost or the ACL permission is not set for your IP!!! @@ -401,7 +401,7 @@ Dies kann aus der Textbox oben kopiert werden. Can not start the simulator! Maybe the piCtory file is corrupt or you have no write permissions for '{0}'. - Kann Simulator nicht starten! Vielleicht ist die piCtory Datei defekt oder es gibt keine Schreibberechtigung für '{0}`. + Kann Simulator nicht starten! Vielleicht ist die piCtory Datei defekt oder es gibt keine Schreibberechtigung für '{0}'. @@ -952,6 +952,27 @@ Dies ist kein Fehler, wenn das SPS Startprogramm bereits auf dem Rev Pi ist. Pr {0}. + + SSHAuth + + + Could not save password + Konnte Kennwort nicht speichern + + + + Could not save password to operating systems password save. + +Maybe your operating system does not support saving passwords. This could be due to missing libraries or programs. + +This is not an error of RevPi Commander. + Konnte das Kennwort nicht im Kennwortspeicher des Betriebssystems speichern. + +Vielleicht untersützt das Betriebssystem keine Kennwortspeicherung. Dies könnte an fehlenden Bibliotheken oder Programmen liegen. + +Dies ist kein Fehler von RevPi Commander. + + Simulator @@ -1672,6 +1693,16 @@ applicable law. SSH password: SSH Passwort: + + + Username and password will be saved in secured operating systems's password storage. + Benutzername und Kennwort werden im Passwortspeicher vom Betriebssystem gesichert. + + + + Save username and password + Benutzername und Kennwort merken + wid_debugcontrol diff --git a/src/revpicommander/locale/revpicommander_en.qm b/src/revpicommander/locale/revpicommander_en.qm new file mode 100644 index 0000000..937ea3e Binary files /dev/null and b/src/revpicommander/locale/revpicommander_en.qm differ diff --git a/src/revpicommander/sshauth.py b/src/revpicommander/sshauth.py index fd0f531..23baaa0 100644 --- a/src/revpicommander/sshauth.py +++ b/src/revpicommander/sshauth.py @@ -4,36 +4,88 @@ __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "GPLv3" -from enum import Enum +from logging import getLogger +import keyring from PyQt5 import QtWidgets +from keyring.errors import KeyringError -from revpicommander.ui.sshauth_ui import Ui_diag_sshauth +from .ui.sshauth_ui import Ui_diag_sshauth - -class SSHAuthType(Enum): - PASS = "pass" - KEYS = "keys" +log = getLogger() class SSHAuth(QtWidgets.QDialog, Ui_diag_sshauth): - """Version information window.""" - def __init__(self, auth_type: SSHAuthType, parent=None): + def __init__(self, user_name="", service_name: str = None, parent=None): + """ + Ask the user for username and password or use saved entries. + + If you want to use the operating system's password storage, you have + to set a 'service_name'. The value must be unique for your application + or for each user, if the username is the same. + + :param user_name: Preset username, also used to check password save + :param service_name: Identity to save passwords in os's password save + :param parent: Qt parent for this dialog + """ + log.debug("SSHAuth.__init__") + super(SSHAuth, self).__init__(parent) self.setupUi(self) - self.wid_password.setVisible(auth_type is SSHAuthType.PASS) - self.wid_keys.setVisible(auth_type is SSHAuthType.KEYS) + self._service_name = service_name + self.cbx_save_password.setVisible(bool(service_name)) + self.txt_username.setText(user_name) + + def accept(self) -> None: + log.debug("SSHAuth.accept") + + if self._service_name and self.cbx_save_password.isChecked(): + try: + keyring.set_password(self._service_name, self.username, self.password) + except KeyringError as e: + log.error(e) + QtWidgets.QMessageBox.warning( + self, self.tr("Could not save password"), self.tr( + "Could not save password to operating systems password save.\n\n" + "Maybe your operating system does not support saving passwords. " + "This could be due to missing libraries or programs.\n\n" + "This is not an error of RevPi Commander." + ) + ) + super().accept() + + def exec(self) -> int: + log.debug("SSHAuth.exec") + + if self._service_name: + try: + saved_password = keyring.get_password(self._service_name, self.username) + if saved_password: + self.txt_password.setText(saved_password) + return QtWidgets.QDialog.Accepted + except KeyringError as e: + log.error(e) + + return super().exec() + + def remove_saved_password(self) -> None: + """Remove saved password.""" + log.debug("SSHAuth.remove_saved_password") + + if self._service_name: + try: + keyring.delete_password(self._service_name, self.username) + except KeyringError as e: + log.error(e) @property def password(self) -> str: + """Get the saved or entered password.""" return self.txt_password.text() @property def username(self) -> str: + """Get the entered username.""" return self.txt_username.text() - - @username.setter - def username(self, value: str): - self.txt_username.setText(value) diff --git a/src/revpicommander/ui/sshauth_ui.py b/src/revpicommander/ui/sshauth_ui.py index 4df10c6..5e1f17f 100644 --- a/src/revpicommander/ui/sshauth_ui.py +++ b/src/revpicommander/ui/sshauth_ui.py @@ -15,7 +15,7 @@ class Ui_diag_sshauth(object): def setupUi(self, diag_sshauth): diag_sshauth.setObjectName("diag_sshauth") diag_sshauth.setWindowModality(QtCore.Qt.ApplicationModal) - diag_sshauth.resize(275, 170) + diag_sshauth.resize(363, 163) self.verticalLayout = QtWidgets.QVBoxLayout(diag_sshauth) self.verticalLayout.setObjectName("verticalLayout") self.wid_password = QtWidgets.QWidget(diag_sshauth) @@ -36,18 +36,18 @@ class Ui_diag_sshauth(object): self.txt_username.setObjectName("txt_username") self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.txt_username) self.verticalLayout.addWidget(self.wid_password) - self.wid_keys = QtWidgets.QWidget(diag_sshauth) - self.wid_keys.setObjectName("wid_keys") - self.verticalLayout.addWidget(self.wid_keys) - self.buttonBox = QtWidgets.QDialogButtonBox(diag_sshauth) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.verticalLayout.addWidget(self.buttonBox) + self.cbx_save_password = QtWidgets.QCheckBox(diag_sshauth) + self.cbx_save_password.setObjectName("cbx_save_password") + self.verticalLayout.addWidget(self.cbx_save_password) + self.btn_box = QtWidgets.QDialogButtonBox(diag_sshauth) + self.btn_box.setOrientation(QtCore.Qt.Horizontal) + self.btn_box.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.btn_box.setObjectName("btn_box") + self.verticalLayout.addWidget(self.btn_box) self.retranslateUi(diag_sshauth) - self.buttonBox.accepted.connect(diag_sshauth.accept) # type: ignore - self.buttonBox.rejected.connect(diag_sshauth.reject) # type: ignore + self.btn_box.accepted.connect(diag_sshauth.accept) # type: ignore + self.btn_box.rejected.connect(diag_sshauth.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(diag_sshauth) def retranslateUi(self, diag_sshauth): @@ -55,6 +55,8 @@ class Ui_diag_sshauth(object): diag_sshauth.setWindowTitle(_translate("diag_sshauth", "SSH authentication")) self.lbl_username.setText(_translate("diag_sshauth", "SSH username:")) self.lbl_password.setText(_translate("diag_sshauth", "SSH password:")) + self.cbx_save_password.setToolTip(_translate("diag_sshauth", "Username and password will be saved in secured operating systems\'s password storage.")) + self.cbx_save_password.setText(_translate("diag_sshauth", "Save username and password")) if __name__ == "__main__": diff --git a/stdeb.cfg b/stdeb.cfg deleted file mode 100644 index abae509..0000000 --- a/stdeb.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[DEFAULT] -Debian-Version=1 -Depends3=python3-pyqt5, python3-revpimodio2 (>= 2.5.0), python3-zeroconf (>= 0.24.4) -Section=universe/x11 -Suite=stable -X-Python3-Version: >=3.4 diff --git a/translate.pro b/translate.pro index 8f935a0..2f7a861 100644 --- a/translate.pro +++ b/translate.pro @@ -11,6 +11,7 @@ SOURCES = src/revpicommander/aclmanager.py \ src/revpicommander/revpiplclist.py \ src/revpicommander/revpiprogram.py \ src/revpicommander/simulator.py \ + src/revpicommander/sshauth.py \ src/revpicommander/revpicommander.py FORMS = ui_dev/aclmanager.ui \ diff --git a/ui_dev/sshauth.ui b/ui_dev/sshauth.ui index e690765..3b821e0 100644 --- a/ui_dev/sshauth.ui +++ b/ui_dev/sshauth.ui @@ -9,8 +9,8 @@ 0 0 - 275 - 170 + 363 + 163 @@ -48,10 +48,17 @@ - + + + Username and password will be saved in secured operating systems's password storage. + + + Save username and password + + - + Qt::Horizontal @@ -65,7 +72,7 @@ - buttonBox + btn_box accepted() diag_sshauth accept() @@ -81,7 +88,7 @@ - buttonBox + btn_box rejected() diag_sshauth reject()