Esempio n. 1
0
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}")
Esempio n. 2
0
async def test_backend_closed_cmds(running_backend, alice):
    async with backend_authenticated_cmds_factory(alice.organization_addr,
                                                  alice.device_id,
                                                  alice.signing_key) as cmds:
        pass
    with pytest.raises(trio.ClosedResourceError):
        await cmds.ping()
Esempio n. 3
0
async def test_backend_disconnect_during_handshake(alice):
    client_answered = False

    async def poorly_serve_client(stream):
        nonlocal client_answered

        transport = await Transport.init_for_server(stream)
        handshake = ServerHandshake()
        await transport.send(handshake.build_challenge_req())
        await transport.recv()
        # Close connection during handshake
        await stream.aclose()

        client_answered = True

    async with trio.open_service_nursery() as nursery:

        listeners = await nursery.start(
            partial(trio.serve_tcp,
                    poorly_serve_client,
                    port=0,
                    host="127.0.0.1"))

        organization_addr = correct_addr(alice.organization_addr,
                                         listeners[0].socket.getsockname()[1])
        with pytest.raises(BackendNotAvailable):
            async with backend_authenticated_cmds_factory(
                    organization_addr, alice.device_id,
                    alice.signing_key) as cmds:
                await cmds.ping()

        nursery.cancel_scope.cancel()

    assert client_answered
Esempio n. 4
0
async def test_invite_claim_device(running_backend, backend, alice):
    new_device_id = DeviceID(f"{alice.user_id}@NewDevice")
    new_device = None
    token = generate_invitation_token()

    async def _from_alice():
        await invite_and_create_device(alice,
                                       new_device_id.device_name,
                                       token=token)

    async def _from_new_device():
        nonlocal new_device
        new_device = await claim_device(alice.organization_addr,
                                        new_device_id,
                                        token=token)

    await _invite_and_claim(running_backend,
                            _from_alice,
                            _from_new_device,
                            event_name="device.claimed")

    # Now connect as the new device
    async with backend_authenticated_cmds_factory(
        new_device.organization_addr, new_device.device_id,
        new_device.signing_key) as cmds:
        await cmds.ping("foo")
Esempio n. 5
0
async def test_handshake_unknown_device(running_backend, alice, mallory):
    with pytest.raises(BackendConnectionRefused) as exc:
        async with backend_authenticated_cmds_factory(
                alice.organization_addr, mallory.device_id,
                mallory.signing_key) as cmds:
            await cmds.ping()
    assert str(exc.value) == "Invalid handshake information"
Esempio n. 6
0
async def test_invite_claim_admin_user(running_backend, backend, alice):
    new_device_id = DeviceID("zack@pc1")
    new_device = None
    token = generate_invitation_token()

    async def _from_alice():
        await invite_and_create_user(alice,
                                     new_device_id.user_id,
                                     token=token,
                                     is_admin=True)

    async def _from_new_device():
        nonlocal new_device
        new_device = await claim_user(alice.organization_addr,
                                      new_device_id,
                                      token=token)

    await _invite_and_claim(running_backend, _from_alice, _from_new_device)

    assert new_device.is_admin

    # Now connect as the new user
    async with backend_authenticated_cmds_factory(
        new_device.organization_addr, new_device.device_id,
        new_device.signing_key) as cmds:
        await cmds.ping("foo")
Esempio n. 7
0
async def _greet_invitation(config, device, token):
    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:
        async with spinner("Retrieving invitation info"):
            rep = await cmds.invite_list()
            if rep["status"] != "ok":
                raise RuntimeError(f"Backend error: {rep}")
        for invitation in rep["invitations"]:
            if invitation["token"] == token:
                break
        else:
            raise RuntimeError(f"Invitation not found")

        if invitation["type"] == InvitationType.USER:
            initial_ctx = UserGreetInitialCtx(cmds=cmds, token=token)
            do_greet = partial(_do_greet_user, device, initial_ctx)
        else:
            assert invitation["type"] == InvitationType.DEVICE
            initial_ctx = DeviceGreetInitialCtx(cmds=cmds, token=token)
            do_greet = partial(_do_greet_device, device, initial_ctx)

        while True:
            try:
                greet_done = await do_greet()
                if greet_done:
                    break
            except InviteError as exc:
                click.secho(str(exc), fg="red")
            click.secho("Restarting the invitation process", fg="red")
