async def test_tab_login_logout(gui_factory, core_config, alice, monkeypatch): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) gui = await gui_factory() # Fix the return value of ensure_string_size, because it can depend of the size of the window monkeypatch.setattr( "parsec.core.gui.main_window.ensure_string_size", lambda s, size, font: (s[:16] + "...") ) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") assert not gui.add_tab_button.isEnabled() first_created_tab = gui.test_get_tab() await gui.test_switch_to_logged_in(alice) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.add_tab_button.isEnabled() assert gui.test_get_tab() == first_created_tab await gui.test_logout() assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") assert not gui.add_tab_button.isEnabled() assert gui.test_get_tab() != first_created_tab
async def prepare_enrollment_request(self): try: self.context = await PkiEnrollmentSubmitterInitialCtx.new(self.addr ) self.widget_user_info.setVisible(True) self.label_cert_error.setVisible(False) self.line_edit_user_name.setText( self.context.x509_certificate.subject_common_name) self.line_edit_user_email.setText( self.context.x509_certificate.subject_email_address) self.line_edit_device.setText(desktop.get_default_device()) self.button_select_cert.setText( str(self.context.x509_certificate.certificate_id)) except PkiEnrollmentCertificateNotFoundError: # User did not provide a certificate (cancelled the prompt). We do nothing. pass except PkiEnrollmentCertificatePinCodeUnavailableError: # User did not provide a pin code (cancelled the prompt). We do nothing. pass except Exception as exc: show_error(self, translate("TEXT_ENROLLMENT_ERROR_LOADING_CERTIFICATE"), exception=exc) self.widget_user_info.setVisible(False) self.label_cert_error.setText( translate("TEXT_ENROLLMENT_ERROR_LOADING_CERTIFICATE")) self.label_cert_error.setVisible(True) self.button_ask_to_join.setEnabled(False)
async def _create_new_device(self, device_label, file_path, passphrase): try: recovery_device = await load_recovery_device(file_path, passphrase) new_device = await generate_new_device_from_recovery( recovery_device, device_label) return new_device except LocalDeviceError as exc: self.button_validate.setEnabled(True) if "Decryption failed" in str(exc): show_error(self, translate("TEXT_IMPORT_KEY_WRONG_PASSPHRASE"), exception=exc) else: show_error(self, translate("IMPORT_KEY_LOCAL_DEVICE_ERROR"), exception=exc) raise JobResultError("error") from exc except BackendNotAvailable as exc: show_error(self, translate("IMPORT_KEY_BACKEND_OFFLINE"), exception=exc) raise JobResultError("backend-error") from exc except BackendConnectionError as exc: show_error(self, translate("IMPORT_KEY_BACKEND_ERROR"), exception=exc) raise JobResultError("backend-error") from exc except Exception as exc: show_error(self, translate("IMPORT_KEY_ERROR"), exception=exc) raise JobResultError("error") from exc
async def test_tab_login_logout_two_tabs(aqtbot, gui_factory, core_config, alice, monkeypatch): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) gui = await gui_factory() # Fix the return value of ensure_string_size, because it can depend of the size of the window monkeypatch.setattr( "parsec.core.gui.main_window.ensure_string_size", lambda s, size, font: (s[:16] + "...") ) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") first_created_tab = gui.test_get_tab() await gui.test_switch_to_logged_in(alice) assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." logged_tab = gui.test_get_tab() aqtbot.mouse_click(gui.add_tab_button, QtCore.Qt.LeftButton) assert gui.tab_center.count() == 2 assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..." assert gui.tab_center.tabText(1) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") gui.switch_to_tab(0) def _logged_tab_displayed(): assert logged_tab == gui.test_get_tab() await aqtbot.wait_until(_logged_tab_displayed) await gui.test_logout() assert gui.tab_center.count() == 1 assert gui.tab_center.tabText(0) == translate("TEXT_TAB_TITLE_LOG_IN_SCREEN") assert gui.test_get_tab() != first_created_tab
async def _export_recovery_device(self, config_dir, device, export_path): try: recovery_device = await generate_recovery_device(device) file_name = get_recovery_device_file_name(recovery_device) file_path = export_path / file_name passphrase = await save_recovery_device(file_path, recovery_device) return recovery_device, file_path, passphrase except BackendNotAvailable as exc: show_error(self, translate("EXPORT_KEY_BACKEND_OFFLINE"), exception=exc) raise JobResultError("backend-error") from exc except BackendConnectionError as exc: show_error(self, translate("EXPORT_KEY_BACKEND_ERROR"), exception=exc) raise JobResultError("backend-error") from exc except LocalDeviceAlreadyExistsError as exc: show_error(self, translate("TEXT_RECOVERY_DEVICE_FILE_ALREADY_EXISTS"), exception=exc) raise JobResultError("already-exists") from exc except Exception as exc: show_error(self, translate("EXPORT_KEY_ERROR"), exception=exc) raise JobResultError("error") from exc self.button_validate.setEnabled(True)
def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.combo_auth_method.addItem(translate("TEXT_AUTH_METHOD_PASSWORD"), DeviceFileType.PASSWORD) if is_smartcard_extension_available(): self.combo_auth_method.addItem( translate("TEXT_AUTH_METHOD_SMARTCARD"), DeviceFileType.SMARTCARD) self.combo_auth_method.setCurrentIndex(0) if self.combo_auth_method.count() == 1: self.combo_auth_method.setEnabled(False) self.combo_auth_method.setToolTip( translate("TEXT_ONLY_ONE_AUTH_METHOD_AVAILABLE")) self.auth_widgets = { DeviceFileType.PASSWORD: PasswordAuthenticationWidget(), DeviceFileType.SMARTCARD: SmartCardAuthenticationWidget(), } self.current_auth_method = DeviceFileType.PASSWORD self.auth_widgets[ DeviceFileType.PASSWORD].authentication_state_changed.connect( self._on_password_state_changed) self.auth_widgets[ DeviceFileType.SMARTCARD].authentication_state_changed.connect( self._on_smartcard_state_changed) self.main_layout.addWidget(self.auth_widgets[self.current_auth_method]) self.combo_auth_method.currentIndexChanged.connect( self._on_auth_method_changed)
def __init__(self, pending): super().__init__() self.setupUi(self) self.pending = pending accept_pix = Pixmap(":/icons/images/material/done.svg") accept_pix.replace_color(QColor(0x00, 0x00, 0x00), QColor(0xFF, 0xFF, 0xFF)) reject_pix = Pixmap(":/icons/images/material/clear.svg") reject_pix.replace_color(QColor(0x00, 0x00, 0x00), QColor(0xFF, 0xFF, 0xFF)) self.button_accept.setIcon(QIcon(accept_pix)) self.button_reject.setIcon(QIcon(reject_pix)) self.label_date.setText(format_datetime(pending.submitted_on)) if isinstance(self.pending, PkiEnrollementAccepterInvalidSubmittedCtx): if self.pending.submitter_x509_certificate: self.widget_cert_infos.setVisible(True) self.widget_cert_error.setVisible(False) self.label_name.setText( self.pending.submitter_x509_certificate.subject_common_name ) self.label_email.setText( self.pending.submitter_x509_certificate. subject_email_address) self.label_issuer.setText( self.pending.submitter_x509_certificate.issuer_common_name) else: self.widget_cert_infos.setVisible(False) self.widget_cert_error.setVisible(True) self.label_error.setText( translate("TEXT_ENROLLMENT_ERROR_WITH_CERTIFICATE")) self.button_accept.setVisible(False) self.label_cert_validity.setStyleSheet( "#label_cert_validity { color: #F44336; }") self.label_cert_validity.setText( "✘ " + translate("TEXT_ENROLLMENT_CERTIFICATE_IS_INVALID")) self.label_cert_validity.setToolTip( textwrap.fill(str(self.pending.error), 80)) else: assert isinstance(self.pending, PkiEnrollementAccepterValidSubmittedCtx) self.widget_cert_infos.setVisible(True) self.widget_cert_error.setVisible(False) self.button_accept.setVisible(True) self.label_name.setText( self.pending.submitter_x509_certificate.subject_common_name) self.label_email.setText( self.pending.submitter_x509_certificate.subject_email_address) self.label_issuer.setText( self.pending.submitter_x509_certificate.issuer_common_name) self.label_cert_validity.setStyleSheet( "#label_cert_validity { color: #8BC34A; }") self.label_cert_validity.setText( "✔ " + translate("TEXT_ENROLLMENT_CERTIFICATE_IS_VALID")) self.button_accept.clicked.connect( lambda: self.accept_clicked.emit(self)) self.button_reject.clicked.connect( lambda: self.reject_clicked.emit(self))
def _overwrite_key(self, dest): if dest.exists(): rep = ask_question( parent=self, title=translate("ASK_OVERWRITE_KEY"), message=translate("TEXT_OVERWRITE_KEY"), button_texts=(translate("ACTION_OVERWRITE_KEY_YES"), translate("ACTION_IMPORT_NO")), ) return rep == translate("ACTION_OVERWRITE_KEY_YES") return True
def _on_import_key(self): key_file, _ = QFileDialog.getOpenFileName( parent=self, caption=translate("ACTION_IMPORT_KEY"), filter=translate("IMPORT_KEY_FILTERS"), initialFilter=translate("IMPORT_KEY_INITIAL_FILTER"), ) if not key_file: return new_device = load_device_file(Path(key_file)) if new_device is None: show_error(self, translate("TEXT_INVALID_DEVICE_KEY")) return rep = ask_question( parent=self, title=translate("ASK_IMPORT_KEY"), message=translate("TEXT_IMPORT_KEY_CONFIRM_organization-user-device").format( organization=new_device.organization_id, user=new_device.short_user_display, device=new_device.device_label, ), button_texts=(translate("ACTION_IMPORT_YES"), translate("ACTION_IMPORT_NO")), ) if rep == translate("ACTION_IMPORT_YES"): key_name = new_device.slughash + ".keys" dest = get_devices_dir(self.config.config_dir).joinpath(key_name) if self._overwrite_key(dest): shutil.copyfile( new_device.key_file_path, os.path.join(get_devices_dir(self.config.config_dir), key_name), ) self.reload_devices() self.key_imported.emit()
async def reject_recruit(self, enrollment_button): try: await enrollment_button.pending.reject() except Exception: SnackbarManager.warn(translate("TEXT_ENROLLMENT_REJECT_FAILURE")) enrollment_button.set_buttons_enabled(True) else: SnackbarManager.inform(translate("TEXT_ENROLLMENT_REJECT_SUCCESS")) self.main_layout.removeWidget(enrollment_button) enrollment_button.hide() enrollment_button.setParent(None)
def _on_import_key_clicked(self): key_file, _ = QDialogInProcess.getOpenFileName( self, translate("ACTION_IMPORT_KEY"), str(Path.home()), filter=translate("RECOVERY_KEY_FILTERS"), initialFilter=translate("RECOVERY_KEY_INITIAL_FILTER"), ) if key_file: self.label_key_file.setText(key_file) self._check_infos()
async def test_workspace_reencryption_do_one_batch_error( caplog, aqtbot, running_backend, logged_gui, autoclose_dialog, monkeypatch, reencryption_needed_workspace, error_type, ): expected_errors = { FSBackendOfflineError: translate("TEXT_WORKPACE_REENCRYPT_OFFLINE_ERROR"), FSError: translate("TEXT_WORKPACE_REENCRYPT_FS_ERROR"), FSWorkspaceNoAccess: translate("TEXT_WORKPACE_REENCRYPT_ACCESS_ERROR"), FSWorkspaceNotFoundError: translate("TEXT_WORKPACE_REENCRYPT_NOT_FOUND_ERROR"), Exception: translate("TEXT_WORKSPACE_REENCRYPT_UNKOWN_ERROR"), } w_w = await logged_gui.test_switch_to_workspaces_widget() await display_reencryption_button(aqtbot, monkeypatch, w_w) wk_button = w_w.layout_workspaces.itemAt(0).widget() async def mocked_start_reencryption(self, workspace_id): class Job: async def do_one_batch(self, *args, **kwargs): raise error_type("") return Job() w_w.core.user_fs.workspace_start_reencryption = mocked_start_reencryption.__get__( w_w.core.user_fs) await aqtbot.mouse_click(wk_button.button_reencrypt, QtCore.Qt.LeftButton) def _assert_error(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [("Error", expected_errors[error_type])] assert wk_button.button_reencrypt.isVisible() await aqtbot.wait_until(_assert_error) # Unexpected error is logged if error_type is Exception: caplog.assert_occured( "[exception] Uncatched error [parsec.core.gui.trio_thread]" )
async def cancelled_step_3_exchange_greeter_sas(self): expected_message = translate("TEXT_GREET_USER_WAIT_PEER_TRUST_ERROR") await self._cancel_invitation() await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None
async def cancelled_step_2_start_claimer(self): expected_message = translate("TEXT_GREET_USER_WAIT_PEER_ERROR") await self._cancel_invitation() await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None
async def reset_step_5_provide_claim_info(self): expected_message = translate("TEXT_GREET_USER_PEER_RESET") async with self._reset_claimer(): await aqtbot.wait_until(partial(self._greet_restart, expected_message)) await self.bootstrap_after_restart() return None
async def cancelled_step_4_exchange_claimer_sas(self): expected_message = translate("TEXT_CLAIM_DEVICE_WAIT_PEER_TRUST_ERROR") await self._cancel_invitation() await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None
async def reset_step_5_provide_claim_info(self): expected_message = translate("TEXT_CLAIM_DEVICE_PEER_RESET") cdpi_w = self.claim_device_provide_info_widget device_label = self.requested_device_label async with self._reset_greeter(): cdpi_w.line_edit_device.clear() await aqtbot.key_clicks(cdpi_w.line_edit_device, device_label.str) await aqtbot.key_clicks( cdpi_w.widget_auth.main_layout.itemAt( 0).widget().line_edit_password, self.password, ) await aqtbot.key_clicks( cdpi_w.widget_auth.main_layout.itemAt( 0).widget().line_edit_password_check, self.password, ) aqtbot.mouse_click(cdpi_w.button_ok, QtCore.Qt.LeftButton) await aqtbot.wait_until( partial(self._claim_restart, expected_message)) await self.bootstrap_after_restart() return None
async def offline_step_6_validate_claim_info(self): expected_message = translate("TEXT_CLAIM_DEVICE_CLAIM_ERROR") with running_backend.offline(): await aqtbot.wait_until( partial(self._claim_aborted, expected_message)) return None
async def test_unshare_workspace_while_connected(aqtbot, running_backend, logged_gui, autoclose_dialog, qt_thread_gateway, alice_user_fs, bob): w_w = await logged_gui.test_switch_to_workspaces_widget() wid = await alice_user_fs.workspace_create("Workspace") await alice_user_fs.workspace_share(wid, bob.user_id, WorkspaceRole.MANAGER) def _one_workspace_listed(): assert w_w.layout_workspaces.count() == 1 wk_button = w_w.layout_workspaces.itemAt(0).widget() assert isinstance(wk_button, WorkspaceButton) wk_button.name == "Workspace" await aqtbot.wait_until(_one_workspace_listed, timeout=2000) await alice_user_fs.workspace_share(wid, bob.user_id, None) def _no_workspace_listed(): assert w_w.layout_workspaces.count() == 1 label = w_w.layout_workspaces.itemAt(0).widget() assert isinstance(label, QtWidgets.QLabel) await aqtbot.wait_until(_no_workspace_listed, timeout=2000) assert autoclose_dialog.dialogs[0] == ( "Error", translate("TEXT_FILE_SHARING_REVOKED_workspace").format( workspace="Workspace"), )
async def test_link_file_unknown_org(core_config, gui_factory, autoclose_dialog, running_backend, alice): password = "******" save_device_with_password(core_config.config_dir, alice, password) # Cheating a bit but it does not matter, we just want a link that appears valid with # an unknown organization org_addr = BackendOrganizationAddr.build( running_backend.addr, "UnknownOrg", alice.organization_addr.root_verify_key) file_link = BackendOrganizationFileLinkAddr.build( org_addr, EntryID(), FsPath("/doesntmattereither")) gui = await gui_factory(core_config=core_config, start_arg=file_link.to_url()) lw = gui.test_get_login_widget() assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs[0][0] == "Error" assert autoclose_dialog.dialogs[0][1] == translate( "TEXT_FILE_LINK_NOT_IN_ORG_organization").format( organization="UnknownOrg") accounts_w = lw.widget.layout().itemAt(0).widget() assert accounts_w assert isinstance(accounts_w, LoginPasswordInputWidget)
def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [( "", translate("TEXT_FILE_LINK_PLEASE_LOG_IN_organization").format( organization=bob.organization_id), )]
async def reset_step_6_validate_claim_info(self): expected_message = translate("TEXT_CLAIM_DEVICE_PEER_RESET") async with self._reset_greeter(): await aqtbot.wait_until(partial(self._claim_restart, expected_message)) await self.bootstrap_after_restart() return None
async def cancelled_step_6_validate_claim_info(self): expected_message = translate("TEXT_INVITATION_ALREADY_USED") await self._cancel_invitation() await aqtbot.wait_until(partial(self._claim_restart, expected_message)) return None
def _assert_error(): assert len(autoclose_dialog.dialogs) == 2 assert ( "Error", translate("TEXT_WORKPACE_REENCRYPT_ACCESS_ERROR"), ) in autoclose_dialog.dialogs assert wk_button.button_reencrypt.isVisible()
async def cancelled_step_5_provide_claim_info(self): expected_message = translate("TEXT_GREET_USER_GET_REQUESTS_ERROR") await self._cancel_invitation() await aqtbot.wait_until(partial(self._greet_restart, expected_message)) return None
async def offline_step_2_start_greeter(self): expected_message = translate("TEXT_CLAIM_DEVICE_WAIT_PEER_ERROR") with running_backend.offline(): await aqtbot.wait_until( partial(self._claim_aborted, expected_message)) return None
def _error_shown(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs[0] == ( "Error", translate("TEXT_WORKSPACE_SHARING_SHARE_ERROR_workspace-user"). format(workspace="Workspace", user="******"), )
def _on_export_key(self, device): default_key_name = f"parsec-{device.organization_id}-{device.human_handle.label}-{device.device_label}.keys" key_path, _ = QFileDialog.getSaveFileName( self, translate("TEXT_EXPORT_KEY"), str(Path.home().joinpath(default_key_name)), filter=translate("IMPORT_KEY_FILTERS"), initialFilter=translate("IMPORT_KEY_INITIAL_FILTER"), ) if not key_path: return keys_dest = Path(key_path) try: shutil.copyfile(device.key_file_path, keys_dest) except IOError as err: show_error(self, translate("EXPORT_KEY_ERROR"), err)
async def test_link_file_unknown_org( aqtbot, core_config, gui_factory, autoclose_dialog, running_backend, alice ): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) # Cheating a bit but it does not matter, we just want a link that appears valid with # an unknown organization org_addr = BackendOrganizationAddr.build( running_backend.addr, OrganizationID("UnknownOrg"), alice.organization_addr.root_verify_key ) file_link = BackendOrganizationFileLinkAddr.build( organization_addr=org_addr, workspace_id=EntryID.new(), encrypted_path=b"<whatever>" ) gui = await gui_factory(core_config=core_config, start_arg=file_link.to_url()) lw = gui.test_get_login_widget() assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs[0][0] == "Error" assert autoclose_dialog.dialogs[0][1] == translate( "TEXT_FILE_LINK_NOT_IN_ORG_organization" ).format(organization="UnknownOrg") def _devices_listed(): assert lw.widget.layout().count() > 0 await aqtbot.wait_until(_devices_listed) accounts_w = lw.widget.layout().itemAt(0).widget() assert accounts_w assert isinstance(accounts_w, LoginPasswordInputWidget)
def test_keys_import(qtbot, core_config, alice, bob, monkeypatch): password = "******" save_device_with_password(core_config.config_dir, alice, password) tmp_path = core_config.config_dir.joinpath("tmp") tmp_path.mkdir() tmp_path.joinpath("devices").mkdir() fake_config = type("fake_config", (), {"config_dir": tmp_path})() w = KeysWidget(fake_config, parent=None) qtbot.addWidget(w) keys_layout = w.scroll_content.layout() assert keys_layout.count() == 0 key_glob = list(core_config.config_dir.joinpath("devices").glob("*.keys")) assert len(key_glob) == 1 monkeypatch.setattr( "parsec.core.gui.keys_widget.QFileDialog.getOpenFileName", lambda *x, **y: (key_glob[0], None), ) monkeypatch.setattr( "parsec.core.gui.keys_widget.ask_question", lambda *x, **y: translate("ACTION_IMPORT_YES") ) qtbot.mouseClick(w.button_import_key, QtCore.Qt.LeftButton) def key_imported(): assert keys_layout.count() == 1 qtbot.wait_until(key_imported)