async def test_handshake_unknown_device(running_backend, alice, mallory): with pytest.raises(BackendConnectionRefused) as exc: async with apiv1_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_backend_closed_cmds(running_backend, alice): async with apiv1_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(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 apiv1_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_backend_switch_offline(running_backend, alice): async with apiv1_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 test_authenticated_cmds_has_right_methods(running_backend, alice): async with apiv1_backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key) as cmds: for method_name in APIV1_AUTHENTICATED_CMDS: assert hasattr(cmds, method_name) for method_name in ALL_CMDS - APIV1_AUTHENTICATED_CMDS: assert not hasattr(cmds, method_name)
async def test_handshake_unknown_organization(running_backend, alice): unknown_org_addr = BackendOrganizationAddr.build( backend_addr=alice.organization_addr, organization_id="dummy", root_verify_key=alice.organization_addr.root_verify_key, ) with pytest.raises(BackendConnectionRefused) as exc: async with apiv1_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 apiv1_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_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 apiv1_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 apiv1_alice_backend_cmds(running_backend, alice): async with apiv1_backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key ) as cmds: yield cmds
async def invite_and_create_user( device: LocalDevice, user_id: UserID, token: str, is_admin: bool, keepalive: Optional[int] = None, ) -> DeviceID: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError InviteClaimInvalidTokenError """ try: async with apiv1_backend_authenticated_cmds_factory( device.organization_addr, device.device_id, device.signing_key, keepalive=keepalive) as cmds: try: rep = await cmds.user_invite(user_id) if rep["status"] == "timeout": raise InviteClaimTimeoutError() elif rep["status"] != "ok": raise InviteClaimError(f"Cannot invite user: {rep}") try: claim = APIV1_UserClaimContent.decrypt_and_load_for( rep["encrypted_claim"], recipient_privkey=device.private_key) except DataError as exc: raise InviteClaimCryptoError( f"Cannot decrypt user claim info: {exc}") from exc if claim.token != token: raise InviteClaimInvalidTokenError( f"Invalid claim token provided by peer: `{claim.token}`" f" (was expecting `{token}`)") device_id = claim.device_id now = pendulum.now() try: user_certificate = UserCertificateContent( author=device.device_id, timestamp=now, user_id=device_id.user_id, public_key=claim.public_key, profile=UserProfile.ADMIN if is_admin else UserProfile.STANDARD, ).dump_and_sign(device.signing_key) device_certificate = DeviceCertificateContent( author=device.device_id, timestamp=now, device_id=device_id, verify_key=claim.verify_key, ).dump_and_sign(device.signing_key) except DataError as exc: raise InviteClaimError( f"Cannot generate user&first device certificates: {exc}" ) from exc except: # Cancel the invitation to prevent the claiming peer from # waiting us until timeout deadline = trio.current_time() + CANCEL_INVITATION_MAX_WAIT with trio.CancelScope(shield=True, deadline=deadline): try: await cmds.user_cancel_invitation(user_id) except BackendConnectionError: pass raise rep = await cmds.user_create(user_certificate, device_certificate) if rep["status"] != "ok": raise InviteClaimError(f"Cannot create user: {rep}") except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot create user: {exc}") from exc return device_id
async def invite_and_create_device(device: LocalDevice, new_device_name: DeviceName, token: str, keepalive: Optional[int] = None) -> None: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError InviteClaimInvalidTokenError """ try: async with apiv1_backend_authenticated_cmds_factory( device.organization_addr, device.device_id, device.signing_key, keepalive=keepalive) as cmds: try: rep = await cmds.device_invite(new_device_name) if rep["status"] != "ok": raise InviteClaimError(f"Cannot invite device: {rep}") try: claim = APIV1_DeviceClaimContent.decrypt_and_load_for( rep["encrypted_claim"], recipient_privkey=device.private_key) except DataError as exc: raise InviteClaimCryptoError( f"Cannot decrypt device claim info: {exc}") from exc if claim.token != token: raise InviteClaimInvalidTokenError( f"Invalid claim token provided by peer: `{claim.token}`" f" (was expecting `{token}`)") try: now = pendulum.now() device_certificate = DeviceCertificateContent( author=device.device_id, timestamp=now, device_id=claim.device_id, verify_key=claim.verify_key, ).dump_and_sign(device.signing_key) except DataError as exc: raise InviteClaimError( f"Cannot generate device certificate: {exc}") from exc try: encrypted_answer = APIV1_DeviceClaimAnswerContent( private_key=device.private_key, user_manifest_id=device.user_manifest_id, user_manifest_key=device.user_manifest_key, ).dump_and_encrypt_for( recipient_pubkey=claim.answer_public_key) except DataError as exc: raise InviteClaimError( f"Cannot generate user claim answer message: {exc}" ) from exc except: # Cancel the invitation to prevent the claiming peer from # waiting us until timeout. deadline = trio.current_time() + CANCEL_INVITATION_MAX_WAIT with trio.CancelScope(shield=True, deadline=deadline): try: await cmds.device_cancel_invitation(new_device_name) except BackendConnectionError: pass raise rep = await cmds.device_create(device_certificate, encrypted_answer) if rep["status"] != "ok": raise InviteClaimError(f"Cannot create device: {rep}") except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot create device: {exc}") from exc
def _cmds_factory(keepalive): return apiv1_backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key, keepalive=keepalive )
async def test_ping(running_backend, alice): async with apiv1_backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key) as cmds: rep = await cmds.ping("Hello World !") assert rep == {"status": "ok", "pong": "Hello World !"}
async def test_events_listen_wait_has_watchdog(monkeypatch, mock_clock, running_backend, alice): # Spy on the transport events to detect the Pings/Pongs # (Note we are talking about websocket ping, not our own higher-level ping api) transport_events_sender, transport_events_receiver = trio.open_memory_channel( 100) _vanilla_next_ws_event = Transport._next_ws_event async def _mocked_next_ws_event(transport): event = await _vanilla_next_ws_event(transport) await transport_events_sender.send((transport, event)) return event monkeypatch.setattr(Transport, "_next_ws_event", _mocked_next_ws_event) async def next_ping_related_event(): while True: transport, event = await transport_events_receiver.receive() if isinstance(event, (Ping, Pong)): return (transport, event) # Highjack the backend api to control the wait time and the final # event that will be returned to the client backend_received_cmd = trio.Event() backend_client_ctx = None vanilla_api_events_listen = running_backend.backend.apis[ APIV1_HandshakeType.AUTHENTICATED]["events_listen"] async def _mocked_api_events_listen(client_ctx, msg): nonlocal backend_client_ctx backend_client_ctx = client_ctx backend_received_cmd.set() return await vanilla_api_events_listen(client_ctx, msg) running_backend.backend.apis[APIV1_HandshakeType.AUTHENTICATED][ "events_listen"] = _mocked_api_events_listen events_listen_rep = None async with apiv1_backend_authenticated_cmds_factory( alice.organization_addr, alice.device_id, alice.signing_key, keepalive=2) as cmds: mock_clock.rate = 1 async with trio.open_service_nursery() as nursery: async def _cmd(): nonlocal events_listen_rep events_listen_rep = await cmds.events_listen(wait=True) nursery.start_soon(_cmd) # Wait for the connection to be established with the backend with trio.fail_after(1): await backend_received_cmd.wait() # Now advance time until ping is requested await trio.testing.wait_all_tasks_blocked() mock_clock.jump(2) with trio.fail_after(2): backend_transport, event = await next_ping_related_event() assert isinstance(event, Ping) client_transport, event = await next_ping_related_event() assert isinstance(event, Pong) assert client_transport is not backend_transport # Wait for another ping, just to be sure... await trio.testing.wait_all_tasks_blocked() mock_clock.jump(2) with trio.fail_after(1): backend_transport2, event = await next_ping_related_event() assert isinstance(event, Ping) assert backend_transport is backend_transport2 client_transport2, event = await next_ping_related_event() assert isinstance(event, Pong) assert client_transport is client_transport2 await backend_client_ctx.send_events_channel.send({ "event": "pinged", "ping": "foo" }) assert events_listen_rep == { "status": "ok", "event": "pinged", "ping": "foo" }
async def test_organization_expired(running_backend, alice, expiredorg): with pytest.raises(BackendConnectionRefused) as exc: async with apiv1_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"