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 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()
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
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")
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"
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")
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")
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
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"
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)
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()
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
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"
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.")
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
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"
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)
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
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")
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
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")
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"
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 _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 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
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
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)
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 !"}