Save SSH credentials with keyring module

The keyring module will save the username and password of the SSH
connection to the operating system password storage.
This commit is contained in:
2023-01-10 01:12:45 +01:00
parent 2039f6cfe2
commit 7a7741e60b
13 changed files with 165 additions and 69 deletions

View File

@@ -4,6 +4,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9 (revpicommander)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -9,5 +9,4 @@ include README.md
include requirements.txt
include setup.iss
include setup.py
include stdeb.cfg
include translate.pro

View File

@@ -4,4 +4,5 @@ revpimodio2>=2.5.6
zeroconf>=0.24.4
setuptools>=65.6.3
wheel
paramiko>=2.12.0
paramiko>=2.12.0
keyring>=23.13.1

View File

@@ -15,6 +15,7 @@ setup(
include_package_data=True,
install_requires=[
"keyring",
"PyQt5",
"revpimodio2",
"zeroconf"

View File

@@ -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()

View File

@@ -101,62 +101,62 @@ Nicht gespeicherte Änderunen gehen verloren</translation>
<context>
<name>ConnectionManager</name>
<message>
<location filename="../helper.py" line="504"/>
<location filename="../helper.py" line="511"/>
<source>SIMULATING</source>
<translation>SIMULATION</translation>
</message>
<message>
<location filename="../helper.py" line="507"/>
<location filename="../helper.py" line="514"/>
<source>NOT CONNECTED</source>
<translation>NICHT VERBUNDEN</translation>
</message>
<message>
<location filename="../helper.py" line="524"/>
<location filename="../helper.py" line="531"/>
<source>SERVER ERROR</source>
<translation>SERVER FEHLER</translation>
</message>
<message>
<location filename="../helper.py" line="549"/>
<location filename="../helper.py" line="556"/>
<source>RUNNING</source>
<translation>LÄUFT</translation>
</message>
<message>
<location filename="../helper.py" line="551"/>
<location filename="../helper.py" line="558"/>
<source>PLC FILE NOT FOUND</source>
<translation>SPS PROGRAMM NICHT GEFUNDEN</translation>
</message>
<message>
<location filename="../helper.py" line="553"/>
<location filename="../helper.py" line="560"/>
<source>NOT RUNNING (NO STATUS)</source>
<translation>LÄUFT NICHT (KEIN STATUS)</translation>
</message>
<message>
<location filename="../helper.py" line="555"/>
<location filename="../helper.py" line="562"/>
<source>PROGRAM KILLED</source>
<translation>PROGRAMM GETÖTET</translation>
</message>
<message>
<location filename="../helper.py" line="557"/>
<location filename="../helper.py" line="564"/>
<source>PROGRAM TERMED</source>
<translation>PROGRAMM BEENDET</translation>
</message>
<message>
<location filename="../helper.py" line="559"/>
<location filename="../helper.py" line="566"/>
<source>NOT RUNNING</source>
<translation>LÄUFT NICHT</translation>
</message>
<message>
<location filename="../helper.py" line="561"/>
<location filename="../helper.py" line="568"/>
<source>FINISHED WITH CODE {0}</source>
<translation>BEENDET MIT CODE {0}</translation>
</message>
<message>
<location filename="../helper.py" line="376"/>
<location filename="../helper.py" line="383"/>
<source>Error</source>
<translation>Fehler</translation>
</message>
<message>
<location filename="../helper.py" line="344"/>
<location filename="../helper.py" line="351"/>
<source>The combination of username and password was rejected from the SSH server.
Try again.</source>
@@ -165,7 +165,7 @@ Try again.</source>
Bitte erneut versuchen.</translation>
</message>
<message>
<location filename="../helper.py" line="353"/>
<location filename="../helper.py" line="360"/>
<source>Could not establish a SSH connection to server:
{0}</source>
@@ -174,7 +174,7 @@ Bitte erneut versuchen.</translation>
{0}</translation>
</message>
<message>
<location filename="../helper.py" line="376"/>
<location filename="../helper.py" line="383"/>
<source>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.</translation>
<message>
<location filename="../revpicommander.py" line="231"/>
<source>Can not start the simulator! Maybe the piCtory file is corrupt or you have no write permissions for &apos;{0}&apos;.</source>
<translation>Kann Simulator nicht starten! Vielleicht ist die piCtory Datei defekt oder es gibt keine Schreibberechtigung für &apos;{0}`.</translation>
<translation>Kann Simulator nicht starten! Vielleicht ist die piCtory Datei defekt oder es gibt keine Schreibberechtigung für &apos;{0}&apos;.</translation>
</message>
<message>
<location filename="../revpicommander.py" line="398"/>
@@ -952,6 +952,27 @@ Dies ist kein Fehler, wenn das SPS Startprogramm bereits auf dem Rev Pi ist. Pr
{0}.</translation>
</message>
</context>
<context>
<name>SSHAuth</name>
<message>
<location filename="../sshauth.py" line="49"/>
<source>Could not save password</source>
<translation>Konnte Kennwort nicht speichern</translation>
</message>
<message>
<location filename="../sshauth.py" line="49"/>
<source>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.</source>
<translation>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.</translation>
</message>
</context>
<context>
<name>Simulator</name>
<message>
@@ -1672,6 +1693,16 @@ applicable law.
<source>SSH password:</source>
<translation>SSH Passwort:</translation>
</message>
<message>
<location filename="../../../ui_dev/sshauth.ui" line="53"/>
<source>Username and password will be saved in secured operating systems&apos;s password storage.</source>
<translation>Benutzername und Kennwort werden im Passwortspeicher vom Betriebssystem gesichert.</translation>
</message>
<message>
<location filename="../../../ui_dev/sshauth.ui" line="56"/>
<source>Save username and password</source>
<translation>Benutzername und Kennwort merken</translation>
</message>
</context>
<context>
<name>wid_debugcontrol</name>

Binary file not shown.

View File

@@ -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)

View File

@@ -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__":

View File

@@ -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

View File

@@ -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 \

View File

@@ -9,8 +9,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>275</width>
<height>170</height>
<width>363</width>
<height>163</height>
</rect>
</property>
<property name="windowTitle">
@@ -48,10 +48,17 @@
</widget>
</item>
<item>
<widget class="QWidget" name="wid_keys" native="true"/>
<widget class="QCheckBox" name="cbx_save_password">
<property name="toolTip">
<string>Username and password will be saved in secured operating systems's password storage.</string>
</property>
<property name="text">
<string>Save username and password</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<widget class="QDialogButtonBox" name="btn_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@@ -65,7 +72,7 @@
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<sender>btn_box</sender>
<signal>accepted()</signal>
<receiver>diag_sshauth</receiver>
<slot>accept()</slot>
@@ -81,7 +88,7 @@
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<sender>btn_box</sender>
<signal>rejected()</signal>
<receiver>diag_sshauth</receiver>
<slot>reject()</slot>