def test_password_save_and_load(path_exists, config_dir, alice): config_dir = config_dir if path_exists else config_dir / "dummy" save_device_with_password_in_config(config_dir, alice, "S3Cr37") key_file = get_key_file(config_dir, alice) alice_reloaded = load_device_with_password(key_file, "S3Cr37") assert alice == alice_reloaded
async def _on_validate_clicked(self): self.button_validate.setEnabled(False) auth_method = self.widget_auth.get_auth_method() try: if auth_method == DeviceFileType.PASSWORD: save_device_with_password_in_config( self.core.config.config_dir, self.loaded_device, self.widget_auth.get_auth()) elif auth_method == DeviceFileType.SMARTCARD: await save_device_with_smartcard_in_config( self.core.config.config_dir, self.loaded_device) show_info(self, _("TEXT_AUTH_CHANGE_SUCCESS")) if self.dialog: self.dialog.accept() elif QApplication.activeModalWidget(): QApplication.activeModalWidget().accept() else: logger.warning( "Cannot close dialog when changing password info") except LocalDeviceCryptoError as exc: self.button_validate.setEnabled(True) if auth_method == DeviceFileType.SMARTCARD: show_error(self, _("TEXT_INVALID_SMARTCARD"), exception=exc) except LocalDeviceNotFoundError as exc: self.button_validate.setEnabled(True) if auth_method == DeviceFileType.PASSWORD: show_error(self, _("TEXT_CANNOT_SAVE_DEVICE"), exception=exc) except LocalDeviceError as exc: self.button_validate.setEnabled(True) show_error(self, _("TEXT_CANNOT_SAVE_DEVICE"), exception=exc)
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 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 test_login_no_available_devices(aqtbot, gui_factory, autoclose_dialog, core_config, alice): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) device = list_available_devices(core_config.config_dir)[0] gui = await gui_factory() ParsecApp.add_connected_device(device.organization_id, device.device_id) lw = gui.test_get_login_widget() lw.reload_devices() def _devices_listed(): assert lw.widget.layout().count() > 0 await aqtbot.wait_until(_devices_listed) no_device_w = lw.widget.layout().itemAt(0).widget() assert isinstance(no_device_w, LoginNoDevicesWidget) # 0 is spacer, 1 is label assert no_device_w.layout().itemAt( 2).widget().text() == "Create an organization" assert no_device_w.layout().itemAt( 3).widget().text() == "Join an organization" assert no_device_w.layout().itemAt(4).widget().text() == "Recover a device"
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_share_workspace(tmp_path, monkeypatch, alice, bob, cli_workspace_role): config_dir = tmp_path / "config" # Mocking factory_mock = AsyncMock() workspace_role, expected_workspace_role = cli_workspace_role @asynccontextmanager async def logged_core_factory(*args, **kwargs): yield factory_mock(*args, **kwargs) password = "******" save_device_with_password_in_config(config_dir, bob, password) alice_info = [ UserInfo( user_id=alice.user_id, human_handle=alice.human_handle, profile=alice.profile, created_on=alice.timestamp(), revoked_on=None, ) ] def _run_cli_share_workspace_test(args, expected_error_code, use_recipiant): factory_mock.reset_mock() factory_mock.return_value.user_fs.workspace_share.is_async = True factory_mock.return_value.find_humans.is_async = True factory_mock.return_value.find_humans.return_value = (alice_info, 1) factory_mock.return_value.find_workspace_from_name.return_value.id = "ws1_id" monkeypatch.setattr( "parsec.core.cli.share_workspace.logged_core_factory", logged_core_factory) runner = CliRunner() result = runner.invoke(cli, args) assert result.exit_code == expected_error_code factory_mock.assert_called_once_with(ANY, bob) factory_mock.return_value.user_fs.workspace_share.assert_called_once_with( "ws1_id", alice.user_id, expected_workspace_role) if use_recipiant: factory_mock.return_value.find_humans.assert_called_once() else: factory_mock.return_value.find_humans.assert_not_called() default_args = ( f"core share_workspace --password {password} " f"--device={bob.slughash} --config-dir={config_dir.as_posix()} " f"--role={workspace_role} " f"--workspace-name=ws1 ") # Test with user-id args = default_args + f"--user-id={alice.user_id}" _run_cli_share_workspace_test(args, 0, False) # Test with recipiant args = default_args + f"[email protected]" _run_cli_share_workspace_test(args, 0, True)
def test_available_devices_slughash_uniqueness(organization_factory, local_device_factory, config_dir): def _to_available(device): return AvailableDevice( key_file_path=get_default_key_file(config_dir, device), organization_id=device.organization_id, device_id=device.device_id, human_handle=device.human_handle, device_label=device.device_label, slug=device.slug, type=DeviceFileType.PASSWORD, ) def _assert_different_as_available(d1, d2): available_device_d1 = _to_available(d1) available_device_d2 = _to_available(d2) assert available_device_d1.slughash != available_device_d2.slughash # Make sure slughash is consistent between LocalDevice and AvailableDevice assert available_device_d1.slughash == d1.slughash assert available_device_d2.slughash == d2.slughash o1 = organization_factory("org1") o2 = organization_factory("org2") # Different user id o1u1d1 = local_device_factory("u1@d1", o1) o1u2d1 = local_device_factory("u2@d1", o1) _assert_different_as_available(o1u1d1, o1u2d1) # Different device name o1u1d2 = local_device_factory("u1@d2", o1) _assert_different_as_available(o1u1d1, o1u1d2) # Different organization id o2u1d1 = local_device_factory("u1@d1", o2) _assert_different_as_available(o1u1d1, o2u1d1) # Same organization_id, but different root verify key ! dummy_key = SigningKey.generate().verify_key o1u1d1_bad_rvk = o1u1d1.evolve( organization_addr=o1u1d1.organization_addr.build( backend_addr=o1u1d1.organization_addr.get_backend_addr(), organization_id=o1u1d1.organization_addr.organization_id, root_verify_key=dummy_key, )) _assert_different_as_available(o1u1d1, o1u1d1_bad_rvk) # Finally make sure slughash is stable through save/load save_device_with_password_in_config(config_dir, o1u1d1, "S3Cr37") key_file = get_key_file(config_dir, o1u1d1) o1u1d1_reloaded = load_device_with_password(key_file, "S3Cr37") available_device = _to_available(o1u1d1) available_device_reloaded = _to_available(o1u1d1_reloaded) assert available_device.slughash == available_device_reloaded.slughash
def test_same_device_id_different_orginazations(config_dir, alice, otheralice): devices = (alice, otheralice) for device in devices: save_device_with_password_in_config( config_dir, device, f"S3Cr37-{device.organization_id}") for device in devices: key_file = get_key_file(config_dir, device) device_reloaded = load_device_with_password( key_file, f"S3Cr37-{device.organization_id}") assert device == device_reloaded
def test_change_password(config_dir, alice): old_password = "******" new_password = "******" save_device_with_password_in_config(config_dir, alice, old_password) key_file = get_key_file(config_dir, alice) change_device_password(key_file, old_password, new_password) alice_reloaded = load_device_with_password(key_file, new_password) assert alice == alice_reloaded with pytest.raises(LocalDeviceCryptoError): load_device_with_password(key_file, old_password)
async def logged_gui(aqtbot, gui_factory, core_config, alice, bob, fixtures_customization): # Logged as bob (i.e. standard profile) by default if fixtures_customization.get("logged_gui_as_admin", False): device = alice else: device = bob save_device_with_password_in_config(core_config.config_dir, device, DEFAULT_PASSWORD) gui = await gui_factory() await gui.test_switch_to_logged_in(device) return gui
def test_pki_enrollment_not_available(tmp_path, alice, no_parsec_extension): # First need to have alice device on the disk config_dir = tmp_path / "config" alice_password = "******" save_device_with_password_in_config(config_dir, alice, alice_password) # Now Run the cli runner = CliRunner() for cmd in [ f"core pki_enrollment_submit --config-dir={config_dir.as_posix()} parsec://parsec.example.com/my_org?action=pki_enrollment", f"core pki_enrollment_poll --config-dir={config_dir.as_posix()}", f"core pki_enrollment_review_pendings --config-dir={config_dir.as_posix()} --device {alice.slughash} --password {alice_password}", ]: result = runner.invoke(cli, cmd) assert result.exit_code == 1 assert "Error: Parsec smartcard extension not available" in result.output
async def _on_validate_clicked(self): if isinstance(self.current_page, DeviceRecoveryImportPage1Widget): # No try/except given `self.line_edit_device` has already been validated against `DeviceLabel` device_label = DeviceLabel( validators.trim_user_name( self.current_page.line_edit_device.text())) self.jobs_ctx.submit_job( self.create_new_device_success, self.create_new_device_failure, self._create_new_device, device_label=device_label, file_path=PurePath(self.current_page.get_recovery_key_file()), passphrase=self.current_page.get_passphrase(), ) else: try: self.button_validate.setEnabled(False) auth_method = self.current_page.get_auth_method() if auth_method == DeviceFileType.PASSWORD: save_device_with_password_in_config( config_dir=self.config.config_dir, device=self.new_device, password=self.current_page.get_auth(), ) else: await save_device_with_smartcard_in_config( config_dir=self.config.config_dir, device=self.new_device) show_info(self, translate("TEXT_RECOVERY_IMPORT_SUCCESS")) self.dialog.accept() except LocalDeviceCryptoError as exc: self.button_validate.setEnabled(True) if auth_method == DeviceFileType.SMARTCARD: show_error(self, translate("TEXT_INVALID_SMARTCARD"), exception=exc) except LocalDeviceNotFoundError as exc: if auth_method == DeviceFileType.PASSWORD: show_error(self, translate("TEXT_CANNOT_SAVE_DEVICE"), exception=exc) self.button_validate.setEnabled(True) except LocalDeviceError as exc: show_error(self, translate("TEXT_CANNOT_SAVE_DEVICE"), exception=exc) self.button_validate.setEnabled(True)
async def test_switch_to_logged_in(self, device, password=DEFAULT_PASSWORD): try: save_device_with_password_in_config(self.config.config_dir, device, password) except LocalDeviceAlreadyExistsError: pass # Reload to take into account the new saved device self.test_get_login_widget().reload_devices() await self.test_proceed_to_login(device, password) central_widget = self.test_get_central_widget() assert central_widget is not None return central_widget
def test_list_devices(organization_factory, local_device_factory, config_dir): org1 = organization_factory("org1") org2 = organization_factory("org2") o1d11 = local_device_factory("d1@1", org1) o1d12 = local_device_factory("d1@2", org1) o1d21 = local_device_factory("d2@1", org1) o2d11 = local_device_factory("d1@1", org2, has_human_handle=False) o2d12 = local_device_factory("d1@2", org2, has_device_label=False) o2d21 = local_device_factory("d2@1", org2, has_human_handle=False, has_device_label=False) for device in [o1d11, o1d12, o1d21]: save_device_with_password_in_config(config_dir, device, "S3Cr37") for device in [o2d11, o2d12, o2d21]: save_device_with_password_in_config(config_dir, device, "secret") # Also add dummy stuff that should be ignored device_dir = config_dir / "devices" (device_dir / "bad1").touch() (device_dir / "373955f566#corp#bob@laptop").mkdir() dummy_slug = "a54ed6df3a#corp#alice@laptop" (device_dir / dummy_slug).mkdir() (device_dir / dummy_slug / f"{dummy_slug}.keys").write_bytes(b"dummy") devices = list_available_devices(config_dir) expected_devices = { AvailableDevice( key_file_path=get_key_file(config_dir, d), organization_id=d.organization_id, device_id=d.device_id, human_handle=d.human_handle, device_label=d.device_label, slug=d.slug, type=DeviceFileType.PASSWORD, ) for d in [o1d11, o1d12, o1d21, o2d11, o2d12, o2d21] } assert set(devices) == expected_devices
async def test_login(aqtbot, gui_factory, autoclose_dialog, core_config, alice, monkeypatch): # Create an existing device before starting the gui password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() 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 # 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] + "...")) # Only one device, we skip the device selection def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) await aqtbot.wait_until(_password_widget_shown) password_w = lw.widget.layout().itemAt(0).widget() await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals( [lw.login_with_password_clicked, tabw.logged_in]): aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) central_widget = gui.test_get_central_widget() assert central_widget is not None assert (central_widget.button_user.text() == f"{alice.organization_id}\n{alice.short_user_display}") assert gui.tab_center.tabText(0) == "CoolOrg - Alicey..."
def test_password_save_already_existing(config_dir, alice, alice2, otheralice): save_device_with_password_in_config(config_dir, alice, "S3Cr37") # Different devices should not overwrite each other save_device_with_password_in_config(config_dir, otheralice, "S3Cr37") save_device_with_password_in_config(config_dir, alice2, "S3Cr37") # Overwritting self is allowed save_device_with_password_in_config(config_dir, alice, "S3Cr37") devices = list_available_devices(config_dir) assert len(devices) == 3
async def test_expired_notification_logging( aqtbot, running_backend, autoclose_dialog, expiredorgalice, gui_factory, core_config, snackbar_catcher, ): # Log has alice on an expired organization save_device_with_password_in_config(core_config.config_dir, expiredorgalice, "P@ssw0rd") gui = await gui_factory() lw = gui.test_get_login_widget() tabw = gui.test_get_tab() def _devices_listed(): assert lw.widget.layout().count() > 0 await aqtbot.wait_until(_devices_listed) def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) await aqtbot.wait_until(_password_widget_shown) password_w = lw.widget.layout().itemAt(0).widget() await aqtbot.key_clicks(password_w.line_edit_password, "P@ssw0rd") async with aqtbot.wait_signals( [lw.login_with_password_clicked, tabw.logged_in]): aqtbot.mouse_click(password_w.button_login, QtCore.Qt.LeftButton) # Assert dialog def _expired_notified(): assert snackbar_catcher.snackbars == [("WARN", "The organization has expired")] await aqtbot.wait_until(_expired_notified)
async def _on_finished(self, new_device, auth_method, password): try: if auth_method == DeviceFileType.PASSWORD: save_device_with_password_in_config( config_dir=self.config.config_dir, device=new_device, password=password) elif auth_method == DeviceFileType.SMARTCARD: await save_device_with_smartcard_in_config( config_dir=self.config.config_dir, device=new_device) show_info(self, _("TEXT_CLAIM_DEVICE_SUCCESSFUL")) self.status = (new_device, auth_method, password) self.dialog.accept() except LocalDeviceCryptoError as exc: if auth_method == DeviceFileType.SMARTCARD: show_error(self, _("TEXT_INVALID_SMARTCARD"), exception=exc) except LocalDeviceNotFoundError as exc: if auth_method == DeviceFileType.PASSWORD: show_error(self, _("TEXT_CANNOT_SAVE_DEVICE"), exception=exc) except LocalDeviceError as exc: show_error(self, _("TEXT_CANNOT_SAVE_DEVICE"), exception=exc)
async def test_login_back_to_account_list(aqtbot, gui_factory, autoclose_dialog, core_config, alice, bob): # Create an existing device before starting the gui password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) save_device_with_password_in_config(core_config.config_dir, bob, password) gui = await gui_factory() lw = gui.test_get_login_widget() 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 async with aqtbot.wait_signal(accounts_w.account_clicked): aqtbot.mouse_click( accounts_w.accounts_widget.layout().itemAt(0).widget(), QtCore.Qt.LeftButton) def _password_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginPasswordInputWidget) await aqtbot.wait_until(_password_widget_shown) password_w = lw.widget.layout().itemAt(0).widget() async with aqtbot.wait_signal(password_w.back_clicked): aqtbot.mouse_click(password_w.button_back, QtCore.Qt.LeftButton) def _account_widget_shown(): assert isinstance(lw.widget.layout().itemAt(0).widget(), LoginAccountsWidget)
async def test_link_file_unknown_workspace( aqtbot, core_config, gui_factory, autoclose_dialog, running_backend, alice ): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) file_link = BackendOrganizationFileLinkAddr.build( organization_addr=alice.organization_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() def _password_prompt(): assert len(autoclose_dialog.dialogs) == 0 lpi_w = lw.widget.layout().itemAt(0).widget() assert isinstance(lpi_w, LoginPasswordInputWidget) await aqtbot.wait_until(_password_prompt) lpi_w = lw.widget.layout().itemAt(0).widget() await aqtbot.key_clicks(lpi_w.line_edit_password, password) await aqtbot.wait_until(lambda: lpi_w.line_edit_password.text() == password) tabw = gui.test_get_tab() async with aqtbot.wait_signals([lw.login_with_password_clicked, tabw.logged_in]): aqtbot.mouse_click(lpi_w.button_login, QtCore.Qt.LeftButton) def _error_shown(): assert len(autoclose_dialog.dialogs) == 1 print(autoclose_dialog.dialogs) assert autoclose_dialog.dialogs[0] == ( "Error", "You do not have access to the workspace containing the file. It may not have been shared with you.", ) await aqtbot.wait_until(_error_shown)
async def _on_finalize_clicked(self): try: if self.widget_auth.get_auth_method() == DeviceFileType.PASSWORD: save_device_with_password_in_config( config_dir=self.config.config_dir, device=self.new_device, password=self.widget_auth.get_auth(), ) elif self.widget_auth.get_auth_method( ) == DeviceFileType.SMARTCARD: await save_device_with_smartcard_in_config( config_dir=self.config.config_dir, device=self.new_device) self.succeeded.emit(self.new_device, self.widget_auth.get_auth_method(), self.widget_auth.get_auth()) except LocalDeviceCryptoError as exc: if self.widget_auth.get_auth_method() == DeviceFileType.SMARTCARD: show_error(self, _("TEXT_INVALID_SMARTCARD"), exception=exc) except LocalDeviceNotFoundError as exc: if self.widget_auth.get_auth_method() == DeviceFileType.PASSWORD: show_error(self, _("TEXT_CANNOT_SAVE_DEVICE"), exception=exc) except LocalDeviceError as exc: show_error(self, _("TEXT_CANNOT_SAVE_DEVICE"), exception=exc)
async def test_login_device_list(aqtbot, gui_factory, autoclose_dialog, core_config, alice, bob, alice2, adam, otheralice): password = "******" local_devices = [alice, bob, alice2, otheralice, adam] devices = [] for d in local_devices: path = save_device_with_password_in_config(core_config.config_dir, d, password) devices.append(load_device_file(path)) # Settings the last device used core_config = core_config.evolve(gui_last_device=bob.device_id.str) gui = await gui_factory(core_config=core_config) lw = gui.test_get_login_widget() def _accounts_widget_listed(): assert lw.widget.layout().count() == 1 await aqtbot.wait_until(_accounts_widget_listed) accounts_w = lw.widget.layout().itemAt(0).widget() assert accounts_w def _devices_listed(): # 5 devices, 1 spacer assert accounts_w.accounts_widget.layout().count() == len(devices) + 1 await aqtbot.wait_until(_devices_listed) for idx in range(len(devices)): acc_w = accounts_w.accounts_widget.layout().itemAt(idx).widget() assert acc_w.device in devices assert acc_w.device.device_display == acc_w.label_device.text() assert acc_w.device.short_user_display == acc_w.label_name.text() assert acc_w.device.organization_id.str == acc_w.label_organization.text( ) # We set the last_device in the config, the first one in the list should be bob if idx == 0: assert acc_w.device.device_id == bob.device_id devices.remove(acc_w.device) assert len(devices) == 0
def test_multiple_files_same_device(config_dir, alice): path = save_device_with_password_in_config(config_dir, alice, "test") # File names contain the slughash assert path.stem == alice.slughash # .. but are no longer meaningful (path.parent / "testing.keys").write_bytes(path.read_bytes()) # Make sure we don't list duplicates devices = list_available_devices(config_dir) assert len(devices) == 1 assert devices[0].device_id == alice.device_id # Remove orignal file path.unlink() devices = list_available_devices(config_dir) assert len(devices) == 1 assert devices[0].device_id == alice.device_id
async def test_import_recovery_device( gui, aqtbot, monkeypatch, core_config, running_backend, catch_import_recovery_widget, autoclose_dialog, tmp_path, alice, kind, ): PASSWORD = "******" NEW_DEVICE_LABEL = DeviceLabel("Alice_New_Device") save_device_with_password_in_config(core_config.config_dir, alice, PASSWORD) recovery_device = await generate_recovery_device(alice) file_name = get_recovery_device_file_name(recovery_device) file_path = tmp_path / file_name passphrase = await save_recovery_device(file_path, recovery_device) with monkeypatch.context() as m: m.setattr( "parsec.core.gui.main_window.ask_question", lambda *args, **kwargs: translate("ACTION_RECOVER_DEVICE"), ) aqtbot.key_click(gui, "i", QtCore.Qt.ControlModifier, 200) imp_w = await catch_import_recovery_widget() assert imp_w assert isinstance(imp_w.current_page, DeviceRecoveryImportPage1Widget) assert not imp_w.button_validate.isEnabled() imp_w.current_page.line_edit_device.setText("") imp_w.current_page.label_key_file.setText(str(file_path)) assert not imp_w.button_validate.isEnabled() await aqtbot.key_clicks(imp_w.current_page.edit_passphrase, "abcdef") assert not imp_w.button_validate.isEnabled() await aqtbot.wait_until( lambda: imp_w.current_page.label_passphrase_error.text() == translate( "TEXT_RECOVERY_INVALID_PASSPHRASE")) assert imp_w.current_page.label_passphrase_error.isVisible() imp_w.current_page.edit_passphrase.setText("") await aqtbot.key_clicks(imp_w.current_page.edit_passphrase, passphrase) await aqtbot.wait_until( lambda: not imp_w.current_page.label_passphrase_error.isVisible()) assert not imp_w.button_validate.isEnabled() await aqtbot.key_clicks(imp_w.current_page.line_edit_device, str(NEW_DEVICE_LABEL)) await aqtbot.wait_until(imp_w.button_validate.isEnabled) if kind == "ok": async with aqtbot.wait_signal(imp_w.create_new_device_success): aqtbot.mouse_click(imp_w.button_validate, QtCore.Qt.LeftButton) def _page2_shown(): assert isinstance(imp_w.current_page, AuthenticationChoiceWidget) await aqtbot.wait_until(_page2_shown) assert not imp_w.button_validate.isEnabled() await aqtbot.key_clicks( imp_w.current_page.main_layout.itemAt( 0).widget().line_edit_password, PASSWORD) assert not imp_w.button_validate.isEnabled() await aqtbot.key_clicks( imp_w.current_page.main_layout.itemAt( 0).widget().line_edit_password_check, PASSWORD) await aqtbot.wait_until(imp_w.button_validate.isEnabled) aqtbot.mouse_click(imp_w.button_validate, QtCore.Qt.LeftButton) await aqtbot.wait_until(lambda: autoclose_dialog.dialogs == [( "", translate("TEXT_RECOVERY_IMPORT_SUCCESS"))]) lw = gui.test_get_login_widget() 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.accounts_widget.layout().count() == 3 assert any( accounts_w.accounts_widget.layout().itemAt(i).widget() is not None and accounts_w.accounts_widget.layout().itemAt( i).widget().label_device.text() == str(NEW_DEVICE_LABEL) for i in range(accounts_w.accounts_widget.layout().count())) elif kind == "offline": with running_backend.offline(): aqtbot.mouse_click(imp_w.button_validate, QtCore.Qt.LeftButton) def _error_shown(): assert autoclose_dialog.dialogs == [ ("Error", translate("IMPORT_KEY_BACKEND_OFFLINE")) ] await aqtbot.wait_until(_error_shown)
async def test_pki_enrollment(tmp_path, mocked_parsec_ext_smartcard, backend, alice): async with cli_with_running_backend_testbed(backend, alice) as (backend_addr, alice): # First, save the local device needed for pki_enrollment_review_pendings command config_dir = tmp_path / "config" alice_password = "******" save_device_with_password_in_config(config_dir, alice, alice_password) runner = CliRunner() async def _cli_invoke_in_thread(cmd: str): # We must run the command from another thread given it will create it own trio loop async with real_clock_timeout(): # Pass DEBUG environment variable for better output on crash return await trio.to_thread.run_sync( lambda: runner.invoke(cli, cmd, env={"DEBUG": "1"})) async def run_review_pendings(extra_args: str = "", check_result: bool = True): result = await _cli_invoke_in_thread( f"core pki_enrollment_review_pendings --config-dir={config_dir.as_posix()} --device {alice.slughash} --password {alice_password} {extra_args}" ) if not check_result: return result if result.exception: raise AssertionError( f"CliRunner raise an exception: {result.exception}" ) from result.exception assert ( result.exit_code == 0 ), f"Bad exit_code: {result.exit_code}\nOutput: {result.output}" first_line, *other_lines = result.output.splitlines() match = re.match(r"^Found ([0-9]+) pending enrollment\(s\):", first_line) assert match enrollments_count = int(match.group(1)) # Just retrieve the enrollment ID enrollments = [] for line in other_lines: match = re.match(r"^Pending enrollment ([a-f0-9]+)", line) if match: enrollments.append(match.group(1)) assert len(enrollments) == enrollments_count return enrollments addr = BackendPkiEnrollmentAddr.build( backend_addr, organization_id=alice.organization_id) async def run_submit(extra_args: str = "", check_result: bool = True): result = await _cli_invoke_in_thread( f"core pki_enrollment_submit --config-dir={config_dir.as_posix()} {addr} --device-label PC1 {extra_args}" ) if not check_result: return result if result.exception: raise result.exception assert ( result.exit_code == 0 ), f"Bad exit_code: {result.exit_code}\nOutput: {result.output}" match = re.match(r"PKI enrollment ([a-f0-9]+) submitted", result.output.splitlines()[-1]) assert match return UUID(match.group(1)) async def run_poll(extra_args: str = "", check_result: bool = True): result = await _cli_invoke_in_thread( f"core pki_enrollment_poll --config-dir={config_dir.as_posix()} --password S3cr3t {extra_args}" ) if not check_result: return result if result.exception: raise AssertionError( f"CliRunner raise an exception: {result.exception}" ) from result.exception assert ( result.exit_code == 0 ), f"Bad exit_code: {result.exit_code}\nOutput: {result.output}" first_line, *other_lines = result.output.splitlines() match = re.match(r"^Found ([0-9]+) pending enrollment\(s\):", first_line) assert match enrollments_count = int(match.group(1)) # Just retrieve the enrollment ID enrollments = [] for line in other_lines: match = re.match(r"^Pending enrollment ([a-f0-9]+)", line) if match: enrollments.append(match.group(1)) assert len(enrollments) == enrollments_count return enrollments # Time for testing ! # List with no enrollments assert await run_review_pendings(extra_args="--list-only") == [] # Poll with no local enrollments assert await run_poll() == [] # New enrollment enrollment_id1 = await run_submit() assert await run_poll() == [enrollment_id1.hex[:3]] # Poll doesn't modify the pending enrollment assert await run_poll() == [enrollment_id1.hex[:3]] # List new enrollment assert await run_review_pendings(extra_args="--list-only" ) == [enrollment_id1.hex[:3]] # Try to reply enrollment without force result = await run_submit(check_result=False) assert result.exit_code == 1 assert ( f"The certificate `{mocked_parsec_ext_smartcard.default_x509_certificate.certificate_sha1.hex()}` has already been submitted" in result.output) assert await run_review_pendings(extra_args="--list-only" ) == [enrollment_id1.hex[:3] ] # No change # Actually reply enrollment with force enrollment_id3 = await run_submit(extra_args="--force") assert await run_review_pendings(extra_args="--list-only" ) == [enrollment_id3.hex[:3]] # Reject enrollment await run_review_pendings(extra_args=f"--reject {enrollment_id3.hex}") assert await run_review_pendings(extra_args="--list-only") == [] # Accept enrollment enrollment_id4 = await run_submit() await run_review_pendings( extra_args= f"--accept {enrollment_id4.hex} --pki-extra-trust-root {mocked_parsec_ext_smartcard.default_trust_root_path}" ) assert await run_review_pendings(extra_args="--list-only") == [] # It is no longer possible to do another enrollment with the same certificate (until the user is revoked) result = await run_submit(check_result=False) assert ( f"The certificate `{mocked_parsec_ext_smartcard.default_x509_certificate.certificate_sha1.hex()}` has already been enrolled" in result.output) # Reject/Accept not possible against unknown/cancelled/accepted enrollments for extra_args in ( # Unknown f"--reject e499f9aed05e4287875a177909d62d90", f"--accept e499f9aed05e4287875a177909d62d90", # Already Cancelled f"--reject {enrollment_id1.hex[:3]}", f"--accept {enrollment_id1.hex[:3]}", # Already Rejected f"--reject {enrollment_id3.hex[:3]}", f"--accept {enrollment_id3.hex[:3]}", # Already Accepted f"--reject {enrollment_id4.hex[:3]}", f"--accept {enrollment_id4.hex[:3]}", ): result = await run_review_pendings(extra_args=extra_args, check_result=False) assert result.exit_code == 1 assert "Additional --accept/--reject elements not used" in result.output # Poll to handle the accepted enrollments, and discard the non-pendings ones ids = await run_poll( extra_args= f"--finalize {enrollment_id4.hex[:3]} --pki-extra-trust-root {mocked_parsec_ext_smartcard.default_trust_root_path}" ) assert set(ids) == { enrollment_id1.hex[:3], enrollment_id3.hex[:3], enrollment_id4.hex[:3] } # Now we should have a new local device ! result = await _cli_invoke_in_thread( f"core list_devices --config-dir={config_dir.as_posix()}") assert result.exit_code == 0 assert result.output.startswith("Found 2 device(s)") assert "CoolOrg: John Doe <*****@*****.**> @ PC1" in result.output # And all the enrollments should have been taken care of assert await run_poll() == []
async def test_export_recovery_device( gui, aqtbot, monkeypatch, core_config, running_backend, autoclose_dialog, catch_export_recovery_widget, tmp_path, alice, kind, ): PASSWORD = "******" save_device_with_password_in_config(core_config.config_dir, alice, PASSWORD) with monkeypatch.context() as m: m.setattr( "parsec.core.gui.main_window.ask_question", lambda *args, **kwargs: translate("ACTION_CREATE_RECOVERY_DEVICE"), ) aqtbot.key_click(gui, "i", QtCore.Qt.ControlModifier, 200) exp_w = await catch_export_recovery_widget() assert exp_w assert isinstance(exp_w.current_page, DeviceRecoveryExportPage1Widget) assert not exp_w.button_validate.isEnabled() assert exp_w.current_page.combo_devices.count() == 1 assert exp_w.current_page.combo_devices.currentData() == alice.slug exp_w.current_page.label_file_path.setText(str(tmp_path)) exp_w.current_page._check_infos() monkeypatch.setattr( "parsec.core.gui.device_recovery_export_widget.get_text_input", lambda *args, **kwargs: PASSWORD, ) if kind == "ok": aqtbot.mouse_click(exp_w.button_validate, QtCore.Qt.LeftButton) def _page2_shown(): assert isinstance(exp_w.current_page, DeviceRecoveryExportPage2Widget) await aqtbot.wait_until(_page2_shown) recovery_file = Path(exp_w.current_page.label_file_path.text()) assert recovery_file.is_file() assert recovery_file.stat().st_size > 0 assert len(exp_w.current_page.edit_passphrase.toPlainText()) > 0 assert exp_w.button_validate.isEnabled() aqtbot.mouse_click(exp_w.button_validate, QtCore.Qt.LeftButton) elif kind == "offline": with running_backend.offline(): aqtbot.mouse_click(exp_w.button_validate, QtCore.Qt.LeftButton) def _error_shown(): assert autoclose_dialog.dialogs == [ ("Error", translate("EXPORT_KEY_BACKEND_OFFLINE")) ] await aqtbot.wait_until(_error_shown) elif kind == "already_exists": file_name = get_recovery_device_file_name(alice) (tmp_path / file_name).touch() aqtbot.mouse_click(exp_w.button_validate, QtCore.Qt.LeftButton) def _error_shown(): assert autoclose_dialog.dialogs == [ ("Error", translate("TEXT_RECOVERY_DEVICE_FILE_ALREADY_EXISTS")) ] await aqtbot.wait_until(_error_shown)
async def test_full_enrollment( aqtbot, mocked_parsec_ext_smartcard, core_config, gui, alice, running_backend, catch_enrollment_accept_check_infos_widget, snackbar_catcher, monkeypatch, autoclose_dialog, catch_enrollment_query_widget, ): # Add trust root to the configuration gui.config = gui.config.evolve(pki_extra_trust_roots={ mocked_parsec_ext_smartcard.default_trust_root_path }) config_dir = gui.config.config_dir alice_password = "******" save_device_with_password_in_config(config_dir, alice, alice_password) pki_org_addr = BackendPkiEnrollmentAddr.build( alice.organization_addr.get_backend_addr(), alice.organization_addr.organization_id) monkeypatch.setattr( "parsec.core.gui.main_window.get_text_input", lambda *args, **kwargs: (pki_org_addr.to_url()), ) # Open the PKI enrollment dialog aqtbot.key_click(gui, "o", QtCore.Qt.ControlModifier, 200) eq_w = await catch_enrollment_query_widget() assert eq_w aqtbot.mouse_click(eq_w.button_select_cert, QtCore.Qt.LeftButton) def _cert_loaded(): assert not eq_w.label_cert_error.isVisible() assert eq_w.widget_user_info.isVisible() assert eq_w.button_ask_to_join.isEnabled() assert eq_w.button_select_cert.isEnabled() assert eq_w.line_edit_user_name.text() == "John Doe" assert eq_w.line_edit_user_email.text() == "*****@*****.**" assert len(eq_w.line_edit_device.text()) await aqtbot.wait_until(_cert_loaded) aqtbot.mouse_click(eq_w.button_ask_to_join, QtCore.Qt.LeftButton) def _request_made(): assert autoclose_dialog.dialogs == [("", "Your request has been sent")] await aqtbot.wait_until(_request_made) # Check if the enrollment appears among the list of devices with a PENDING status lw = gui.test_get_login_widget() def _devices_listed(): assert lw.widget.layout().count() == 1 assert lw.widget.layout().itemAt(0) is not None assert lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().count() == 3 pending = lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().itemAt(0).widget() assert isinstance(pending, EnrollmentPendingButton) assert pending.label_org.text( ) == alice.organization_addr.organization_id.str assert pending.label_name.text() == "John Doe" assert pending.label_status.text() == "Pending request" assert pending.button_action.isVisible() is False await aqtbot.wait_until(_devices_listed) # Log in with an admin device cw = await gui.test_switch_to_logged_in(alice, alice_password) cw.button_user.text() == "Alice" assert cw.menu.button_enrollment.isVisible() is True # Check if the enrollment request we made appears e_w = await gui.test_switch_to_enrollment_widget() def _enrollments_listed(): assert e_w.main_layout.count() == 1 assert e_w.main_layout.itemAt(0) is not None assert isinstance(e_w.main_layout.itemAt(0).widget(), EnrollmentButton) await aqtbot.wait_until(_enrollments_listed) button = e_w.main_layout.itemAt(0).widget() assert isinstance(button, EnrollmentButton) assert button.button_accept.isEnabled() assert button.button_reject.isEnabled() assert button.widget_cert_infos.isVisible() assert not button.widget_cert_error.isVisible() assert button.label_name.text() == "John Doe" assert button.label_email.text() == "*****@*****.**" assert button.label_issuer.text() == "My CA" assert button.label_cert_validity.text() == "✔ Valid certificate" # Accept the enrollment request aqtbot.mouse_click(button.button_accept, QtCore.Qt.LeftButton) acc_w = await catch_enrollment_accept_check_infos_widget() assert acc_w assert acc_w.line_edit_user_full_name.text() == "John Doe" assert acc_w.line_edit_user_email.text() == "*****@*****.**" assert len(acc_w.line_edit_device.text()) > 0 assert acc_w.combo_profile.currentData() is None assert acc_w.button_create_user.isEnabled() is False acc_w.combo_profile.setCurrentIndex(2) def _button_create_user_enabled(): acc_w.button_create_user.isEnabled() is True await aqtbot.wait_until(_button_create_user_enabled) aqtbot.mouse_click(acc_w.button_create_user, QtCore.Qt.LeftButton) await aqtbot.wait_until(lambda: snackbar_catcher.snackbars == [( "INFO", translate("TEXT_ENROLLMENT_ACCEPT_SUCCESS"))]) snackbar_catcher.reset() monkeypatch.setattr("parsec.core.gui.users_widget.ensure_string_size", lambda s, size, font: (s[:12] + "...")) # Check if the new user appears u_w = await gui.test_switch_to_users_widget() assert u_w.layout_users.count() == 4 a_w = u_w.layout_users.itemAt(0).widget() assert a_w.label_username.text() == "Adamy McAdam..." assert a_w.label_email.text() == "adam@example..." j_w = u_w.layout_users.itemAt(3).widget() assert j_w.label_username.text() == "John Doe..." assert j_w.label_email.text() == "john@example..." # Logout and check the status of the enrollment request lw = await gui.test_logout_and_switch_to_login_widget() def _devices_listed(): assert lw.widget.layout().count() == 1 assert lw.widget.layout().itemAt(0) is not None assert lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().count() == 3 pending = lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().itemAt(0).widget() assert isinstance(pending, EnrollmentPendingButton) assert pending.label_org.text( ) == alice.organization_addr.organization_id.str assert pending.label_name.text() == "John Doe" assert pending.label_status.text() == "Request approved" assert pending.button_action.isVisible() is True await aqtbot.wait_until(_devices_listed) # Finalize the enrollment pending = lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().itemAt(0).widget() aqtbot.mouse_click(pending.button_action, QtCore.Qt.LeftButton) # Check if the new device appears def _new_device_listed(): org_name = alice.organization_addr.organization_id.str devices = ["John Doe", "Alicey McAliceFace"] assert lw.widget.layout().itemAt(0) is not None assert lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().count() == 3 for i in [0, 1]: device = (lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().itemAt(i).widget()) assert isinstance(device, AccountButton) assert device.label_name.text() in devices devices.remove(device.label_name.text()) assert device.label_organization.text() == org_name await aqtbot.wait_until(_new_device_listed) # Log in with the new device new_device = None for i in [0, 1]: device_button = (lw.widget.layout().itemAt( 0).widget().accounts_widget.layout().itemAt(i).widget()) if device_button.label_name.text() == "John Doe": new_device = device_button.device break cw = await gui.test_proceed_to_login(new_device) assert isinstance(cw, CentralWidget) assert cw.button_user.text( ) == f"{alice.organization_addr.organization_id.str}\nJohn Doe"
async def test_share_workspace( aqtbot, running_backend, logged_gui, gui_workspace_sharing, autoclose_dialog, core_config, alice, bob, adam, catch_share_workspace_widget, monkeypatch, snackbar_catcher, ): _, w_w, share_w_w = gui_workspace_sharing # 1) Logged as Bob, we share our workspace with Adam # Fix the return value of ensure_string_size, because it can depend of the size of the window monkeypatch.setattr( "parsec.core.gui.workspace_button.ensure_string_size", lambda s, size, font: (s[:16] + "..."), ) def _users_listed(): assert share_w_w.scroll_content.layout().count() == 4 await aqtbot.wait_until(_users_listed) user_w = share_w_w.scroll_content.layout().itemAt(1).widget() assert user_w.combo_role.currentIndex() == 0 assert user_w.user_info.short_user_display == adam.human_handle.label user_w.status_timer.setInterval(200) snackbar_catcher.reset() def _sharing_updated(): assert snackbar_catcher.snackbars == [( "INFO", "The workspace <b>Workspace</b> has been shared with <b>Adamy McAdamFace</b>." )] async with aqtbot.wait_signal(share_w_w.share_success): user_w.combo_role.setCurrentIndex(3) await aqtbot.wait_until(_sharing_updated) async with aqtbot.wait_signal(user_w.status_timer.timeout): def _timer_started(): assert not user_w.label_status.pixmap().isNull() assert user_w.status_timer.isActive() await aqtbot.wait_until(_timer_started) def _timer_stopped(): assert user_w.label_status.pixmap().isNull() assert not user_w.status_timer.isActive() await aqtbot.wait_until(_timer_stopped) # 2) Sharing info should now be displayed in the workspaces list view # We have to be careful about keeping a reference to the parent. # Otherwise, it's garbage collected later on and can trigger a # sporadic segfault, causing the test to become inconsistent parent = share_w_w.parent().parent() async with aqtbot.wait_signals([parent.closing, w_w.list_success]): parent.reject() def _workspace_listed(): assert w_w.layout_workspaces.count() == 1 wk_button = w_w.layout_workspaces.itemAt(0).widget() assert isinstance(wk_button, WorkspaceButton) assert wk_button.name == EntryName("Workspace") assert wk_button.label_title.toolTip( ) == "Workspace (shared with Adamy McAdamFace)" assert wk_button.label_title.text() == "Workspace (share..." assert not autoclose_dialog.dialogs await aqtbot.wait_until(_workspace_listed) # 3) Now loggin as Adam and check the workspaces view password = "******" save_device_with_password_in_config(core_config.config_dir, adam, password) await logged_gui.test_logout() await logged_gui.test_proceed_to_login(adam, password) w_w = await logged_gui.test_switch_to_workspaces_widget() def _workspace_listed(): assert w_w.layout_workspaces.count() == 1 wk_button = w_w.layout_workspaces.itemAt(0).widget() assert isinstance(wk_button, WorkspaceButton) assert wk_button.name == EntryName("Workspace") assert not autoclose_dialog.dialogs await aqtbot.wait_until(_workspace_listed) w_b = w_w.layout_workspaces.itemAt(0).widget() assert isinstance(w_b, WorkspaceButton) assert w_b.workspace_name == EntryName("Workspace") assert w_b.is_owner is False # Also check the workspace shared with view aqtbot.mouse_click(w_b.button_share, QtCore.Qt.LeftButton) share_w_w = await catch_share_workspace_widget() def _users_listed(): assert share_w_w.scroll_content.layout().count() == 4 await aqtbot.wait_until(_users_listed) user_w = share_w_w.scroll_content.layout().itemAt(0).widget() assert user_w.user_info.user_id == bob.user_id assert user_w.role == WorkspaceRole.OWNER assert not user_w.is_current_user assert user_w.combo_role.currentIndex() == 4 assert user_w.isEnabled() is False user_w = share_w_w.scroll_content.layout().itemAt(1).widget() assert user_w.user_info.user_id == adam.user_id assert user_w.role == WorkspaceRole.MANAGER assert user_w.is_current_user assert user_w.combo_role.currentIndex() == 3 assert user_w.isEnabled() is False user_w = share_w_w.scroll_content.layout().itemAt(2).widget() assert user_w.user_info.user_id == alice.user_id assert user_w.role is None assert not user_w.is_current_user assert user_w.combo_role.currentIndex() == 0 assert user_w.isEnabled() is True
def test_load_bad_password(config_dir, alice): save_device_with_password_in_config(config_dir, alice, "S3Cr37") with pytest.raises(LocalDeviceCryptoError): key_file = get_key_file(config_dir, alice) load_device_with_password(key_file, "dummy")