Esempio n. 8
0
async def _list_invitations(config, device):
    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_list()
        if rep["status"] != "ok":
            raise RuntimeError(
                f"Backend error while listing invitations: {rep}")
        display_statuses = {
            InvitationStatus.READY: click.style("ready", fg="green"),
            InvitationStatus.IDLE: click.style("idle", fg="yellow"),
            InvitationStatus.DELETED: click.style("deleted", fg="red"),
        }
        for invitation in rep["invitations"]:
            display_status = display_statuses[invitation["status"]]
            display_token = invitation["token"].hex
            if invitation["type"] == InvitationType.USER:
                display_type = f"user (email={invitation['claimer_email']})"
            else:  # Device
                display_type = f"device"
            click.echo(f"{display_token}\t{display_status}\t{display_type}")
        if not rep["invitations"]:
            click.echo("No invitations.")
async def test_backend_disconnect_during_handshake(tcp_stream_spy, alice,
                                                   backend_addr):
    client_answered = False

    async def poorly_serve_client(stream):
        nonlocal client_answered

        transport = await Transport.init_for_server(stream)
        handshake = ServerHandshake()
        await transport.send(handshake.build_challenge_req())
        await transport.recv()
        # Close connection during handshake
        await stream.aclose()

        client_answered = True

    async with trio.open_service_nursery() as nursery:

        async def connection_factory(*args, **kwargs):
            client_stream, server_stream = trio.testing.memory_stream_pair()
            nursery.start_soon(poorly_serve_client, server_stream)
            return client_stream

        with tcp_stream_spy.install_hook(backend_addr, connection_factory):
            with pytest.raises(BackendNotAvailable):
                async with backend_authenticated_cmds_factory(
                        alice.organization_addr, alice.device_id,
                        alice.signing_key) as cmds:
                    await cmds.ping()

        nursery.cancel_scope.cancel()

    assert client_answered
Esempio n. 10
0
async def test_organization_expired(running_backend, alice, expiredorg):
    with pytest.raises(BackendConnectionRefused) as exc:
        async with backend_authenticated_cmds_factory(
            expiredorg.addr, alice.device_id, alice.signing_key
        ) as cmds:
            await cmds.ping()
    assert str(exc.value) == "Trial organization has expired"
Esempio n. 11
0
async def test_authenticated_cmds_has_right_methods(running_backend, alice):
    async with backend_authenticated_cmds_factory(alice.organization_addr,
                                                  alice.device_id,
                                                  alice.signing_key) as cmds:
        for method_name in AUTHENTICATED_CMDS:
            assert hasattr(cmds, method_name)
        for method_name in ALL_CMDS - AUTHENTICATED_CMDS:
            assert not hasattr(cmds, method_name)
Esempio n. 12
0
async def test_backend_switch_offline(running_backend, alice):
    async with backend_authenticated_cmds_factory(alice.organization_addr,
                                                  alice.device_id,
                                                  alice.signing_key) as cmds:
        await cmds.ping()
        with running_backend.offline():
            with pytest.raises(BackendNotAvailable):
                await cmds.ping()
Esempio n. 13
0
 async def _alice_context():
     async with backend_authenticated_cmds_factory(
         alice.organization_addr, alice.device_id, alice.signing_key
     ) as cmds:
         rdm = RemoteDevicesManager(cmds, alice.root_verify_key)
         async with UserFS.run(
             alice, path, cmds, rdm, event_bus, get_prevent_sync_pattern()
         ) as user_fs:
             yield user_fs
Esempio n. 14
0
async def test_organization_expired(running_backend, alice, expiredorg):

    with running_backend.backend.event_bus.listen() as spy:
        with pytest.raises(BackendConnectionRefused) as exc:
            async with backend_authenticated_cmds_factory(
                    expiredorg.addr, alice.device_id,
                    alice.signing_key) as cmds:
                await cmds.ping()
        await spy.wait_with_timeout(BackendEvent.ORGANIZATION_EXPIRED)
    assert str(exc.value) == "Trial organization has expired"
Esempio n. 15
0
async def _cancel_invitation(config, device, token):
    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_delete(token=token, reason=InvitationDeletedReason.CANCELLED)
        if rep["status"] != "ok":
            raise RuntimeError(f"Backend error while cancelling invitation: {rep}")
        click.echo("Invitation deleted.")
