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}")
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 _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_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)
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 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 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 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, )
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_user_claim(backend, running_backend, data_base_dir, alice, alice_backend_cmds, user_fs_factory, with_labels): claimer_email = "*****@*****.**" invitation = await backend.invite.new_for_user( organization_id=alice.organization_id, greeter_user_id=alice.user_id, claimer_email=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, ) if with_labels: # Let's pretent we invited a Fortnite player... requested_human_handle = HumanHandle(email="*****@*****.**", label="xXx_Z4ck_xXx") requested_device_label = DeviceLabel("Ultr4_B00st") granted_human_handle = HumanHandle(email="*****@*****.**", label="Zack") granted_device_label = DeviceLabel("Desktop") else: requested_human_handle = None requested_device_label = None granted_human_handle = None granted_device_label = None granted_profile = UserProfile.STANDARD 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, UserClaimInitialCtx) assert initial_ctx.claimer_email == claimer_email 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_user( requested_device_label=requested_device_label, requested_human_handle=requested_human_handle, ) assert isinstance(new_device, LocalDevice) # User storage should be populated with non-speculative user manifest # before save the device await user_storage_non_speculative_init( data_base_dir=data_base_dir, device=new_device) async def _run_greeter(): initial_ctx = UserGreetInitialCtx(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 assert in_progress_ctx.requested_human_handle == requested_human_handle await in_progress_ctx.do_create_new_user( author=alice, device_label=granted_device_label, human_handle=granted_human_handle, profile=granted_profile, ) 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.device_id != alice.device_id assert new_device.device_label == granted_device_label # Label is normally ignored when comparing HumanLabel if with_labels: assert new_device.human_handle.label == granted_human_handle.label assert new_device.human_handle.email == granted_human_handle.email else: assert new_device.human_handle is None assert new_device.profile == granted_profile # Extra check to make sure claimer&greeter data are not mixed assert new_device.user_manifest_id != alice.user_manifest_id assert new_device.user_manifest_key != alice.user_manifest_key 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 user, device = await backend.user.get_user_with_device( new_device.organization_id, new_device.device_id) assert user.profile == granted_profile assert user.human_handle == granted_human_handle assert device.device_label == granted_device_label if with_labels: assert user.user_certificate != user.redacted_user_certificate assert device.device_certificate != device.redacted_device_certificate else: assert user.user_certificate == user.redacted_user_certificate assert device.device_certificate == device.redacted_device_certificate # Test the behavior of this new user device async with user_fs_factory(alice) as alicefs: async with user_fs_factory(new_device) as newfs: # New user should start with a non-speculative user manifest um = newfs.get_user_manifest() assert um.is_placeholder assert not um.speculative # Share a workspace with new user aw_id = await alicefs.workspace_create(EntryName("alice_workspace") ) await alicefs.workspace_share(aw_id, new_device.user_id, WorkspaceRole.CONTRIBUTOR) # New user cannot create a new workspace zw_id = await newfs.workspace_create(EntryName("zack_workspace")) await newfs.workspace_share(zw_id, alice.user_id, WorkspaceRole.READER) # Now both users should have the same workspaces await alicefs.process_last_messages() await newfs.process_last_messages() await newfs.sync() # Not required, but just to make sure it works alice_um = alicefs.get_user_manifest() zack_um = newfs.get_user_manifest() assert {(w.id, w.key.secret) for w in alice_um.workspaces } == {(w.id, w.key.secret) for w in zack_um.workspaces}
async def test_claimer_handle_reset(backend, running_backend, alice, alice_backend_cmds): 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 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 # Step 1 async with real_clock_timeout(): async with trio.open_nursery() as nursery: async def _do_claimer(): nonlocal claimer_in_progress_ctx claimer_in_progress_ctx = await claimer_initial_ctx.do_wait_peer( ) async def _do_greeter(): nonlocal greeter_in_progress_ctx greeter_in_progress_ctx = await greeter_initial_ctx.do_wait_peer( ) nursery.start_soon(_do_claimer) nursery.start_soon(_do_greeter) # Claimer restart the conduit while greeter try to do step 2 async with real_clock_timeout(): async with trio.open_nursery() as nursery: async def _do_claimer(): nonlocal claimer_in_progress_ctx claimer_in_progress_ctx = await claimer_initial_ctx.do_wait_peer( ) nursery.start_soon(_do_claimer) with pytest.raises(InvitePeerResetError): await greeter_in_progress_ctx.do_wait_peer_trust() # Greeter redo step 1 greeter_in_progress_ctx = await greeter_initial_ctx.do_wait_peer( ) # Now do the other way around: greeter restart conduit while claimer try step 2 async with real_clock_timeout(): async with trio.open_nursery() as nursery: async def _do_greeter(): nonlocal greeter_in_progress_ctx greeter_in_progress_ctx = await greeter_initial_ctx.do_wait_peer( ) nursery.start_soon(_do_greeter) with pytest.raises(InvitePeerResetError): await claimer_in_progress_ctx.do_signify_trust() # Claimer redo step 1 claimer_in_progress_ctx = await claimer_initial_ctx.do_wait_peer( )
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"