async def _do_new_user_invitation( conn, organization_id: OrganizationID, greeter_user_id: UserID, claimer_email: Optional[str], created_on: DateTime, ) -> InvitationToken: if claimer_email: invitation_type = InvitationType.USER q = _q_retrieve_compatible_user_invitation( organization_id=organization_id.str, type=invitation_type.value, greeter_user_id=greeter_user_id.str, claimer_email=claimer_email, ) else: invitation_type = InvitationType.DEVICE q = _q_retrieve_compatible_device_invitation( organization_id=organization_id.str, type=invitation_type.value, greeter_user_id=greeter_user_id.str, ) # Check if no compatible invitations already exists row = await conn.fetchrow(*q) if row: token = InvitationToken(row["token"]) else: # No risk of UniqueViolationError given token is a uuid4 token = InvitationToken.new() await conn.execute( *_q_insert_invitation( organization_id=organization_id.str, type=invitation_type.value, token=token, greeter_user_id=greeter_user_id.str, claimer_email=claimer_email, created_on=created_on, ) ) await send_signal( conn, BackendEvent.INVITE_STATUS_CHANGED, organization_id=organization_id, greeter=greeter_user_id, token=token, status=InvitationStatus.IDLE, ) return 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 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_handshake_expired_organization(backend, server_factory, expiredorg, alice, type): if type == "invited": ch = InvitedClientHandshake( organization_id=expiredorg.organization_id, invitation_type=InvitationType.USER, token=InvitationToken.new(), ) else: # authenticated ch = AuthenticatedClientHandshake( organization_id=expiredorg.organization_id, device_id=alice.device_id, user_signkey=alice.signing_key, root_verify_key=expiredorg.root_verify_key, ) with backend.event_bus.listen() as spy: async with server_factory(backend.handle_client) as server: stream = await server.connection_factory() transport = await Transport.init_for_client(stream, "127.0.0.1") challenge_req = await transport.recv() answer_req = ch.process_challenge_req(challenge_req) await transport.send(answer_req) result_req = await transport.recv() with pytest.raises(HandshakeOrganizationExpired): ch.process_result_req(result_req) await spy.wait_with_timeout(BackendEvent.ORGANIZATION_EXPIRED)
async def test_handshake_unknown_organization(backend, server_factory, organization_factory, alice, type): bad_org = organization_factory() if type == "invited": ch = InvitedClientHandshake( organization_id=bad_org.organization_id, invitation_type=InvitationType.USER, token=InvitationToken.new(), ) else: # authenticated ch = AuthenticatedClientHandshake( organization_id=bad_org.organization_id, device_id=alice.device_id, user_signkey=alice.signing_key, root_verify_key=bad_org.root_verify_key, ) async with server_factory(backend.handle_client) as server: stream = await server.connection_factory() transport = await Transport.init_for_client(stream, "127.0.0.1") challenge_req = await transport.recv() answer_req = ch.process_challenge_req(challenge_req) await transport.send(answer_req) result_req = await transport.recv() with pytest.raises(HandshakeBadIdentity): ch.process_result_req(result_req)
def test_backend_invitation_addr_build(): from parsec.core.types.backend_address import ( _PyBackendInvitationAddr, BackendInvitationAddr, _RsBackendInvitationAddr, BackendAddr, ) from parsec.api.protocol import InvitationToken assert _RsBackendInvitationAddr is BackendInvitationAddr INVITATION_TOKEN = InvitationToken(uuid4()) BACKEND_ADDR = BackendAddr.from_url("parsec://parsec.cloud/") py_ba = _PyBackendInvitationAddr.build( BACKEND_ADDR, organization_id=OrganizationID("MyOrg"), invitation_type=InvitationType.USER, token=INVITATION_TOKEN, ) rs_ba = BackendInvitationAddr.build( BACKEND_ADDR, organization_id=OrganizationID("MyOrg"), invitation_type=InvitationType.USER, token=INVITATION_TOKEN, ) _check_equal_backend_invitation_addrs(rs_ba, py_ba)
def test_backend_invitation_addr_init(): from parsec.core.types.backend_address import ( _PyBackendInvitationAddr, BackendInvitationAddr, _RsBackendInvitationAddr, ) from parsec.api.protocol import InvitationToken assert _RsBackendInvitationAddr is BackendInvitationAddr TOKEN = InvitationToken(uuid4()) py_ba = _PyBackendInvitationAddr( OrganizationID("MyOrg"), invitation_type=InvitationType.USER, token=TOKEN, hostname="parsec.cloud", ) rs_ba = BackendInvitationAddr( OrganizationID("MyOrg"), invitation_type=InvitationType.USER, token=TOKEN, hostname="parsec.cloud", ) _check_equal_backend_invitation_addrs(rs_ba, py_ba)
async def test_greeter_exchange_bad_access(alice, backend, alice_backend_sock, reason): if reason == "deleted_invitation": invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id ) await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation.token, on=datetime(2000, 1, 2), reason=InvitationDeletedReason.ROTTEN, ) token = invitation.token status = "already_deleted" else: assert reason == "unknown_token" token = InvitationToken.new() status = "not_found" greeter_privkey = PrivateKey.generate() for command, params in [ ( invite_1_greeter_wait_peer, {"token": token, "greeter_public_key": greeter_privkey.public_key}, ), (invite_2a_greeter_get_hashed_nonce, {"token": token}), (invite_2b_greeter_send_nonce, {"token": token, "greeter_nonce": b"<greeter_nonce>"}), (invite_3a_greeter_wait_peer_trust, {"token": token}), (invite_3b_greeter_signify_trust, {"token": token}), (invite_4_greeter_communicate, {"token": token, "payload": b"<payload>"}), ]: async with real_clock_timeout(): rep = await command(alice_backend_sock, **params) assert rep == {"status": status}
def test_good_invited_handshake(coolorg, invitation_type): organization_id = OrganizationID("Org") token = InvitationToken.new() sh = ServerHandshake() ch = InvitedClientHandshake( organization_id=organization_id, invitation_type=invitation_type, token=token ) assert sh.state == "stalled" challenge_req = sh.build_challenge_req() assert sh.state == "challenge" answer_req = ch.process_challenge_req(challenge_req) sh.process_answer_req(answer_req) assert sh.state == "answer" assert sh.answer_type == HandshakeType.INVITED assert sh.answer_data == { "client_api_version": API_VERSION, "organization_id": organization_id, "invitation_type": invitation_type, "token": token, } result_req = sh.build_result_req() assert sh.state == "result" ch.process_result_req(result_req) assert sh.client_api_version == API_VERSION
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 list(self, organization_id: OrganizationID, greeter: UserID) -> List[Invitation]: async with self.dbh.pool.acquire() as conn: rows = await conn.fetch( *_q_list_invitations( organization_id=organization_id.str, greeter_user_id=greeter.str ) ) invitations_with_claimer_online = self._claimers_ready[organization_id] invitations = [] for ( token_uuid, type, greeter, greeter_human_handle_email, greeter_human_handle_label, claimer_email, created_on, deleted_on, deleted_reason, ) in rows: token = InvitationToken(token_uuid) greeter_human_handle = None if greeter_human_handle_email: greeter_human_handle = HumanHandle( email=greeter_human_handle_email, label=greeter_human_handle_label ) if deleted_on: status = InvitationStatus.DELETED elif token in invitations_with_claimer_online: status = InvitationStatus.READY else: status = InvitationStatus.IDLE invitation: Invitation if type == InvitationType.USER.value: invitation = UserInvitation( greeter_user_id=UserID(greeter), greeter_human_handle=greeter_human_handle, claimer_email=claimer_email, token=token, created_on=created_on, status=status, ) else: # Device invitation = DeviceInvitation( greeter_user_id=UserID(greeter), greeter_human_handle=greeter_human_handle, token=token, created_on=created_on, status=status, ) invitations.append(invitation) return invitations
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=InvitationToken.new(), ) 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 test_get_redirect_invitation(backend_http_send, running_backend, backend_addr): invitation_addr = BackendInvitationAddr.build( backend_addr=backend_addr, organization_id=OrganizationID("Org"), invitation_type=InvitationType.USER, token=InvitationToken.new(), ) # TODO: should use invitation_addr.to_redirection_url() when available ! *_, target = invitation_addr.to_url().split("/") status, headers, body = await backend_http_send(f"/redirect/{target}") assert status == (302, "Found") location_addr = BackendInvitationAddr.from_url(headers["location"]) assert location_addr == invitation_addr
async def test_invited_handshake_bad_token(backend, server_factory, coolorg, invitation_type): ch = InvitedClientHandshake( organization_id=coolorg.organization_id, invitation_type=invitation_type, token=InvitationToken.new(), ) async with server_factory(backend.handle_client) as server: stream = await server.connection_factory() transport = await Transport.init_for_client(stream, "127.0.0.1") challenge_req = await transport.recv() answer_req = ch.process_challenge_req(challenge_req) await transport.send(answer_req) result_req = await transport.recv() with pytest.raises(HandshakeBadIdentity): ch.process_result_req(result_req)
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)
def _from_url_parse_and_consume_params(cls, params): kwargs = super()._from_url_parse_and_consume_params(params) value = params.pop("action", ()) if len(value) != 1: raise ValueError("Missing mandatory `action` param") if value[0] == "claim_user": kwargs["invitation_type"] = InvitationType.USER elif value[0] == "claim_device": kwargs["invitation_type"] = InvitationType.DEVICE else: raise ValueError( "Expected `action=claim_user` or `action=claim_device` param value" ) value = params.pop("token", ()) if len(value) != 1: raise ValueError("Missing mandatory `token` param") try: kwargs["token"] = InvitationToken.from_hex(value[0]) except ValueError: raise ValueError("Invalid `token` param value") return kwargs