Esempio n. 16
0
 async def _user_fs_factory(device, event_bus=None, initialize_in_v0: bool = False):
     event_bus = event_bus or event_bus_factory()
     async with backend_authenticated_cmds_factory(
         device.organization_addr, device.device_id, device.signing_key
     ) as cmds:
         path = local_storage_path(device)
         rdm = RemoteDevicesManager(cmds, device.root_verify_key)
         async with UserFS.run(device, path, cmds, rdm, event_bus) as user_fs:
             if not initialize_in_v0:
                 await initialize_userfs_storage_v1(user_fs.storage)
             yield user_fs
Esempio n. 17
0
async def test_handshake_unknown_organization(running_backend, alice):
    unknown_org_addr = BackendOrganizationAddr.build(
        backend_addr=alice.organization_addr.get_backend_addr(),
        organization_id=OrganizationID("dummy"),
        root_verify_key=alice.organization_addr.root_verify_key,
    )
    with pytest.raises(BackendConnectionRefused) as exc:
        async with backend_authenticated_cmds_factory(
                unknown_org_addr, alice.device_id, alice.signing_key) as cmds:
            await cmds.ping()
    assert str(exc.value) == "Invalid handshake information"
async def test_handshake_rvk_mismatch(running_backend, alice, otherorg):
    bad_rvk_org_addr = BackendOrganizationAddr.build(
        backend_addr=alice.organization_addr,
        organization_id=alice.organization_id,
        root_verify_key=otherorg.root_verify_key,
    )
    with pytest.raises(BackendConnectionRefused) as exc:
        async with backend_authenticated_cmds_factory(
            bad_rvk_org_addr, alice.device_id, alice.signing_key
        ) as cmds:
            await cmds.ping()
    assert str(exc.value) == "Root verify key for organization differs between client and server"
Esempio n. 19
0
async def test_expired_notification_from_connection(aqtbot, running_backend,
                                                    autoclose_dialog,
                                                    expiredorgalice,
                                                    gui_factory, core_config):
    save_device_with_password(core_config.config_dir, expiredorgalice,
                              "P@ssw0rd")
    gui = await gui_factory()
    lw = gui.test_get_login_widget()
    tabw = gui.test_get_tab()

    # Force logging on an expired organization
    with freeze_time("1989-12-17"):

        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]):
            await aqtbot.mouse_click(password_w.button_login,
                                     QtCore.Qt.LeftButton)

        # Assert logged in
        def _notified():
            assert autoclose_dialog.dialogs == []
            central_widget = gui.test_get_central_widget()
            assert central_widget is not None

        await aqtbot.wait_until(_notified)

    # Trigger another handshake
    with pytest.raises(BackendConnectionRefused):
        async with backend_authenticated_cmds_factory(
                expiredorgalice.organization_addr,
                expiredorgalice.device_id,
                expiredorgalice.signing_key,
        ) as cmds:
            async with cmds.acquire_transport():
                # This shall never happen, we shall have been rejected while acquiring the transport
                assert False

    # Assert dialog
    def _expired_notified():
        assert autoclose_dialog.dialogs == [("Error",
                                             "The organization has expired")]

    await aqtbot.wait_until(_expired_notified)
Esempio n. 20
0
    async def _user_fs_factory(device,
                               event_bus=None,
                               data_base_dir=data_base_dir):
        event_bus = event_bus or event_bus_factory()

        async with backend_authenticated_cmds_factory(
                device.organization_addr, device.device_id,
                device.signing_key) as cmds:
            rdm = RemoteDevicesManager(cmds, device.root_verify_key)
            async with UserFS.run(data_base_dir, device, cmds, rdm, event_bus,
                                  get_prevent_sync_pattern()) as user_fs:

                yield user_fs
