1 # -*- coding: utf-8 -*- |
|
2 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> |
|
3 # |
|
4 |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to handle the various WebAuth requests. |
|
8 """ |
|
9 |
|
10 from PyQt6.QtCore import Qt, pyqtSlot |
|
11 from PyQt6.QtWebEngineCore import QWebEngineWebAuthUxRequest |
|
12 from PyQt6.QtWidgets import ( |
|
13 QButtonGroup, |
|
14 QDialog, |
|
15 QDialogButtonBox, |
|
16 QLineEdit, |
|
17 QRadioButton, |
|
18 QSizePolicy, |
|
19 QVBoxLayout, |
|
20 ) |
|
21 |
|
22 from eric7.EricGui import EricPixmapCache |
|
23 |
|
24 from .Ui_WebBrowserWebAuthDialog import Ui_WebBrowserWebAuthDialog |
|
25 |
|
26 |
|
27 class WebBrowserWebAuthDialog(QDialog, Ui_WebBrowserWebAuthDialog): |
|
28 """ |
|
29 Class implementing a dialog to handle the various WebAuth requests. |
|
30 """ |
|
31 |
|
32 def __init__(self, uxRequest, parent=None): |
|
33 """ |
|
34 Constructor |
|
35 |
|
36 @param uxRequest reference to the WebAuth request object |
|
37 @type QWebEngineWebAuthUxRequest |
|
38 @param parent reference to the parent widget (defaults to None) |
|
39 @type QWidget (optional) |
|
40 """ |
|
41 super().__init__(parent) |
|
42 self.setupUi(self) |
|
43 |
|
44 self.__uxRequest = uxRequest |
|
45 |
|
46 self.pinButton.setIcon(EricPixmapCache.getIcon("showPassword")) |
|
47 |
|
48 self.selectAccountButtonGroup = QButtonGroup(self) |
|
49 self.selectAccountButtonGroup.setExclusive(True) |
|
50 |
|
51 self.selectAccountLayout = QVBoxLayout(self.selectAccountWidget) |
|
52 self.selectAccountLayout.setAlignment(Qt.AlignmentFlag.AlignTop) |
|
53 |
|
54 self.buttonBox.accepted.connect(self.__acceptRequest) |
|
55 self.buttonBox.rejected.connect(self.__cancelRequest) |
|
56 self.buttonBox.button(QDialogButtonBox.StandardButton.Retry).clicked.connect( |
|
57 self.__retry |
|
58 ) |
|
59 |
|
60 self.updateDialog() |
|
61 |
|
62 self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) |
|
63 |
|
64 @pyqtSlot(str) |
|
65 def on_pinEdit_textEdited(self, pin): |
|
66 """ |
|
67 Private slot handling entering a PIN. |
|
68 |
|
69 @param pin entered PIN |
|
70 @type str |
|
71 """ |
|
72 self.confirmPinErrorLabel.setVisible( |
|
73 self.confirmPinEdit.isVisible() and pin != self.confirmPinEdit.text() |
|
74 ) |
|
75 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( |
|
76 (self.confirmPinEdit.isVisible() and pin == self.confirmPinEdit.text()) |
|
77 or not self.confirmPinEdit.isVisible() |
|
78 ) |
|
79 |
|
80 @pyqtSlot(bool) |
|
81 def on_pinButton_toggled(self, checked): |
|
82 """ |
|
83 Private slot to handle the toggling of the PIN visibility. |
|
84 |
|
85 @param checked state of the PIN visibility button |
|
86 @type bool |
|
87 """ |
|
88 pinRequestInfo = self.__uxRequest.pinRequest() |
|
89 |
|
90 if checked: |
|
91 self.pinButton.setIcon(EricPixmapCache.getIcon("hidePassword")) |
|
92 self.pinEdit.setEchoMode(QLineEdit.EchoMode.Normal) |
|
93 else: |
|
94 self.pinButton.setIcon(EricPixmapCache.getIcon("showPassword")) |
|
95 self.pinEdit.setEchoMode(QLineEdit.EchoMode.Password) |
|
96 |
|
97 if pinRequestInfo.reason != QWebEngineWebAuthUxRequest.PinEntryReason.Challenge: |
|
98 self.confirmPinLabel.setVisible(not checked) |
|
99 self.confirmPinEdit.setVisible(not checked) |
|
100 self.on_pinEdit_textEdited(self.pinEdit.text()) |
|
101 |
|
102 @pyqtSlot(str) |
|
103 def on_confirmPinEdit_textEdited(self, pin): |
|
104 """ |
|
105 Private slot handling entering of a confirmation PIN. |
|
106 |
|
107 @param pin entered confirmation PIN |
|
108 @type str |
|
109 """ |
|
110 self.confirmPinErrorLabel.setVisible(pin != self.pinEdit.text()) |
|
111 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled( |
|
112 pin == self.confirmPinEdit.text() |
|
113 ) |
|
114 |
|
115 @pyqtSlot() |
|
116 def __acceptRequest(self): |
|
117 """ |
|
118 Private slot to accept the WebAuth request. |
|
119 """ |
|
120 requestState = self.__uxRequest.state() |
|
121 if requestState == QWebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: |
|
122 checkedButton = self.selectAccountButtonGroup.checkedButton() |
|
123 if checkedButton: |
|
124 self.__uxRequest.setSelectedAccount(checkedButton.text()) |
|
125 elif requestState == QWebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: |
|
126 self.__uxRequest.setPin(self.pinEdit.text()) |
|
127 |
|
128 @pyqtSlot() |
|
129 def __cancelRequest(self): |
|
130 """ |
|
131 Private slot to cancel the WebAuth request. |
|
132 """ |
|
133 self.__uxRequest.cancel() |
|
134 |
|
135 @pyqtSlot() |
|
136 def __retry(self): |
|
137 """ |
|
138 Private slot to retry the WebAuth request. |
|
139 """ |
|
140 self.__uxRequest.retry() |
|
141 |
|
142 @pyqtSlot() |
|
143 def updateDialog(self): |
|
144 """ |
|
145 Public slot to update the dialog depending on the current WebAuth request state. |
|
146 """ |
|
147 requestState = self.__uxRequest.state() |
|
148 if requestState == QWebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: |
|
149 self.__setupSelectAccountUi() |
|
150 elif requestState == QWebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: |
|
151 self.__setupCollectPinUi() |
|
152 elif ( |
|
153 requestState |
|
154 == QWebEngineWebAuthUxRequest.WebAuthUxState.FinishTokenCollection |
|
155 ): |
|
156 self.__setupFinishCollectTokenUi() |
|
157 elif requestState == QWebEngineWebAuthUxRequest.WebAuthUxState.RequestFailed: |
|
158 self.__setupErrorUi() |
|
159 |
|
160 self.adjustSize() |
|
161 |
|
162 def __setupSelectAccountUi(self): |
|
163 """ |
|
164 Private method to configure the 'Select Account' UI. |
|
165 """ |
|
166 self.__clearSelectAccountButtons() |
|
167 |
|
168 self.headerLabel.setText(self.tr("<b>Choose Passkey</b>")) |
|
169 self.descriptionLabel.setText( |
|
170 self.tr("Which passkey do you want to use for {0}?").format( |
|
171 self.__uxRequest.relyingPartyId() |
|
172 ) |
|
173 ) |
|
174 self.pinGroupBox.setVisible(False) |
|
175 |
|
176 self.selectAccountArea.setVisible(True) |
|
177 self.selectAccountWidget.resize(self.width(), self.height()) |
|
178 userNames = self.__uxRequest.userNames() |
|
179 for name in sorted(userNames): |
|
180 button = QRadioButton(name) |
|
181 self.selectAccountLayout.addWidget(button) |
|
182 self.selectAccountButtonGroup.addButton(button) |
|
183 if len(userNames) == 1: |
|
184 # nothing to select from, select the one and only button |
|
185 self.selectAccountButtonGroup.buttons()[0].setChecked(True) |
|
186 |
|
187 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(self.tr("Ok")) |
|
188 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setVisible(True) |
|
189 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setVisible(True) |
|
190 self.buttonBox.button(QDialogButtonBox.StandardButton.Retry).setVisible(False) |
|
191 |
|
192 def __setupCollectPinUi(self): |
|
193 """ |
|
194 Private method to configure the 'Collect PIN' UI. |
|
195 """ |
|
196 self.__clearSelectAccountButtons() |
|
197 |
|
198 self.selectAccountArea.setVisible(False) |
|
199 |
|
200 self.pinGroupBox.setVisible(True) |
|
201 self.confirmPinLabel.setVisible(False) |
|
202 self.confirmPinEdit.setVisible(False) |
|
203 self.confirmPinErrorLabel.setVisible(False) |
|
204 |
|
205 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText( |
|
206 self.tr("Next") |
|
207 ) |
|
208 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setVisible(True) |
|
209 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setVisible(True) |
|
210 self.buttonBox.button(QDialogButtonBox.StandardButton.Retry).setVisible(False) |
|
211 |
|
212 pinRequestInfo = self.__uxRequest.pinRequest() |
|
213 if pinRequestInfo.reason == QWebEngineWebAuthUxRequest.PinEntryReason.Challenge: |
|
214 self.headerLabel.setText(self.tr("<b>PIN Required</b>")) |
|
215 self.descriptionLabel.setText( |
|
216 self.tr("Enter the PIN for your security key.") |
|
217 ) |
|
218 else: |
|
219 if pinRequestInfo.reason == QWebEngineWebAuthUxRequest.PinEntryReason.Set: |
|
220 self.headerLabel.setText(self.tr("<b>New PIN Required</b>")) |
|
221 self.descriptionLabel.setText( |
|
222 self.tr("Set new PIN for your security key.") |
|
223 ) |
|
224 else: |
|
225 self.headerLabel.setText(self.tr("<b>PIN Change Required</b>")) |
|
226 self.descriptionLabel.setText( |
|
227 self.tr("Change the PIN for your security key.") |
|
228 ) |
|
229 self.confirmPinLabel.setVisible(True) |
|
230 self.confirmPinEdit.setVisible(True) |
|
231 |
|
232 errorDetails = "" |
|
233 if ( |
|
234 pinRequestInfo.error |
|
235 == QWebEngineWebAuthUxRequest.PinEntryError.InternalUvLocked |
|
236 ): |
|
237 errorDetails = self.tr("Internal User Verification Locked!") |
|
238 elif pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.WrongPin: |
|
239 errorDetails = self.tr("Wrong PIN!") |
|
240 elif pinRequestInfo.error == QWebEngineWebAuthUxRequest.PinEntryError.TooShort: |
|
241 errorDetails = self.tr("PIN Too Short!") |
|
242 elif ( |
|
243 pinRequestInfo.error |
|
244 == QWebEngineWebAuthUxRequest.PinEntryError.InvalidCharacters |
|
245 ): |
|
246 errorDetails = self.tr("PIN Contains Invalid Characters!") |
|
247 elif ( |
|
248 pinRequestInfo.error |
|
249 == QWebEngineWebAuthUxRequest.PinEntryError.SameAsCurrentPin |
|
250 ): |
|
251 errorDetails = self.tr("New PIN is same as current PIN!") |
|
252 if errorDetails: |
|
253 errorDetails = self.tr( |
|
254 "{0} %n attempt(s) remaining.", "", pinRequestInfo.remainingAttempts |
|
255 ).format(errorDetails) |
|
256 self.pinErrorLabel.setText(errorDetails) |
|
257 |
|
258 def __setupFinishCollectTokenUi(self): |
|
259 """ |
|
260 Private method to configure the 'Finish Collect Token' UI. |
|
261 """ |
|
262 self.__clearSelectAccountButtons() |
|
263 |
|
264 self.headerLabel.setText( |
|
265 self.tr("<b>Use your security key with {0}</b>").format( |
|
266 self.__uxRequest.relyingPartyId() |
|
267 ) |
|
268 ) |
|
269 self.descriptionLabel.setText( |
|
270 self.tr("Touch your security key to complete the request.") |
|
271 ) |
|
272 self.pinGroupBox.setVisible(False) |
|
273 |
|
274 self.selectAccountArea.setVisible(False) |
|
275 |
|
276 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setVisible(False) |
|
277 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setVisible(True) |
|
278 self.buttonBox.button(QDialogButtonBox.StandardButton.Retry).setVisible(False) |
|
279 |
|
280 def __setupErrorUi(self): |
|
281 """ |
|
282 Private method to configure the 'Error' UI. |
|
283 """ |
|
284 self.__clearSelectAccountButtons() |
|
285 |
|
286 errorMsg = "" |
|
287 retryVisible = False |
|
288 |
|
289 requestFailureReason = self.__uxRequest.requestFailureReason() |
|
290 if ( |
|
291 requestFailureReason |
|
292 == QWebEngineWebAuthUxRequest.RequestFailureReason.Timeout |
|
293 ): |
|
294 errorMsg = self.tr("Request Timeout") |
|
295 elif ( |
|
296 requestFailureReason |
|
297 == QWebEngineWebAuthUxRequest.RequestFailureReason.KeyNotRegistered |
|
298 ): |
|
299 errorMsg = self.tr("Security key is not registered.") |
|
300 elif ( |
|
301 requestFailureReason |
|
302 == QWebEngineWebAuthUxRequest.RequestFailureReason.KeyAlreadyRegistered |
|
303 ): |
|
304 errorMsg = self.tr( |
|
305 "You already registered this security key. Try again with another" |
|
306 " security key." |
|
307 ) |
|
308 retryVisible = True |
|
309 elif ( |
|
310 requestFailureReason |
|
311 == QWebEngineWebAuthUxRequest.RequestFailureReason.SoftPinBlock |
|
312 ): |
|
313 errorMsg = self.tr( |
|
314 "The security key is locked because the wrong PIN was entered too" |
|
315 " many times. To unlock it, remove and reinsert it." |
|
316 ) |
|
317 retryVisible = True |
|
318 elif ( |
|
319 requestFailureReason |
|
320 == QWebEngineWebAuthUxRequest.RequestFailureReason.HardPinBlock |
|
321 ): |
|
322 errorMsg = self.tr( |
|
323 "The security key is locked because the wrong PIN was entered too" |
|
324 " many times. You will need to reset the security key." |
|
325 ) |
|
326 elif ( |
|
327 requestFailureReason |
|
328 == QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorRemovedDuringPinEntry # noqa: E501 |
|
329 ): |
|
330 errorMsg = self.tr( |
|
331 "Security key removed during verification. Please reinsert and try" |
|
332 " again." |
|
333 ) |
|
334 elif ( |
|
335 requestFailureReason |
|
336 == QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingResidentKeys # noqa: E501 |
|
337 ): |
|
338 errorMsg = self.tr("Security key does not have resident key support.") |
|
339 elif ( |
|
340 requestFailureReason |
|
341 == QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingUserVerification # noqa: E501 |
|
342 ): |
|
343 errorMsg = self.tr("Security key is missing user verification.") |
|
344 elif ( |
|
345 requestFailureReason |
|
346 == QWebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingLargeBlob # noqa: E501 |
|
347 ): |
|
348 errorMsg = self.tr("Security key is missing Large Blob support.") |
|
349 elif ( |
|
350 requestFailureReason |
|
351 == QWebEngineWebAuthUxRequest.RequestFailureReason.NoCommonAlgorithms |
|
352 ): |
|
353 errorMsg = self.tr("Security key does not provide a common algorithm.") |
|
354 elif ( |
|
355 requestFailureReason |
|
356 == QWebEngineWebAuthUxRequest.RequestFailureReason.StorageFull |
|
357 ): |
|
358 errorMsg = self.tr("No storage space left on the security key.") |
|
359 elif ( |
|
360 requestFailureReason |
|
361 == QWebEngineWebAuthUxRequest.RequestFailureReason.UserConsentDenied |
|
362 ): |
|
363 errorMsg = self.tr("User consent denied.") |
|
364 elif ( |
|
365 requestFailureReason |
|
366 == QWebEngineWebAuthUxRequest.RequestFailureReason.WinUserCancelled |
|
367 ): |
|
368 errorMsg = self.tr("User canceled the WebAuth request.") |
|
369 |
|
370 self.headerLabel.setText(self.tr("<b>Something went wrong</b>")) |
|
371 self.descriptionLabel.setText(errorMsg) |
|
372 self.descriptionLabel.adjustSize() |
|
373 |
|
374 self.pinGroupBox.setVisible(False) |
|
375 self.selectAccountArea.setVisible(False) |
|
376 |
|
377 self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setVisible(False) |
|
378 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setVisible(True) |
|
379 self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText( |
|
380 self.tr("Close") |
|
381 ) |
|
382 self.buttonBox.button(QDialogButtonBox.StandardButton.Retry).setVisible( |
|
383 retryVisible |
|
384 ) |
|
385 if retryVisible: |
|
386 self.buttonBox.button(QDialogButtonBox.StandardButton.Retry).setFocus() |
|
387 |
|
388 def __clearSelectAccountButtons(self): |
|
389 """ |
|
390 Private method to remove the account selection buttons. |
|
391 """ |
|
392 for button in self.selectAccountButtonGroup.buttons(): |
|
393 self.selectAccountLayout.removeWidget(button) |
|
394 self.selectAccountButtonGroup.removeButton(button) |
|