def _to_http_redirection_url(client_ctx, invitation): return BackendInvitationAddr.build( backend_addr=self._config.backend_addr, organization_id=client_ctx.organization_id, invitation_type=invitation.TYPE, token=invitation.token, ).to_http_redirection_url()
async def _invite_user( config: CoreConfig, device: LocalDevice, email: str, send_email: bool ) -> None: async with spinner("Creating user invitation"): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_new( type=InvitationType.USER, claimer_email=email, send_email=send_email ) if rep["status"] != "ok": raise RuntimeError(f"Backend refused to create user invitation: {rep}") if send_email and "email_sent" in rep: if rep["email_sent"] != InvitationEmailSentStatus.SUCCESS: click.secho("Email could not be sent", fg="red") action_addr = BackendInvitationAddr.build( backend_addr=device.organization_addr.get_backend_addr(), organization_id=device.organization_id, invitation_type=InvitationType.USER, token=rep["token"], ) action_addr_display = click.style(action_addr.to_url(), fg="yellow") click.echo(f"url: {action_addr_display}")
def _on_list_success(self, job): assert job.is_finished() assert job.status == "ok" total, users, invitations = job.ret # Securing if page go to far if total == 0 and self._page > 1: self._page -= 1 self.reset() self._flush_users_list() current_user = self.core.device.user_id for invitation in reversed(invitations): addr = BackendInvitationAddr.build( backend_addr=self.core.device.organization_addr, organization_id=self.core.device.organization_id, invitation_type=InvitationType.USER, token=invitation["token"], ) self.add_user_invitation(invitation["claimer_email"], addr) for user_info in users: self.add_user(user_info=user_info, is_current_user=current_user == user_info.user_id) self.spinner.hide() self.pagination(total=total, users_on_page=len(users)) self.button_users_filter.setEnabled(True) self.line_edit_search.setEnabled(True)
async def test_claim_device_backend_desync(aqtbot, running_backend, backend, autoclose_dialog, alice, gui, monkeypatch): # Client is 5 minutes ahead def _timestamp(self): return pendulum_now().add(minutes=5) monkeypatch.setattr("parsec.api.protocol.BaseClientHandshake.timestamp", _timestamp) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=InvitationToken.new(), ) gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [ ("Error", translate("TEXT_BACKEND_STATE_DESYNC")) ] await aqtbot.wait_until(_assert_dialogs)
async def test_claim_device_already_deleted(aqtbot, running_backend, backend, autoclose_dialog, alice, gui): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation_addr.token, on=pendulum_now(), reason=InvitationDeletedReason.CANCELLED, ) gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [ ("Error", translate("TEXT_INVITATION_ALREADY_USED")) ] await aqtbot.wait_until(_assert_dialogs)
async def new_device_invitation( self, send_email: bool ) -> Tuple[BackendInvitationAddr, InvitationEmailSentStatus]: """ Raises: BackendConnectionError """ rep = await self._backend_conn.cmds.invite_new( type=InvitationType.DEVICE, send_email=send_email) if rep["status"] != "ok": raise BackendConnectionError(f"Backend error: {rep}") if not ("email_sent" in rep): email_sent = InvitationEmailSentStatus.SUCCESS else: email_sent = rep["email_sent"] return ( BackendInvitationAddr.build( backend_addr=self.device.organization_addr.get_backend_addr(), organization_id=self.device.organization_id, invitation_type=InvitationType.DEVICE, token=rep["token"], ), email_sent, )
async def new_user_invitation( self, email: str, send_email: bool ) -> Tuple[BackendInvitationAddr, InvitationEmailSentStatus]: """ Raises: BackendConnectionError """ rep = await self._backend_conn.cmds.invite_new( type=InvitationType.USER, claimer_email=email, send_email=send_email) if rep["status"] == "already_member": raise BackendInvitationOnExistingMember( "An user already exist with this email") elif rep["status"] != "ok": raise BackendConnectionError(f"Backend error: {rep}") if not ("email_sent" in rep): email_sent = InvitationEmailSentStatus.SUCCESS else: email_sent = rep["email_sent"] return ( BackendInvitationAddr.build( backend_addr=self.device.organization_addr.get_backend_addr(), organization_id=self.device.organization_id, invitation_type=InvitationType.USER, token=rep["token"], ), email_sent, )
def _parse_invitation_token_or_url(raw: str) -> Union[BackendInvitationAddr, InvitationToken]: try: return InvitationToken.from_hex(raw) except ValueError: try: return BackendInvitationAddr.from_url(raw) except ValueError: raise ValueError("Must be an invitation URL or Token")
async def device_invitation_addr(backend, bob): invitation = await backend.invite.new_for_device( organization_id=bob.organization_id, greeter_user_id=bob.user_id) return BackendInvitationAddr.build( backend_addr=bob.organization_addr, organization_id=bob.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, )
async def bootstrap(self): author = logged_gui.test_get_central_widget().core.device claimer_email = self.requested_email # Create new invitation invitation = await backend.invite.new_for_user( organization_id=author.organization_id, greeter_user_id=author.user_id, claimer_email=claimer_email, ) invitation_addr = BackendInvitationAddr.build( backend_addr=author.organization_addr.get_backend_addr(), organization_id=author.organization_id, invitation_type=InvitationType.USER, token=invitation.token, ) # Switch to users page users_widget = await logged_gui.test_switch_to_users_widget() assert users_widget.layout_users.count() == 4 invitation_widget = users_widget.layout_users.itemAt(0).widget() assert isinstance(invitation_widget, UserInvitationButton) assert invitation_widget.email == claimer_email # Click on the invitation button aqtbot.mouse_click(invitation_widget.button_greet, QtCore.Qt.LeftButton) greet_user_widget = await catch_greet_user_widget() assert isinstance(greet_user_widget, GreetUserWidget) greet_user_information_widget = await catch_greet_user_widget() assert isinstance(greet_user_information_widget, GreetUserInstructionsWidget) def _greet_user_displayed(): assert greet_user_widget.dialog.isVisible() assert greet_user_widget.isVisible() assert greet_user_widget.dialog.label_title.text( ) == "Greet a new user" assert greet_user_information_widget.isVisible() await aqtbot.wait_until(_greet_user_displayed) self.author = author self.users_widget = users_widget self.invitation_widget = invitation_widget self.invitation_addr = invitation_addr self.greet_user_widget = greet_user_widget self.greet_user_information_widget = greet_user_information_widget self.assert_initial_state() # Sanity check
async def invitation_addr(backend, alice): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) return BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, )
async def test_user_claim_but_active_users_limit_reached( backend, running_backend, alice): # Organization has reached active user limit await backend.organization.update(alice.organization_id, active_users_limit=1) # Invitation is still ok... invitation = await backend.invite.new_for_user( organization_id=alice.organization_id, greeter_user_id=alice.user_id, claimer_email="*****@*****.**", ) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.USER, token=invitation.token, ) async def _run_greeter(): async with backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key) as alice_backend_cmds: initial_ctx = UserGreetInitialCtx(cmds=alice_backend_cmds, token=invitation_addr.token) in_progress_ctx = await initial_ctx.do_wait_peer() in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() in_progress_ctx = await in_progress_ctx.do_signify_trust() in_progress_ctx = await in_progress_ctx.do_get_claim_requests() # ...this is where the limit should be enforced with pytest.raises(InviteActiveUsersLimitReachedError): await in_progress_ctx.do_create_new_user( author=alice, device_label=in_progress_ctx.requested_device_label, human_handle=in_progress_ctx.requested_human_handle, profile=UserProfile.STANDARD, ) async def _run_claimer(): async with backend_invited_cmds_factory(addr=invitation_addr) as cmds: initial_ctx = await claimer_retrieve_info(cmds) in_progress_ctx = await initial_ctx.do_wait_peer() in_progress_ctx = await in_progress_ctx.do_signify_trust() in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() await in_progress_ctx.do_claim_user(requested_device_label=None, requested_human_handle=None) async with real_clock_timeout(): async with trio.open_nursery() as nursery: nursery.start_soon(_run_claimer) await _run_greeter() # Claimer is not notified that the greeter has failed so we # must explicitly cancel it nursery.cancel_scope.cancel()
async def test_handshake_unknown_organization(running_backend, coolorg): invitation_addr = BackendInvitationAddr.build( backend_addr=running_backend.addr, organization_id=coolorg.organization_id, invitation_type=InvitationType.DEVICE, token=uuid4(), ) with pytest.raises(BackendInvitationNotFound) as exc: async with backend_invited_cmds_factory(invitation_addr) as cmds: await cmds.ping() assert str(exc.value) == "Invalid handshake: Invitation not found"
async def user_invitation_addr(backend, bob): invitation = await backend.invite.new_for_user( organization_id=bob.organization_id, greeter_user_id=bob.user_id, claimer_email="*****@*****.**", ) return BackendInvitationAddr.build( backend_addr=bob.organization_addr.get_backend_addr(), organization_id=bob.organization_id, invitation_type=InvitationType.USER, token=invitation.token, )
async def test_proxy_with_websocket(monkeypatch, connection_type, proxy_type): signing_key = SigningKey.generate() device_id = DeviceID("zack@pc1") proxy_events = [] def _event_hook(event): proxy_events.append(event) async with trio.open_nursery() as nursery: target_port = await start_port_watchdog(nursery, _event_hook) proxy_port = await start_proxy_for_websocket(nursery, target_port, _event_hook) if proxy_type == "http_proxy": proxy_url = f"http://127.0.0.1:{proxy_port}" monkeypatch.setitem(os.environ, "http_proxy", proxy_url) else: assert proxy_type == "http_proxy_pac" pac_server_port = await start_pac_server( nursery=nursery, pac_rule=f"PROXY 127.0.0.1:{proxy_port}", event_hook=_event_hook) pac_server_url = f"http://127.0.0.1:{pac_server_port}" monkeypatch.setitem(os.environ, "http_proxy_pac", pac_server_url) # HTTP_PROXY_PAC has priority over HTTP_PROXY monkeypatch.setitem(os.environ, "http_proxy", f"http://127.0.0.1:{target_port}") async with real_clock_timeout(): with pytest.raises(BackendNotAvailable): if connection_type == "authenticated": await connect_as_authenticated( addr=BackendOrganizationAddr.from_url( f"parsec://127.0.0.1:{target_port}/CoolOrg?no_ssl=true&rvk=7NFDS4VQLP3XPCMTSEN34ZOXKGGIMTY2W2JI2SPIHB2P3M6K4YWAssss" ), device_id=device_id, signing_key=signing_key, ) else: assert connection_type == "invited" await connect_as_invited(addr=BackendInvitationAddr.from_url( f"parsec://127.0.0.1:{target_port}/CoolOrg?no_ssl=true&action=claim_user&token=3a50b191122b480ebb113b10216ef343" )) assert proxy_events == [ *(["PAC file retreived from server"] if proxy_type == "http_proxy_pac" else []), "Connected to proxy", "Reaching target through proxy", ] nursery.cancel_scope.cancel()
def test_build_addrs(): backend_addr = BackendAddr.from_url(BackendAddrTestbed.url) assert backend_addr.hostname == "parsec.cloud.com" assert backend_addr.port == 443 assert backend_addr.use_ssl is True organization_id = OrganizationID("MyOrg") root_verify_key = SigningKey.generate().verify_key organization_addr = BackendOrganizationAddr.build( backend_addr=backend_addr, organization_id=organization_id, root_verify_key=root_verify_key ) assert organization_addr.organization_id == organization_id assert organization_addr.root_verify_key == root_verify_key organization_bootstrap_addr = BackendOrganizationBootstrapAddr.build( backend_addr=backend_addr, organization_id=organization_id, token="a0000000000000000000000000000001", ) assert organization_bootstrap_addr.token == "a0000000000000000000000000000001" assert organization_bootstrap_addr.organization_id == organization_id organization_bootstrap_addr2 = BackendOrganizationBootstrapAddr.build( backend_addr=backend_addr, organization_id=organization_id, token=None ) assert organization_bootstrap_addr2.organization_id == organization_id assert organization_bootstrap_addr2.token == "" organization_file_link_addr = BackendOrganizationFileLinkAddr.build( organization_addr=organization_addr, workspace_id=EntryID.from_hex("2d4ded12-7406-4608-833b-7f57f01156e2"), encrypted_path=b"<encrypted_payload>", ) assert organization_file_link_addr.workspace_id == EntryID.from_hex( "2d4ded12-7406-4608-833b-7f57f01156e2" ) assert organization_file_link_addr.encrypted_path == b"<encrypted_payload>" invitation_addr = BackendInvitationAddr.build( backend_addr=backend_addr, organization_id=organization_id, invitation_type=InvitationType.USER, token=InvitationToken.from_hex("a0000000000000000000000000000001"), ) assert invitation_addr.organization_id == organization_id assert invitation_addr.token == InvitationToken.from_hex("a0000000000000000000000000000001") assert invitation_addr.invitation_type == InvitationType.USER
async def new_device_invitation(self, send_email: bool) -> BackendInvitationAddr: """ Raises: BackendConnectionError """ rep = await self._backend_conn.cmds.invite_new( type=InvitationType.DEVICE, send_email=send_email ) if rep["status"] != "ok": raise BackendConnectionError(f"Backend error: {rep}") return BackendInvitationAddr.build( backend_addr=self.device.organization_addr, organization_id=self.device.organization_id, invitation_type=InvitationType.DEVICE, token=rep["token"], )
async def test_handshake_organization_expired(running_backend, expiredorg, expiredorgalice): invitation = await running_backend.backend.invite.new_for_device( organization_id=expiredorgalice.organization_id, greeter_user_id=expiredorgalice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=running_backend.addr, organization_id=expiredorgalice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) with pytest.raises(BackendConnectionRefused) as exc: async with backend_invited_cmds_factory(invitation_addr) as cmds: await cmds.ping() assert str(exc.value) == "Trial organization has expired"
async def test_invited_cmd_keepalive(mock_clock, monkeypatch, backend, running_backend, backend_addr, alice): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr, organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) def _cmds_factory(keepalive): return backend_invited_cmds_factory(invitation_addr, keepalive=keepalive) await _test_keepalive(mock_clock, monkeypatch, _cmds_factory)
async def new_user_invitation(self, email: str, send_email: bool) -> BackendInvitationAddr: """ Raises: BackendConnectionError """ rep = await self._backend_conn.cmds.invite_new( type=InvitationType.USER, claimer_email=email, send_email=send_email ) if rep["status"] == "already_member": raise InviteAlreadyMemberError() elif rep["status"] != "ok": raise BackendConnectionError(f"Backend error: {rep}") return BackendInvitationAddr.build( backend_addr=self.device.organization_addr, organization_id=self.device.organization_id, invitation_type=InvitationType.USER, token=rep["token"], )
async def bootstrap(self): author = logged_gui.test_get_central_widget().core.device # Create new invitation invitation = await backend.invite.new_for_device( organization_id=author.organization_id, greeter_user_id=author.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=author.organization_addr.get_backend_addr(), organization_id=author.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) # Switch to devices page devices_widget = await logged_gui.test_switch_to_devices_widget() assert devices_widget.layout_devices.count() == 2 # Click on the invitation button aqtbot.mouse_click(devices_widget.button_add_device, QtCore.Qt.LeftButton) greet_device_widget = await catch_greet_device_widget() assert isinstance(greet_device_widget, GreetDeviceWidget) greet_device_information_widget = await catch_greet_device_widget() assert isinstance(greet_device_information_widget, GreetDeviceInstructionsWidget) def _greet_device_displayed(): assert greet_device_widget.dialog.isVisible() assert greet_device_widget.isVisible() assert greet_device_widget.dialog.label_title.text( ) == "Greet a new device" assert greet_device_information_widget.isVisible() await aqtbot.wait_until(_greet_device_displayed) self.author = author self.devices_widget = devices_widget self.invitation_addr = invitation_addr self.greet_device_widget = greet_device_widget self.greet_device_information_widget = greet_device_information_widget self.assert_initial_state() # Sanity check
async def test_claim_user_unknown_invitation(aqtbot, running_backend, backend, autoclose_dialog, alice, gui): invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.USER, token=InvitationToken.new(), ) gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [ ("Error", translate("TEXT_CLAIM_USER_INVITATION_NOT_FOUND")) ] await aqtbot.wait_until(_assert_dialogs)
async def _invite_device(config, device): async with spinner("Creating device invitation"): async with backend_authenticated_cmds_factory( addr=device.organization_addr, device_id=device.device_id, signing_key=device.signing_key, keepalive=config.backend_connection_keepalive, ) as cmds: rep = await cmds.invite_new(type=InvitationType.DEVICE) if rep["status"] != "ok": raise RuntimeError(f"Backend refused to create device invitation: {rep}") action_addr = BackendInvitationAddr.build( backend_addr=device.organization_addr, organization_id=device.organization_id, invitation_type=InvitationType.DEVICE, token=rep["token"], ) action_addr_display = click.style(action_addr.to_url(), fg="yellow") click.echo(f"url: {action_addr_display}")
async def test_claim_device_unknown_invitation(aqtbot, running_backend, backend, autoclose_dialog, alice, gui): invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr, organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=uuid4(), ) await aqtbot.run(gui.add_instance, invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [ ("Error", translate("TEXT_CLAIM_DEVICE_INVITATION_NOT_FOUND")) ] await aqtbot.wait_until(_assert_dialogs)
async def test_claim_device_offline_backend(aqtbot, running_backend, backend, autoclose_dialog, alice, gui): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) with running_backend.offline(): gui.add_instance(invitation_addr.to_url()) def _assert_dialogs(): assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs == [ ("Error", translate("TEXT_INVITATION_BACKEND_NOT_AVAILABLE")) ] await aqtbot.wait_until(_assert_dialogs)
async def bootstrap(self): claimer_email = self.requested_human_handle.email # Create new invitation invitation = await backend.invite.new_for_user( organization_id=self.author.organization_id, greeter_user_id=self.author.user_id, claimer_email=claimer_email, ) invitation_addr = BackendInvitationAddr.build( backend_addr=self.author.organization_addr.get_backend_addr(), organization_id=self.author.organization_id, invitation_type=InvitationType.USER, token=invitation.token, ) # Switch to users claim page gui.add_instance(invitation_addr.to_url()) cu_w = await catch_claim_user_widget() assert isinstance(cu_w, ClaimUserWidget) cui_w = await catch_claim_user_widget() assert isinstance(cui_w, ClaimUserInstructionsWidget) def _register_user_displayed(): tab = gui.test_get_tab() assert tab and tab.isVisible() assert cu_w.isVisible() assert cu_w.dialog.label_title.text() == "Register a user" assert cui_w.isVisible() await aqtbot.wait_until(_register_user_displayed) self.invitation_addr = invitation_addr self.claim_user_widget = cu_w self.claim_user_instructions_widget = cui_w self.assert_initial_state() # Sanity check
async def bootstrap(self): # Create new invitation invitation = await backend.invite.new_for_device( organization_id=self.author.organization_id, greeter_user_id=self.author.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=self.author.organization_addr.get_backend_addr(), organization_id=self.author.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) # Switch to device claim page gui.add_instance(invitation_addr.to_url()) cd_w = await catch_claim_device_widget() assert isinstance(cd_w, ClaimDeviceWidget) cdi_w = await catch_claim_device_widget() assert isinstance(cdi_w, ClaimDeviceInstructionsWidget) def _register_device_displayed(): tab = gui.test_get_tab() assert tab and tab.isVisible() assert cd_w.isVisible() assert cd_w.dialog.label_title.text() == "Register a device" assert cdi_w.isVisible() await aqtbot.wait_until(_register_device_displayed) self.invitation_addr = invitation_addr self.claim_device_widget = cd_w self.claim_device_instructions_widget = cdi_w self.assert_initial_state() # Sanity check
async def test_good_device_claim(backend, running_backend, alice, bob, alice_backend_cmds, user_fs_factory, with_labels): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) if with_labels: requested_device_label = DeviceLabel("Foo's label") granted_device_label = DeviceLabel("Bar's label") else: requested_device_label = None granted_device_label = None new_device = None # Simulate out-of-bounds canal oob_send, oob_recv = trio.open_memory_channel(0) async def _run_claimer(): async with backend_invited_cmds_factory(addr=invitation_addr) as cmds: initial_ctx = await claimer_retrieve_info(cmds) assert isinstance(initial_ctx, DeviceClaimInitialCtx) assert initial_ctx.greeter_user_id == alice.user_id assert initial_ctx.greeter_human_handle == alice.human_handle in_progress_ctx = await initial_ctx.do_wait_peer() choices = in_progress_ctx.generate_greeter_sas_choices(size=4) assert len(choices) == 4 assert in_progress_ctx.greeter_sas in choices greeter_sas = await oob_recv.receive() assert greeter_sas == in_progress_ctx.greeter_sas in_progress_ctx = await in_progress_ctx.do_signify_trust() await oob_send.send(in_progress_ctx.claimer_sas) in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() nonlocal new_device new_device = await in_progress_ctx.do_claim_device( requested_device_label=requested_device_label) assert isinstance(new_device, LocalDevice) async def _run_greeter(): initial_ctx = DeviceGreetInitialCtx(cmds=alice_backend_cmds, token=invitation_addr.token) in_progress_ctx = await initial_ctx.do_wait_peer() await oob_send.send(in_progress_ctx.greeter_sas) in_progress_ctx = await in_progress_ctx.do_wait_peer_trust() choices = in_progress_ctx.generate_claimer_sas_choices(size=5) assert len(choices) == 5 assert in_progress_ctx.claimer_sas in choices claimer_sas = await oob_recv.receive() assert claimer_sas == in_progress_ctx.claimer_sas in_progress_ctx = await in_progress_ctx.do_signify_trust() in_progress_ctx = await in_progress_ctx.do_get_claim_requests() assert in_progress_ctx.requested_device_label == requested_device_label await in_progress_ctx.do_create_new_device( author=alice, device_label=granted_device_label) async with real_clock_timeout(): async with trio.open_nursery() as nursery: nursery.start_soon(_run_claimer) nursery.start_soon(_run_greeter) assert new_device is not None assert new_device.user_id == alice.user_id assert new_device.device_name != alice.device_name assert new_device.device_label == granted_device_label assert new_device.human_handle == alice.human_handle assert new_device.private_key == alice.private_key assert new_device.signing_key != alice.signing_key assert new_device.profile == alice.profile assert new_device.user_manifest_id == alice.user_manifest_id assert new_device.user_manifest_key == alice.user_manifest_key # Make sure greeter&claimer data are not mixed assert new_device.local_symkey != alice.local_symkey # Now invitation should have been deleted rep = await alice_backend_cmds.invite_list() assert rep == {"status": "ok", "invitations": []} # Verify user&device data in backend _, device = await backend.user.get_user_with_device( new_device.organization_id, new_device.device_id) assert device.device_label == granted_device_label if with_labels: assert device.device_certificate != device.redacted_device_certificate else: assert device.device_certificate == device.redacted_device_certificate # Test the behavior of this new device async with user_fs_factory(bob) as bobfs: async with user_fs_factory(alice) as alicefs: async with user_fs_factory(new_device) as newfs: # New device should start with a speculative user manifest um = newfs.get_user_manifest() assert um.is_placeholder assert um.speculative # Old device modify user manifest await alicefs.workspace_create(EntryName("wa")) await alicefs.sync() # New sharing from other user wb_id = await bobfs.workspace_create(EntryName("wb")) await bobfs.workspace_share(wb_id, alice.user_id, WorkspaceRole.CONTRIBUTOR) # Test new device get access to both new workspaces await newfs.process_last_messages() await newfs.sync() newfs_um = newfs.get_user_manifest() # Make sure new and old device have the same view on data await alicefs.sync() alicefs_um = alicefs.get_user_manifest() assert newfs_um == alicefs_um
async def test_claimer_handle_cancel_event(backend, running_backend, alice, alice_backend_cmds, fail_on_step): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) async def _cancel_invitation(): await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation_addr.token, on=pendulum_now(), reason=InvitationDeletedReason.CANCELLED, ) async with backend_invited_cmds_factory( addr=invitation_addr) as claimer_cmds: greeter_initial_ctx = UserGreetInitialCtx(cmds=alice_backend_cmds, token=invitation_addr.token) claimer_initial_ctx = await claimer_retrieve_info(claimer_cmds) claimer_in_progress_ctx = None greeter_in_progress_ctx = None async def _do_claimer(): nonlocal claimer_in_progress_ctx if fail_on_step == "wait_peer": return claimer_in_progress_ctx = await claimer_initial_ctx.do_wait_peer() if fail_on_step == "signify_trust": return claimer_in_progress_ctx = await claimer_in_progress_ctx.do_signify_trust( ) if fail_on_step == "wait_peer_trust": return claimer_in_progress_ctx = await claimer_in_progress_ctx.do_wait_peer_trust( ) async def _do_greeter(): nonlocal greeter_in_progress_ctx if fail_on_step == "wait_peer": return greeter_in_progress_ctx = await greeter_initial_ctx.do_wait_peer() if fail_on_step == "signify_trust": return greeter_in_progress_ctx = await greeter_in_progress_ctx.do_wait_peer_trust( ) if fail_on_step == "wait_peer_trust": return greeter_in_progress_ctx = await greeter_in_progress_ctx.do_signify_trust( ) async with real_clock_timeout(): async with trio.open_nursery() as nursery: nursery.start_soon(_do_claimer) nursery.start_soon(_do_greeter) async with real_clock_timeout(): async with trio.open_nursery() as nursery: async def _do_claimer_wait_peer(): with pytest.raises( BackendInvitationAlreadyUsed) as exc_info: await claimer_initial_ctx.do_wait_peer() assert str( exc_info.value ) == "Invalid handshake: Invitation already deleted" async def _do_claimer_signify_trust(): with pytest.raises( BackendInvitationAlreadyUsed) as exc_info: await claimer_in_progress_ctx.do_signify_trust() assert str( exc_info.value ) == "Invalid handshake: Invitation already deleted" async def _do_claimer_wait_peer_trust(): with pytest.raises( BackendInvitationAlreadyUsed) as exc_info: await claimer_in_progress_ctx.do_wait_peer_trust() assert str( exc_info.value ) == "Invalid handshake: Invitation already deleted" async def _do_claimer_claim_device(): with pytest.raises( BackendInvitationAlreadyUsed) as exc_info: await claimer_in_progress_ctx.do_claim_device( requested_device_label=DeviceLabel( "TheSecretDevice")) assert str( exc_info.value ) == "Invalid handshake: Invitation already deleted" steps = { "wait_peer": _do_claimer_wait_peer, "signify_trust": _do_claimer_signify_trust, "wait_peer_trust": _do_claimer_wait_peer_trust, "claim_device": _do_claimer_claim_device, } _do_claimer = steps[fail_on_step] with backend.event_bus.listen() as spy: nursery.start_soon(_do_claimer) # Be sure that _do_claimer got valid invitations before cancelation await spy.wait_with_timeout( BackendEvent.INVITE_CONDUIT_UPDATED) await _cancel_invitation() await spy.wait_with_timeout( BackendEvent.INVITE_STATUS_CHANGED)
async def test_claimer_handle_command_failure(backend, running_backend, alice, alice_backend_cmds, monkeypatch, fail_on_step): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) invitation_addr = BackendInvitationAddr.build( backend_addr=alice.organization_addr.get_backend_addr(), organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) async def _cancel_invitation(): await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation_addr.token, on=pendulum_now(), reason=InvitationDeletedReason.CANCELLED, ) async with backend_invited_cmds_factory( addr=invitation_addr) as claimer_cmds: greeter_initial_ctx = UserGreetInitialCtx(cmds=alice_backend_cmds, token=invitation_addr.token) claimer_initial_ctx = await claimer_retrieve_info(claimer_cmds) claimer_in_progress_ctx = None greeter_in_progress_ctx = None async def _do_claimer(): nonlocal claimer_in_progress_ctx if fail_on_step == "wait_peer": return claimer_in_progress_ctx = await claimer_initial_ctx.do_wait_peer() if fail_on_step == "signify_trust": return claimer_in_progress_ctx = await claimer_in_progress_ctx.do_signify_trust( ) if fail_on_step == "wait_peer_trust": return claimer_in_progress_ctx = await claimer_in_progress_ctx.do_wait_peer_trust( ) async def _do_greeter(): nonlocal greeter_in_progress_ctx if fail_on_step == "wait_peer": return greeter_in_progress_ctx = await greeter_initial_ctx.do_wait_peer() if fail_on_step == "signify_trust": return greeter_in_progress_ctx = await greeter_in_progress_ctx.do_wait_peer_trust( ) if fail_on_step == "wait_peer_trust": return greeter_in_progress_ctx = await greeter_in_progress_ctx.do_signify_trust( ) async with real_clock_timeout(): async with trio.open_nursery() as nursery: nursery.start_soon(_do_claimer) nursery.start_soon(_do_greeter) deleted_event = trio.Event() async def _send_event(*args, **kwargs): if BackendEvent.INVITE_STATUS_CHANGED in args and ( kwargs.get("status") == InvitationStatus.DELETED): deleted_event.set() await trio.sleep(0) backend.invite._send_event = _send_event monkeypatch.setattr("parsec.backend.postgresql.invite.send_signal", _send_event) async with real_clock_timeout(): await _cancel_invitation() await deleted_event.wait() with pytest.raises(BackendInvitationAlreadyUsed) as exc_info: if fail_on_step == "wait_peer": await claimer_initial_ctx.do_wait_peer() elif fail_on_step == "signify_trust": await claimer_in_progress_ctx.do_signify_trust() elif fail_on_step == "wait_peer_trust": await claimer_in_progress_ctx.do_wait_peer_trust() elif fail_on_step == "claim_device": await claimer_in_progress_ctx.do_claim_device( requested_device_label=DeviceLabel("TheSecretDevice")) else: raise AssertionError(f"Unknown step {fail_on_step}") assert str(exc_info.value ) == "Invalid handshake: Invitation already deleted"