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