Example #1
0
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
Example #3
0
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)
Example #4
0
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)
Example #5
0
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)
Example #6
0
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)
Example #7
0
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}
Example #9
0
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
Example #10
0
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")
Example #11
0
    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
Example #12
0
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"
Example #13
0
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
Example #14
0
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)
Example #15
0
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)
Example #16
0
    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