mirror of
https://github.com/naruxde/revpicommander.git
synced 2025-11-08 16:43:53 +01:00
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:
1
.idea/revpicommander.iml
generated
1
.idea/revpicommander.iml
generated
@@ -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" />
|
||||
|
||||
@@ -9,5 +9,4 @@ include README.md
|
||||
include requirements.txt
|
||||
include setup.iss
|
||||
include setup.py
|
||||
include stdeb.cfg
|
||||
include translate.pro
|
||||
|
||||
@@ -5,3 +5,4 @@ zeroconf>=0.24.4
|
||||
setuptools>=65.6.3
|
||||
wheel
|
||||
paramiko>=2.12.0
|
||||
keyring>=23.13.1
|
||||
1
setup.py
1
setup.py
@@ -15,6 +15,7 @@ setup(
|
||||
include_package_data=True,
|
||||
|
||||
install_requires=[
|
||||
"keyring",
|
||||
"PyQt5",
|
||||
"revpimodio2",
|
||||
"zeroconf"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Binary file not shown.
@@ -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 '{0}'.</source>
|
||||
<translation>Kann Simulator nicht starten! Vielleicht ist die piCtory Datei defekt oder es gibt keine Schreibberechtigung für '{0}`.</translation>
|
||||
<translation>Kann Simulator nicht starten! Vielleicht ist die piCtory Datei defekt oder es gibt keine Schreibberechtigung für '{0}'.</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'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>
|
||||
|
||||
BIN
src/revpicommander/locale/revpicommander_en.qm
Normal file
BIN
src/revpicommander/locale/revpicommander_en.qm
Normal file
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
@@ -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 \
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user