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_organization_file_link_addr_init(): from parsec.core.types.backend_address import ( _PyBackendOrganizationFileLinkAddr, _RsBackendOrganizationFileLinkAddr, BackendOrganizationFileLinkAddr, ) from parsec.api.data import EntryID assert BackendOrganizationFileLinkAddr is _RsBackendOrganizationFileLinkAddr WORKSPACE_ID = uuid4() py_ba = _PyBackendOrganizationFileLinkAddr( OrganizationID("MyOrg"), workspace_id=EntryID(WORKSPACE_ID), encrypted_path=b"/", hostname="parsec.cloud", ) rs_ba = BackendOrganizationFileLinkAddr( OrganizationID("MyOrg"), workspace_id=EntryID(WORKSPACE_ID), encrypted_path=b"/", hostname="parsec.cloud", ) _check_equal_backend_organization_file_link_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)
def test_backend_organization_bootstrap_addr_init(): from parsec.core.types.backend_address import ( _PyBackendOrganizationBootstrapAddr, _RsBackendOrganizationBootstrapAddr, BackendOrganizationBootstrapAddr, ) assert BackendOrganizationBootstrapAddr is _RsBackendOrganizationBootstrapAddr token = "1234ABCD" py_ba = _PyBackendOrganizationBootstrapAddr(OrganizationID("MyOrg"), token=token, hostname="parsec.cloud") rs_ba = BackendOrganizationBootstrapAddr(OrganizationID("MyOrg"), token=token, hostname="parsec.cloud") _check_equal_backend_organization_bootstrap_addrs(rs_ba, py_ba) py_ba = _PyBackendOrganizationBootstrapAddr(OrganizationID("MyOrg"), token="", hostname="parsec.cloud") rs_ba = BackendOrganizationBootstrapAddr(OrganizationID("MyOrg"), token="", hostname="parsec.cloud") _check_equal_backend_organization_bootstrap_addrs(rs_ba, py_ba)
async def _http_api_organization_config(self, req: HTTPRequest, **kwargs: str) -> HTTPResponse: if req.method == "GET": error_rep, _ = await self._api_check_auth_and_load_body( req, organization_config_req_serializer) if error_rep: return error_rep try: organization_id = OrganizationID(kwargs["organization_id"]) except ValueError: return HTTPResponse.build_rest(404, {"error": "not_found"}) try: organization = await self._organization_component.get( id=organization_id) except OrganizationNotFoundError: return HTTPResponse.build_rest(404, {"error": "not_found"}) return HTTPResponse.build_rest( 200, organization_config_rep_serializer.dump({ "is_bootstrapped": organization.is_bootstrapped(), "is_expired": organization.is_expired, "user_profile_outsider_allowed": organization.user_profile_outsider_allowed, "active_users_limit": organization.active_users_limit, }), ) else: assert req.method == "PATCH" error_rep, data = await self._api_check_auth_and_load_body( req, organization_update_req_serializer) if error_rep: return error_rep try: organization_id = OrganizationID(kwargs["organization_id"]) except ValueError: return HTTPResponse.build_rest(404, {"error": "not_found"}) try: await self._organization_component.update(id=organization_id, **data) except OrganizationNotFoundError: return HTTPResponse.build_rest(404, {"error": "not_found"}) return HTTPResponse.build_rest( 200, organization_update_rep_serializer.dump({}))
def test_organization_id_user_id_and_device_name(raw): organization_id = OrganizationID(raw) assert str(organization_id) == raw assert organization_id == OrganizationID(raw) user_id = UserID(raw) assert str(user_id) == raw assert user_id == UserID(raw) device_name = DeviceName(raw) assert str(device_name) == raw assert device_name == DeviceName(raw)
def test_backend_pki_enrollment_addr_init(): from parsec.core.types.backend_address import ( _PyBackendPkiEnrollmentAddr, _RsBackendPkiEnrollmentAddr, BackendPkiEnrollmentAddr, ) assert BackendPkiEnrollmentAddr is _RsBackendPkiEnrollmentAddr py_ba = _PyBackendPkiEnrollmentAddr(OrganizationID("MyOrg"), hostname="parsec.cloud") rs_ba = BackendPkiEnrollmentAddr(OrganizationID("MyOrg"), hostname="parsec.cloud") _check_equal_backend_pki_enrollment_addrs(rs_ba, py_ba)
def test_good_invited_handshake(coolorg, invitation_type): organization_id = OrganizationID("Org") token = uuid4() 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_V2_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_V2_VERSION
async def test_organization_create_already_exists_not_bootstrapped( backend, backend_rest_send, expired): organization_id = OrganizationID("NewOrg") original_bootstrap_token = "123" await backend.organization.create(id=organization_id, bootstrap_token=original_bootstrap_token) if expired: await backend.organization.update(id=organization_id, is_expired=True) status, _, body = await backend_rest_send( f"/administration/organizations", method="POST", body={"organization_id": str(organization_id)}, ) assert (status, body) == ((200, "OK"), {"bootstrap_token": ANY}) # Token should be regenerated each time, and the configuration should be overwritten assert body["bootstrap_token"] != original_bootstrap_token org = await backend.organization.get(id=organization_id) assert org == Organization( organization_id=organization_id, bootstrap_token=body["bootstrap_token"], is_expired=False, root_verify_key=None, user_profile_outsider_allowed=True, active_users_limit=None, )
async def invitation_organization_link(running_backend): org_id = OrganizationID("ShinraElectricPowerCompany") org_token = "123" await running_backend.backend.organization.create(org_id, org_token) return str( BackendOrganizationBootstrapAddr.build(running_backend.addr, org_id, org_token))
async def test_handshake_incompatible_version(backend, server_factory): async with server_factory(backend.handle_client) as server: stream = server.connection_factory() transport = await Transport.init_for_client(stream, server.addr.hostname) incompatible_version = ApiVersion(API_VERSION.version + 1, 0) await transport.recv() # Get challenge req = { "handshake": "answer", "type": "anonymous", "client_api_version": incompatible_version, "organization_id": OrganizationID("Org"), "token": "whatever", } await transport.send(packb(req)) result_req = await transport.recv() assert unpackb(result_req) == { "handshake": "result", "result": "bad_protocol", "help": "No overlap between client API versions {3.0} and backend API versions {" + str(API_VERSION) + ", 1.3}", }
async def test_link_file_unknown_org( aqtbot, core_config, gui_factory, autoclose_dialog, running_backend, alice ): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) # Cheating a bit but it does not matter, we just want a link that appears valid with # an unknown organization org_addr = BackendOrganizationAddr.build( running_backend.addr, OrganizationID("UnknownOrg"), alice.organization_addr.root_verify_key ) file_link = BackendOrganizationFileLinkAddr.build( organization_addr=org_addr, workspace_id=EntryID.new(), encrypted_path=b"<whatever>" ) gui = await gui_factory(core_config=core_config, start_arg=file_link.to_url()) lw = gui.test_get_login_widget() assert len(autoclose_dialog.dialogs) == 1 assert autoclose_dialog.dialogs[0][0] == "Error" assert autoclose_dialog.dialogs[0][1] == translate( "TEXT_FILE_LINK_NOT_IN_ORG_organization" ).format(organization="UnknownOrg") def _devices_listed(): assert lw.widget.layout().count() > 0 await aqtbot.wait_until(_devices_listed) accounts_w = lw.widget.layout().itemAt(0).widget() assert accounts_w assert isinstance(accounts_w, LoginPasswordInputWidget)
def test_bad_organization_id_user_id_and_device_name(raw): with pytest.raises(ValueError): OrganizationID(raw) with pytest.raises(ValueError): UserID(raw) with pytest.raises(ValueError): DeviceName(raw)
def _on_validate_clicked(self): backend_addr = None org_id = None device_name = None human_handle = None if self.start_addr: backend_addr = self.start_addr else: try: org_id = OrganizationID( self.user_widget.line_edit_org_name.text()) except ValueError as exc: show_error(self, _("TEXT_ORG_WIZARD_INVALID_ORGANIZATION_ID"), exception=exc) return try: backend_addr = BackendOrganizationBootstrapAddr.build( backend_addr=self.user_widget.backend_addr if self.user_widget.radio_use_custom.isChecked() else self.config.preferred_org_creation_backend_addr, organization_id=org_id, ) except ValueError as exc: show_error(self, _("TEXT_ORG_WIZARD_INVALID_BACKEND_ADDR"), exception=exc) return try: device_name = DeviceName( self.device_widget.line_edit_device.text()) except ValueError as exc: show_error(self, _("TEXT_ORG_WIZARD_INVALID_DEVICE_NAME"), exception=exc) return try: user_name = validators.trim_user_name( self.user_widget.line_edit_user_full_name.text()) human_handle = HumanHandle( self.user_widget.line_edit_user_email.text(), user_name) except ValueError as exc: show_error(self, _("TEXT_ORG_WIZARD_INVALID_HUMAN_HANDLE"), exception=exc) return self.create_job = self.jobs_ctx.submit_job( ThreadSafeQtSignal(self, "req_success"), ThreadSafeQtSignal(self, "req_error"), _do_create_org, config=self.config, human_handle=human_handle, device_name=device_name, password=self.device_widget.password, backend_addr=backend_addr, ) self.button_validate.setEnabled(False)
async def test_organization_update_ok(backend, backend_rest_send, coolorg, bootstrapped): if not bootstrapped: organization_id = OrganizationID("NewOrg") await backend.organization.create(id=organization_id, bootstrap_token="123") else: organization_id = coolorg.organization_id with backend.event_bus.listen() as spy: status, _, body = await backend_rest_send( f"/administration/organizations/{organization_id}", method="PATCH", body={ "user_profile_outsider_allowed": False, "active_users_limit": 10 }, ) assert (status, body) == ((200, "OK"), {}) org = await backend.organization.get(organization_id) assert org.user_profile_outsider_allowed is False assert org.active_users_limit == 10 # Partial update status, _, body = await backend_rest_send( f"/administration/organizations/{organization_id}", method="PATCH", body={"active_users_limit": None}, ) assert (status, body) == ((200, "OK"), {}) org = await backend.organization.get(organization_id) assert org.user_profile_outsider_allowed is False assert org.active_users_limit is None # Partial update with unknown field status, _, body = await backend_rest_send( f"/administration/organizations/{organization_id}", method="PATCH", body={"dummy": "whatever"}, ) assert (status, body) == ((200, "OK"), {}) # Empty update status, _, body = await backend_rest_send( f"/administration/organizations/{organization_id}", method="PATCH", body={}) assert (status, body) == ((200, "OK"), {}) org = await backend.organization.get(organization_id) assert org.user_profile_outsider_allowed is False assert org.active_users_limit is None # No BackendEvent.ORGANIZATION_EXPIRED should have occured await trio.testing.wait_all_tasks_blocked() assert spy.events == []
def test_backend_pki_enrollment_addr_build(): from parsec.core.types.backend_address import ( _PyBackendPkiEnrollmentAddr, _RsBackendPkiEnrollmentAddr, BackendPkiEnrollmentAddr, BackendAddr, ) assert BackendPkiEnrollmentAddr is _RsBackendPkiEnrollmentAddr BACKEND_ADDR = BackendAddr.from_url("parsec://parsec.cloud:1337") py_ba = _PyBackendPkiEnrollmentAddr.build(BACKEND_ADDR, OrganizationID("MyOrg")) rs_ba = BackendPkiEnrollmentAddr.build(BACKEND_ADDR, OrganizationID("MyOrg")) _check_equal_backend_pki_enrollment_addrs(rs_ba, py_ba)
def validate(self, string, pos): try: if len(string) == 0: return QValidator.Intermediate, string, pos OrganizationID(string) return QValidator.Acceptable, string, pos except ValueError: return QValidator.Invalid, string, pos
def test_organization_id_user_id_and_device_name(raw): organization_id = OrganizationID(raw) assert organization_id == raw user_id = UserID(raw) assert user_id == raw device_name = DeviceName(raw) assert device_name == raw
def test_list_devices_support_key_file(config_dir, type): if type == "password": data_extra = {"type": "password", "salt": b"12345"} available_device_extra = {"type": DeviceFileType.PASSWORD} elif type == "smartcard": data_extra = { "type": "smartcard", "encrypted_key": b"12345", "certificate_id": "42", "certificate_sha1": b"12345", } available_device_extra = {"type": DeviceFileType.SMARTCARD} # Device information user_id = uuid4().hex device_name = uuid4().hex organization_id = "Org" rvk_hash = (uuid4().hex)[:10] device_id = f"{user_id}@{device_name}" slug = f"{rvk_hash}#{organization_id}#{device_id}" human_label = "Billy Mc BillFace" human_email = "*****@*****.**" device_label = "My device" # Craft file data key_file_data = packb({ "ciphertext": b"whatever", "human_handle": (human_email.encode(), human_label.encode()), "device_label": device_label, "device_id": device_id, "organization_id": organization_id, "slug": slug, **data_extra, }) key_file_path = get_devices_dir(config_dir) / "device.keys" key_file_path.parent.mkdir(parents=True) key_file_path.write_bytes(key_file_data) devices = list_available_devices(config_dir) expected_device = AvailableDevice( key_file_path=key_file_path, organization_id=OrganizationID(organization_id), device_id=DeviceID(device_id), human_handle=HumanHandle(human_email, human_label), device_label=DeviceLabel(device_label), slug=slug, **available_device_extra, ) assert devices == [expected_device] assert get_key_file(config_dir, expected_device) == key_file_path
def _unslug(val): parts = val.split(":") if len(parts) == 1: return (None, DeviceID(val), val) elif len(parts) == 2: raw_org, raw_device_id = parts return (OrganizationID(raw_org), DeviceID(raw_device_id), val) else: raise ValueError( "Must follow format `[<organization>:]<user_id>@<device_name>`")
def test_backend_organization_addr_build(): from parsec.core.types.backend_address import ( _PyBackendOrganizationAddr, _RsBackendOrganizationAddr, BackendOrganizationAddr, BackendAddr, ) assert BackendOrganizationAddr is _RsBackendOrganizationAddr BACKEND_ADDR = BackendAddr.from_url("parsec://parsec.cloud:1337") vk = SigningKey.generate().verify_key py_ba = _PyBackendOrganizationAddr.build(BACKEND_ADDR, OrganizationID("MyOrg"), vk) rs_ba = BackendOrganizationAddr.build(BACKEND_ADDR, OrganizationID("MyOrg"), vk) _check_equal_backend_organization_addrs(rs_ba, py_ba)
def test_backend_organization_addr_init(): from parsec.core.types.backend_address import ( _PyBackendOrganizationAddr, _RsBackendOrganizationAddr, BackendOrganizationAddr, ) assert BackendOrganizationAddr is _RsBackendOrganizationAddr vk = SigningKey.generate().verify_key py_ba = _PyBackendOrganizationAddr(OrganizationID("MyOrg"), vk, hostname="parsec.cloud") rs_ba = BackendOrganizationAddr(OrganizationID("MyOrg"), vk, hostname="parsec.cloud") _check_equal_backend_organization_addrs(rs_ba, py_ba)
async def test_good( running_backend, backend, alice, bob, alice_backend_cmds, user_fs_factory, with_labels ): org_id = OrganizationID("NewOrg") org_token = "123456" await backend.organization.create(org_id, org_token) organization_addr = BackendOrganizationBootstrapAddr.build( running_backend.addr, org_id, org_token ) if with_labels: human_handle = HumanHandle(email="*****@*****.**", label="Zack") device_label = "PC1" else: human_handle = None device_label = None async with apiv1_backend_anonymous_cmds_factory(addr=organization_addr) as cmds: new_device = await bootstrap_organization( cmds, human_handle=human_handle, device_label=device_label ) assert new_device is not None assert new_device.organization_id == org_id assert new_device.device_label == device_label assert new_device.human_handle == human_handle assert new_device.profile == UserProfile.ADMIN # Test the behavior of this new device async with user_fs_factory(new_device, initialize_in_v0=True) as newfs: await newfs.workspace_create("wa") await newfs.sync() # Test the device in correct in the backend backend_user, backend_device = await backend.user.get_user_with_device( org_id, new_device.device_id ) assert backend_user.user_id == new_device.user_id assert backend_user.human_handle == new_device.human_handle assert backend_user.profile == new_device.profile assert backend_user.user_certifier is None if with_labels: assert backend_user.user_certificate != backend_user.redacted_user_certificate else: assert backend_user.user_certificate == backend_user.redacted_user_certificate assert backend_device.device_id == new_device.device_id assert backend_device.device_label == new_device.device_label assert backend_device.device_certifier is None if with_labels: assert backend_device.device_certificate != backend_device.redacted_device_certificate else: assert backend_device.device_certificate == backend_device.redacted_device_certificate
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"
def test_backend_organization_bootstrap_addr_build(): from parsec.core.types.backend_address import ( _PyBackendOrganizationBootstrapAddr, _RsBackendOrganizationBootstrapAddr, BackendOrganizationBootstrapAddr, BackendAddr, ) assert BackendOrganizationBootstrapAddr is _RsBackendOrganizationBootstrapAddr BACKEND_ADDR = BackendAddr.from_url("parsec://parsec.cloud:1337") token = "1234ABCD" py_ba = _PyBackendOrganizationBootstrapAddr.build(BACKEND_ADDR, OrganizationID("MyOrg"), token=token) rs_ba = BackendOrganizationBootstrapAddr.build(BACKEND_ADDR, OrganizationID("MyOrg"), token=token) _check_equal_backend_organization_bootstrap_addrs(rs_ba, py_ba)
async def _http_api_anonymous(self, req: HTTPRequest, **kwargs: str) -> HTTPResponse: # Check whether the organization exists try: organization_id = OrganizationID(kwargs["organization_id"]) await self._organization_component.get(organization_id) except OrganizationNotFoundError: organization_exists = False except ValueError: return HTTPResponse.build_msgpack(404, {}) else: organization_exists = True # Reply to GET if req.method == "GET": status = 200 if organization_exists else 404 return HTTPResponse.build_msgpack(status, {}) # Reply early to POST when the organization doesn't exists if not organization_exists and not self._config.organization_spontaneous_bootstrap: return HTTPResponse.build_msgpack(404, {}) # Get and unpack the body body = await req.get_body() try: msg = unpackb(body) except SerdePackingError: return HTTPResponse.build_msgpack(200, {"status": "invalid_msg_format"}) # Lazy creation of the organization if necessary cmd = msg.get("cmd") if cmd == "organization_bootstrap" and not organization_exists: assert self._config.organization_spontaneous_bootstrap try: await self._organization_component.create(id=organization_id, bootstrap_token="") except OrganizationAlreadyExistsError: pass # Retreive command client_ctx = AnonymousClientContext(organization_id) try: if not isinstance(cmd, str): raise KeyError() cmd_func = self.anonymous_api[cmd] except KeyError: return HTTPResponse.build_msgpack(200, {"status": "unknown_command"}) # Run command rep = await cmd_func(client_ctx, msg) return HTTPResponse.build_msgpack(200, rep)
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_s3_read(caplog): org_id = OrganizationID("org42") block_id = BlockID.from_hex("0694a21176354e8295e28a543e5887f9") def _assert_log(): log = caplog.assert_occured_once("[warning ] Block read error") assert f"organization_id={org_id}" in log assert f"block_id={block_id}" in log assert len(caplog.messages) == 1 caplog.clear() with mock.patch("boto3.client") as client_mock: client_mock.return_value = Mock() client_mock().head_bucket.return_value = True blockstore = S3BlockStoreComponent("europe", "parsec", "john", "secret") # Ok response_mock = Mock() response_mock.read.return_value = "content" client_mock().get_object.return_value = {"Body": response_mock} assert await blockstore.read(org_id, block_id) == "content" client_mock().get_object.assert_called_once_with( Bucket="parsec", Key="org42/0694a211-7635-4e82-95e2-8a543e5887f9") client_mock().get_object.reset_mock() assert not caplog.messages # Not found client_mock().get_object.side_effect = S3ClientError( error_response={"Error": { "Code": "404" }}, operation_name="GET") with pytest.raises(BlockStoreError): assert await blockstore.read(org_id, block_id) _assert_log() # Connection error client_mock().get_object.side_effect = S3EndpointConnectionError( endpoint_url="url") with pytest.raises(BlockStoreError): assert await blockstore.read(org_id, block_id) _assert_log() # Unknown exception client_mock().get_object.side_effect = S3ClientError( error_response={"Error": { "Code": "401" }}, operation_name="GET") with pytest.raises(BlockStoreError): assert await blockstore.read(org_id, block_id) _assert_log()
async def test_invalid_token(running_backend, backend): org_id = OrganizationID("NewOrg") old_token = "123456" new_token = "abcdef" await backend.organization.create(org_id, old_token) await backend.organization.create(org_id, new_token) organization_addr = BackendOrganizationBootstrapAddr.build( running_backend.addr, org_id, old_token) with pytest.raises(InviteNotFoundError): await bootstrap_organization(organization_addr, human_handle=None, device_label=None)
def test_backend_organization_addr_good(base_url, expected, verify_key): org = OrganizationID("org") backend_addr = BackendAddr.from_url(base_url) addr = BackendOrganizationAddr.build(backend_addr, organization_id=org, root_verify_key=verify_key) assert addr.hostname == "foo" assert addr.port == expected["port"] assert addr.use_ssl == expected["ssl"] assert addr.organization_id == org assert addr.root_verify_key == verify_key addr2 = BackendOrganizationAddr.from_url(addr.to_url()) assert addr == addr2