--- a/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py Sat Jul 20 11:14:51 2024 +0200 +++ b/src/eric7/WebBrowser/WebAuth/Fido2ManagementDialog.py Mon Jul 22 10:15:41 2024 +0200 @@ -7,7 +7,13 @@ """ from PyQt6.QtCore import Qt, QTimer, pyqtSlot -from PyQt6.QtWidgets import QDialog, QTreeWidgetItem +from PyQt6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QMenu, + QToolButton, + QTreeWidgetItem, +) from eric7.EricGui import EricPixmapCache from eric7.EricGui.EricOverrideCursor import EricOverrideCursor @@ -31,10 +37,13 @@ DisplayNameColumn = 2 UserNameColumn = 3 - def __init__(self, parent=None): + def __init__(self, standalone=False, parent=None): """ Constructor + @param standalone flag indicating the standalone management application + (defaults to False) + @type bool (optional) @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ @@ -44,14 +53,56 @@ self.reloadButton.setIcon(EricPixmapCache.getIcon("reload")) self.lockButton.setIcon(EricPixmapCache.getIcon("locked")) + self.menuButton.setObjectName("fido2_supermenu_button") + self.menuButton.setIcon(EricPixmapCache.getIcon("superMenu")) + self.menuButton.setToolTip(self.tr("Security Key Management Menu")) + self.menuButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + self.menuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) + self.menuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.menuButton.setAutoRaise(True) + self.menuButton.setShowMenuInside(True) + + self.__initManagementMenu() + + if standalone: + self.buttonBox.button(QDialogButtonBox.StandardButton.Close).setText( + self.tr("Quit") + ) + self.reloadButton.clicked.connect(self.__populateDeviceSelector) self.__manager = Fido2Management(parent=self) - ##self.__manager.deviceConnected.connect(self.__deviceConnected) - ##self.__manager.deviceDisconnected.connect(self.__deviceDisconnected) QTimer.singleShot(0, self.__populateDeviceSelector) + def __initManagementMenu(self): + """ + Private method to initialize the security key management menu with + actions not needed so much. + """ + self.__mgmtMenu = QMenu() + self.__mgmtMenu.addAction(self.tr("Show Info"), self.__showSecurityKeyInfo) + self.__mgmtMenu.addSeparator() + self.__mgmtMenu.addAction( + self.tr("Reset Security Key"), self.__resetSecurityKey + ) + # TODO: potentially add these 'config' actions + # - Force PIN Change + # - Set Minimum PIN Length + # - Toggle 'Always Require UV' + + self.__mgmtMenu.aboutToShow.connect(self.__aboutToShowManagementMenu) + + self.menuButton.setMenu(self.__mgmtMenu) + + @pyqtSlot() + def __aboutToShowManagementMenu(self): + """ + Private slot to prepare the security key management menu before it is shown. + """ + # TODO: not implemented yet + pass + ############################################################################ ## methods related to device handling ############################################################################ @@ -104,8 +155,7 @@ self.lockButton.setEnabled(securityKey is not None) self.pinButton.setEnabled(securityKey is not None) - self.showInfoButton.setEnabled(securityKey is not None) - self.resetButton.setEnabled(securityKey is not None) + self.menuButton.setEnabled(securityKey is not None) self.loadPasskeysButton.setEnabled(securityKey is not None) if securityKey is not None: @@ -166,12 +216,61 @@ self.__manager.lockDevice() @pyqtSlot() - def on_showInfoButton_clicked(self): + def __showSecurityKeyInfo(self): + """ + Private slot to show some info about the selected security key. + """ + from .Fido2InfoDialog import Fido2InfoDialog + + securityKey = self.securityKeysComboBox.currentData() + dlg = Fido2InfoDialog( + header=securityKey.product_name, manager=self.__manager, parent=self + ) + dlg.exec() + + @pyqtSlot() + def __resetSecurityKey(self): + """ + Private slot to reset the selected security key. """ - Slot documentation goes here. - """ - # TODO: not implemented yet - pass + title = self.tr("Reset Security Key") + + yes = EricMessageBox.yesNo( + parent=self, + title=title, + text=self.tr( + "<p>Shall the selected security key really be reset?</p><p><b>WARNING" + ":</b> This will delete all passkeys and restore factory settings.</p>" + ), + ) + if yes: + if len(self.__manager.getDevices()) != 1: + EricMessageBox.critical( + self, + title=title, + text=self.tr( + "Only one security key can be connected to perform a reset." + " Remove all other security keys and try again." + ), + ) + return + + EricMessageBox.information( + self, + title=title, + text=self.tr( + "Confirm this dialog then remove and re-insert the security key." + " Confirm the reset by touching it." + ), + ) + + ok, msg = self.__manager.resetDevice() + if ok: + EricMessageBox.information(self, title, msg) + else: + EricMessageBox.warning(self, title, msg) + + self.__populateDeviceSelector() ############################################################################ ## methods related to PIN handling @@ -241,13 +340,9 @@ dlg = Fido2PinDialog( mode=Fido2PinDialogMode.GET, title=title, - message=self.tr( - "Enter the PIN to unlock the security key (%n attempt(s)" - " remaining.", - "", - retries, - ), + message=self.tr("Enter the PIN to unlock the security key."), minLength=self.__manager.getMinimumPinLength(), + retries=retries, parent=self, ) if dlg.exec() == QDialog.DialogCode.Accepted: @@ -262,23 +357,60 @@ """ Private slot to set a PIN for the selected security key. """ - # TODO: not implemented yet - pass + retries = self.__manager.getPinRetries()[0] + title = self.tr("Set PIN") + + dlg = Fido2PinDialog( + mode=Fido2PinDialogMode.SET, + title=title, + message=self.tr("Enter the PIN for the security key."), + minLength=self.__manager.getMinimumPinLength(), + retries=retries, + parent=self, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + newPin = dlg.getPins()[1] + ok, msg = self.__manager.setPin(newPin) + if ok: + self.lockButton.setEnabled(True) + self.lockButton.setChecked(False) + self.pinButton.setText(self.tr("Change PIN")) + self.loadPasskeysButton.setEnabled(True) + self.__manager.reconnectToDevice() + EricMessageBox.information(self, title, msg) + else: + EricMessageBox.warning(self, title, msg) @pyqtSlot() def __changePin(self): """ - Private slot to set a PIN for the selected security key. + Private slot to change the PIN of the selected security key. """ - # TODO: not implemented yet - pass + retries = self.__manager.getPinRetries()[0] + title = self.tr("Change PIN") + + dlg = Fido2PinDialog( + mode=Fido2PinDialogMode.CHANGE, + title=title, + message=self.tr("Enter the current and new PINs."), + minLength=self.__manager.getMinimumPinLength(), + retries=retries, + parent=self, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + oldPin, newPin = dlg.getPins() + ok, msg = self.__manager.changePin(oldPin, newPin) + if ok: + self.lockButton.setChecked(False) + EricMessageBox.information(self, title, msg) + else: + EricMessageBox.warning(self, title, msg) @pyqtSlot() def on_pinButton_clicked(self): """ Private slot to set or change the PIN for the selected security key. """ - # TODO: not implemented yet if self.__manager.hasPin(): self.__changePin() else: @@ -345,14 +477,14 @@ @pyqtSlot() def on_loadPasskeysButton_clicked(self): """ - Slot documentation goes here. + Private slot to (re-)populate the passkeys list. """ self.__populatePasskeysList() @pyqtSlot() def on_passkeysList_itemSelectionChanged(self): """ - Slot documentation goes here. + Private slot handling the selection of a passkey. """ enableButtons = ( len(self.passkeysList.selectedItems()) == 1