Esempio n. 21
0
async def test_invite_claim_3_chained_users(running_backend, backend, alice):
    # Zeta will be invited by Zoe, Zoe will be invited by Zack
    new_device_id_1 = DeviceID("zack@pc1")
    new_device_1 = None
    token_1 = generate_invitation_token()
    new_device_id_2 = DeviceID("zoe@pc2")
    new_device_2 = None
    token_2 = generate_invitation_token()
    new_device_id_3 = DeviceID("zeta@pc3")
    new_device_3 = None
    token_3 = generate_invitation_token()

    async def _invite_from_alice():
        await invite_and_create_user(alice, new_device_id_1.user_id, token=token_1, is_admin=True)

    async def _claim_from_1():
        nonlocal new_device_1
        new_device_1 = await claim_user(alice.organization_addr, new_device_id_1, token=token_1)

    async def _invite_from_1():
        await invite_and_create_user(
            new_device_1, new_device_id_2.user_id, token=token_2, is_admin=True
        )

    async def _claim_from_2():
        nonlocal new_device_2
        new_device_2 = await claim_user(alice.organization_addr, new_device_id_2, token=token_2)

    async def _invite_from_2():
        await invite_and_create_user(
            new_device_2, new_device_id_3.user_id, token=token_3, is_admin=False
        )

    async def _claim_from_3():
        nonlocal new_device_3
        new_device_3 = await claim_user(alice.organization_addr, new_device_id_3, token=token_3)

    await _invite_and_claim(running_backend, _invite_from_alice, _claim_from_1)
    await _invite_and_claim(running_backend, _invite_from_1, _claim_from_2)
    await _invite_and_claim(running_backend, _invite_from_2, _claim_from_3)

    assert new_device_1.is_admin
    assert new_device_2.is_admin
    assert not new_device_3.is_admin

    # Now connect as the last user
    async with backend_authenticated_cmds_factory(
        new_device_2.organization_addr, new_device_2.device_id, new_device_2.signing_key
    ) as cmds:
        await cmds.ping("foo")
Esempio n. 22
0
async def _create_new_device_for_self(
        original_device: LocalDevice,
        new_device_label: DeviceLabel) -> LocalDevice:
    """
    Raises:
        BackendConnectionError
    """
    new_device = LocalDevice(
        organization_addr=original_device.organization_addr,
        device_id=DeviceID(f"{original_device.user_id}@{DeviceName.new()}"),
        device_label=new_device_label,
        human_handle=original_device.human_handle,
        profile=original_device.profile,
        private_key=original_device.private_key,
        signing_key=SigningKey.generate(),
        user_manifest_id=original_device.user_manifest_id,
        user_manifest_key=original_device.user_manifest_key,
        local_symkey=SecretKey.generate(),
    )
    now = pendulum_now()

    device_certificate = DeviceCertificateContent(
        author=original_device.device_id,
        timestamp=now,
        device_id=new_device.device_id,
        device_label=new_device.device_label,
        verify_key=new_device.verify_key,
    )
    redacted_device_certificate = device_certificate.evolve(device_label=None)

    device_certificate = device_certificate.dump_and_sign(
        original_device.signing_key)
    redacted_device_certificate = redacted_device_certificate.dump_and_sign(
        original_device.signing_key)

    async with backend_authenticated_cmds_factory(
            addr=original_device.organization_addr,
            device_id=original_device.device_id,
            signing_key=original_device.signing_key,
    ) as cmds:
        rep = await cmds.device_create(
            device_certificate=device_certificate,
            redacted_device_certificate=redacted_device_certificate,
        )

    if rep["status"] != "ok":
        raise BackendConnectionError(f"Cannot create recovery device: {rep}")

    return new_device
Esempio n. 23
0
async def test_invite_claim_multiple_devices_from_chained_user(running_backend, backend, alice):
    # The devices are invited from one another
    new_device_id_1 = DeviceID("zack@pc1")
    new_device_1 = None
    token_1 = generate_invitation_token()

    new_device_id_2 = DeviceID("zack@pc2")
    new_device_2 = None
    token_2 = generate_invitation_token()

    new_device_id_3 = DeviceID("zack@pc3")
    new_device_3 = None
    token_3 = generate_invitation_token()

    async def _invite_from_alice():
        await invite_and_create_user(alice, new_device_id_1.user_id, token=token_1, is_admin=True)

    async def _claim_from_1():
        nonlocal new_device_1
        new_device_1 = await claim_user(alice.organization_addr, new_device_id_1, token=token_1)

    async def _invite_from_1():
        await invite_and_create_device(new_device_1, new_device_id_2.device_name, token=token_2)

    async def _claim_from_2():
        nonlocal new_device_2
        new_device_2 = await claim_device(alice.organization_addr, new_device_id_2, token=token_2)

    async def _invite_from_2():
        await invite_and_create_device(new_device_2, new_device_id_3.device_name, token=token_3)

    async def _claim_from_3():
        nonlocal new_device_3
        new_device_3 = await claim_device(alice.organization_addr, new_device_id_3, token=token_3)

    await _invite_and_claim(running_backend, _invite_from_alice, _claim_from_1)
    await _invite_and_claim(
        running_backend, _invite_from_1, _claim_from_2, event_name="device.claimed"
    )
    await _invite_and_claim(
        running_backend, _invite_from_2, _claim_from_3, event_name="device.claimed"
    )

    # Now connect as the last device
    async with backend_authenticated_cmds_factory(
        new_device_3.organization_addr, new_device_3.device_id, new_device_3.signing_key
    ) as cmds:
        await cmds.ping("foo")
Esempio n. 24
0
async def test_handshake_revoked_device(running_backend, alice, bob):
    revoked_user_certificate = RevokedUserCertificateContent(
        author=alice.device_id, timestamp=pendulum.now(),
        user_id=bob.user_id).dump_and_sign(alice.signing_key)
    await running_backend.backend.user.revoke_user(
        organization_id=alice.organization_id,
        user_id=bob.user_id,
        revoked_user_certificate=revoked_user_certificate,
        revoked_user_certifier=alice.device_id,
    )

    with pytest.raises(BackendConnectionRefused) as exc:
        async with backend_authenticated_cmds_factory(bob.organization_addr,
                                                      bob.device_id,
                                                      bob.signing_key) as cmds:
            await cmds.ping()
    assert str(exc.value) == "Device has been revoked"
Esempio n. 25
0
    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,
                )
Esempio n. 26
0
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}")
Esempio n. 27
0
async def bob_backend_cmds(running_backend, bob):
    async with backend_authenticated_cmds_factory(bob.organization_addr,
                                                  bob.device_id,
                                                  bob.signing_key) as cmds:
        yield cmds
Esempio n. 28
0
async def alice2_backend_cmds(running_backend, alice2):
    async with backend_authenticated_cmds_factory(alice2.organization_addr,
                                                  alice2.device_id,
                                                  alice2.signing_key) as cmds:
        yield cmds
Esempio n. 29
0
 async def _remote_devices_manager_factory(device):
     async with backend_authenticated_cmds_factory(
             device.organization_addr, device.device_id,
             device.signing_key) as cmds:
         yield RemoteDevicesManager(cmds, device.root_verify_key)
Esempio n. 30
0
async def test_user_invite_then_claim_ok(
    backend, running_backend, alice, alice_backend_cmds, mallory
):
    token = "424242"

    async def _alice_invite():
        rep = await alice_backend_cmds.user_invite(mallory.user_id)
        assert rep["status"] == "ok"
        claim = UserClaimContent.decrypt_and_load_for(
            rep["encrypted_claim"], recipient_privkey=alice.private_key
        )

        assert claim.token == token

        now = pendulum.now()
        user_certificate = UserCertificateContent(
            author=alice.device_id,
            timestamp=now,
            user_id=claim.device_id.user_id,
            public_key=claim.public_key,
            is_admin=False,
        ).dump_and_sign(alice.signing_key)
        device_certificate = DeviceCertificateContent(
            author=alice.device_id,
            timestamp=now,
            device_id=claim.device_id,
            verify_key=claim.verify_key,
        ).dump_and_sign(alice.signing_key)
        with trio.fail_after(1):
            rep = await alice_backend_cmds.user_create(user_certificate, device_certificate)
            assert rep["status"] == "ok"

    async def _mallory_claim():
        async with backend_anonymous_cmds_factory(mallory.organization_addr) as cmds:
            rep = await cmds.user_get_invitation_creator(mallory.user_id)
            assert rep["trustchain"] == {"devices": [], "revoked_users": [], "users": []}
            creator = UserCertificateContent.unsecure_load(rep["user_certificate"])
            creator_device = DeviceCertificateContent.unsecure_load(rep["device_certificate"])
            assert creator_device.device_id.user_id == creator.user_id

            encrypted_claim = UserClaimContent(
                device_id=mallory.device_id,
                token=token,
                public_key=mallory.public_key,
                verify_key=mallory.verify_key,
            ).dump_and_encrypt_for(recipient_pubkey=creator.public_key)
            with trio.fail_after(1):
                rep = await cmds.user_claim(mallory.user_id, encrypted_claim)
                assert rep["status"] == "ok"

    with running_backend.backend.event_bus.listen() as spy:
        async with trio.open_service_nursery() as nursery:
            nursery.start_soon(_alice_invite)
            await spy.wait_with_timeout("event.connected", {"event_name": "user.claimed"})
            nursery.start_soon(_mallory_claim)

    # Now mallory should be able to connect to backend
    async with backend_authenticated_cmds_factory(
        mallory.organization_addr, mallory.device_id, mallory.signing_key
    ) as cmds:
        rep = await cmds.ping("Hello World !")
        assert rep == {"status": "ok", "pong": "Hello World !"}