Enable correct Diaspora relayable behaviour

Store the original object when signing with parent, then use that for sending, not serializing our entity object.

This fixes relayable support broken with the new Diaspora protocol.
merge-requests/130/head
Jason Robinson 2017-07-21 22:43:30 +03:00
rodzic 5b04e5ea84
commit 10fa2cf846
10 zmienionych plików z 82 dodań i 24 usunięć

Wyświetl plik

@ -2,6 +2,9 @@
## [unreleased] ## [unreleased]
### Backwards incompatible changes
* When processing Diaspora payloads, entity used to get a `_source_object` stored to it. This was an `etree.Element` created from the source object. Due to serialization issues in applications (for example pushing the object to a task queue or saving to database), `_source_object` is now a byte string representation for the element done with `etree.tostring()`.
### Added ### Added
* New style Diaspora private encrypted JSON payloads are now supported in the receiving side. Outbound private Diaspora payloads are still sent as legacy encrypted payloads. ([issue](https://github.com/jaywink/federation/issues/83)) * New style Diaspora private encrypted JSON payloads are now supported in the receiving side. Outbound private Diaspora payloads are still sent as legacy encrypted payloads. ([issue](https://github.com/jaywink/federation/issues/83))
* No additional changes need to be made when calling `handle_receive` from your task processing. Just pass in the full received XML or JSON payload as a string with recipient user object as before. * No additional changes need to be made when calling `handle_receive` from your task processing. Just pass in the full received XML or JSON payload as a string with recipient user object as before.
@ -12,6 +15,8 @@
* Correctly extend entity `_children`. Certain Diaspora payloads caused `_children` for an entity to be written over by an empty list, causing for example status message photos to not be saved. Correctly do an extend on it. ([issue](https://github.com/jaywink/federation/issues/89)) * Correctly extend entity `_children`. Certain Diaspora payloads caused `_children` for an entity to be written over by an empty list, causing for example status message photos to not be saved. Correctly do an extend on it. ([issue](https://github.com/jaywink/federation/issues/89))
* Fix parsing Diaspora profile `tag_string` into `Profile.tag_list` if the `tag_string` is an empty string. This caused the whole `Profile` object creation to fail. ([issue](https://github.com/jaywink/federation/issues/88)) * Fix parsing Diaspora profile `tag_string` into `Profile.tag_list` if the `tag_string` is an empty string. This caused the whole `Profile` object creation to fail. ([issue](https://github.com/jaywink/federation/issues/88))
* Fix processing Diaspora payload if it is passed to `handle_receive` as a `bytes` object. ([issue](https://github.com/jaywink/federation/issues/91)) * Fix processing Diaspora payload if it is passed to `handle_receive` as a `bytes` object. ([issue](https://github.com/jaywink/federation/issues/91))
* Fix broken Diaspora relayables after latest 0.2.0 protocol changes. Previously relayables worked only because they were reverse engineered from the legacy protocol. Now that XML order is not important and tag names can be different depending on which protocol version, the relayable forwarding broke. To fix, we don't regenerate the entity when forwarding it but store the original received object when generating a `parent_author_signature` (which is optional in some cases, but we generate it anyway for now). This happens in the previously existing `entity.sign_with_parent()` method. In the sending part, if the original received object (now with a parent author signature) exists in the entity, we send that to the remote instead of serializing the entity to XML.
* To forward a relayable you must call `entity.sign_with_parent()` before calling `handle_send` to send the entity.
### Removed ### Removed
* `Post.photos` entity attribute was never used by any code and has been removed. Child entities of type `Image` are stored in the `Post._children` as before. * `Post.photos` entity attribute was never used by any code and has been removed. Child entities of type `Image` are stored in the `Post._children` as before.

Wyświetl plik

@ -9,11 +9,12 @@ __all__ = (
) )
class BaseEntity(object): class BaseEntity:
_allowed_children = () _allowed_children = ()
# If we have a receiver for a private payload, store receiving user guid here # If we have a receiver for a private payload, store receiving user guid here
_receiving_guid = "" _receiving_guid = ""
_source_protocol = "" _source_protocol = ""
# Contains the original object from payload as a string
_source_object = None _source_object = None
_sender_key = "" _sender_key = ""
signature = "" signature = ""
@ -92,7 +93,11 @@ class BaseEntity(object):
pass pass
def sign(self, private_key): def sign(self, private_key):
"""Implement in subclasses.""" """Implement in subclasses if needed."""
pass
def sign_with_parent(self, private_key):
"""Implement in subclasses if needed."""
pass pass

Wyświetl plik

@ -1,13 +1,16 @@
from lxml import etree from lxml import etree
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction, BaseEntity, Follow from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction, BaseEntity, Follow
from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes, add_element_to_doc
from federation.exceptions import SignatureVerificationError from federation.exceptions import SignatureVerificationError
from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature
from federation.utils.diaspora import retrieve_and_parse_profile from federation.utils.diaspora import retrieve_and_parse_profile
class DiasporaEntityMixin(BaseEntity): class DiasporaEntityMixin(BaseEntity):
# Normally outbound document is generated from entity. Store one here if at some point we already have a doc
outbound_doc = None
def to_xml(self): def to_xml(self):
"""Override in subclasses.""" """Override in subclasses."""
raise NotImplementedError raise NotImplementedError
@ -43,14 +46,18 @@ class DiasporaRelayableMixin(DiasporaEntityMixin):
super()._validate_signatures() super()._validate_signatures()
if not self._sender_key: if not self._sender_key:
raise SignatureVerificationError("Cannot verify entity signature - no sender key available") raise SignatureVerificationError("Cannot verify entity signature - no sender key available")
if not verify_relayable_signature(self._sender_key, self._source_object, self.signature): source_doc = etree.fromstring(self._source_object)
if not verify_relayable_signature(self._sender_key, source_doc, self.signature):
raise SignatureVerificationError("Signature verification failed.") raise SignatureVerificationError("Signature verification failed.")
def sign(self, private_key): def sign(self, private_key):
self.signature = create_relayable_signature(private_key, self.to_xml()) self.signature = create_relayable_signature(private_key, self.to_xml())
def sign_with_parent(self, private_key): def sign_with_parent(self, private_key):
self.parent_signature = create_relayable_signature(private_key, self.to_xml()) doc = etree.fromstring(self._source_object)
signature = create_relayable_signature(private_key, doc)
add_element_to_doc(doc, "parent_author_signature", signature)
self.outbound_doc = doc
class DiasporaComment(DiasporaRelayableMixin, Comment): class DiasporaComment(DiasporaRelayableMixin, Comment):

Wyświetl plik

@ -76,7 +76,7 @@ def element_to_objects(element, sender_key_fetcher=None, user=None):
# Add protocol name # Add protocol name
entity._source_protocol = "diaspora" entity._source_protocol = "diaspora"
# Save element object to entity for possible later use # Save element object to entity for possible later use
entity._source_object = element entity._source_object = etree.tostring(element)
# Save receiving guid to object # Save receiving guid to object
if user and hasattr(user, "guid"): if user and hasattr(user, "guid"):
entity._receiving_guid = user.guid entity._receiving_guid = user.guid
@ -212,6 +212,9 @@ def get_outbound_entity(entity, private_key):
:returns: Protocol specific entity class instance. :returns: Protocol specific entity class instance.
:raises ValueError: If conversion cannot be done. :raises ValueError: If conversion cannot be done.
""" """
if getattr(entity, "outbound_doc", None):
# If the entity already has an outbound doc, just return the entity as is
return entity
outbound = None outbound = None
cls = entity.__class__ cls = entity.__class__
if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction, if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,

Wyświetl plik

@ -62,3 +62,11 @@ def get_full_xml_representation(entity, private_key):
diaspora_entity = get_outbound_entity(entity, private_key) diaspora_entity = get_outbound_entity(entity, private_key)
xml = diaspora_entity.to_xml() xml = diaspora_entity.to_xml()
return "<XML><post>%s</post></XML>" % etree.tostring(xml).decode("utf-8") return "<XML><post>%s</post></XML>" % etree.tostring(xml).decode("utf-8")
def add_element_to_doc(doc, tag, value):
"""Set text value of an etree.Element of tag, appending a new element with given tag if it doesn't exist."""
element = doc.find(".//%s" % tag)
if element is None:
element = etree.SubElement(doc, tag)
element.text = value

Wyświetl plik

@ -244,7 +244,11 @@ class Protocol(BaseProtocol):
def build_send(self, entity, from_user, to_user=None, *args, **kwargs): def build_send(self, entity, from_user, to_user=None, *args, **kwargs):
"""Build POST data for sending out to remotes.""" """Build POST data for sending out to remotes."""
xml = entity.to_xml() if entity.outbound_doc:
# Use pregenerated outbound document
xml = entity.outbound_doc
else:
xml = entity.to_xml()
self.init_message(xml, from_user.handle, from_user.private_key) self.init_message(xml, from_user.handle, from_user.private_key)
xml = self.create_salmon_envelope(to_user) xml = self.create_salmon_envelope(to_user)
return {'xml': xml} return {'xml': xml}

Wyświetl plik

@ -1,4 +1,3 @@
import datetime
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -93,7 +92,7 @@ class TestEntitiesConvertToXML:
assert etree.tostring(result) == converted assert etree.tostring(result) == converted
class TestDiasporaProfileFillExtraAttributes(): class TestDiasporaProfileFillExtraAttributes:
def test_raises_if_no_handle(self): def test_raises_if_no_handle(self):
attrs = {"foo": "bar"} attrs = {"foo": "bar"}
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -122,17 +121,18 @@ class TestDiasporaRetractionEntityConverters:
class TestDiasporaRelayableMixin: class TestDiasporaRelayableMixin:
def test_signing_comment_works(self): @patch("federation.entities.diaspora.entities.format_dt", side_effect=lambda v: v)
def test_signing_comment_works(self, mock_format_dt):
entity = DiasporaComment( entity = DiasporaComment(
raw_content="raw_content", guid="guid", target_guid="target_guid", handle="handle", raw_content="raw_content", guid="guid", target_guid="target_guid", handle="handle",
created_at=datetime.datetime(2016, 3, 2), created_at="created_at",
) )
entity.sign(get_dummy_private_key()) entity.sign(get_dummy_private_key())
assert entity.signature == "Z7Yh/zvH8oSct+UZhvHHLESd5HmjyC9LOhXqO/Kan4DYVwW3aoIwQWtWDESnjzjdNeBTVale5koGI1wI" \ assert entity.signature == "OWvW/Yxw4uCnx0WDn0n5/B4uhyZ8Pr6h3FZaw8J7PCXyPluOfYXFoHO21bykP8c2aVnuJNHe+lmeAkUC" \
"HGFd1WbaD7h5Fzi2uh4pl8u75ELhN0qTfWsd5hULj6eCkun0ytc2W+cwJAmRzhyxlmCkxwvmUoP4AS7M" \ "/kHnl4yxk/jqe3uroW842OWvsyDRQ11vHxhIqNMjiepFPkZmXX3vqrYYh5FrC/tUsZrEc8hHoOIHXFR2" \
"OVmV/79PkVfyJWp9XcPn0TB4IBifI/i6iA2PBPrczcAnopzmIg7xehqwd7aX/dGaRruAPR9mxDTMrKmd" \ "kGD0gPV+4EEG6pbMNNZ+SBVun0hvruX8iKQVnBdc/+zUI9+T/MZmLyqTq/CvuPxDyHzQPSHi68N9rJyr" \
"w8cuLarcMfHQTU5lu9Py2kCie+kGYbg7O92khaQdZrLkly1i2tyLZGpC6uFdGXYOYfLcZ7e2aOWHnwzp" \ "4Xa1K+R33Xq8eHHxs8LVNRqzaHGeD3DX8yBu/vP9TYmZsiWlymbuGwLCa4Yfv/VS1hQZovhg6YTxV4CR" \
"QxbyIb7jhjSWf9i97GTtAA==" "v4ToGL+CAJ7UHEugRRBwDw=="
def test_signing_like_works(self): def test_signing_like_works(self):
entity = DiasporaLike(guid="guid", target_guid="target_guid", handle="handle") entity = DiasporaLike(guid="guid", target_guid="target_guid", handle="handle")
@ -154,13 +154,11 @@ class TestDiasporaRelayableEntityValidate():
def test_calls_verify_signature(self, mock_verify): def test_calls_verify_signature(self, mock_verify):
entity = DiasporaComment() entity = DiasporaComment()
entity._sender_key = "key" entity._sender_key = "key"
entity._source_object = "obj" entity._source_object = "<obj></obj>"
entity.signature = "sig" entity.signature = "sig"
mock_verify.return_value = False mock_verify.return_value = False
with pytest.raises(SignatureVerificationError): with pytest.raises(SignatureVerificationError):
entity._validate_signatures() entity._validate_signatures()
mock_verify.assert_called_once_with("key", "obj", "sig")
mock_verify.reset_mock() mock_verify.reset_mock()
mock_verify.return_value = True mock_verify.return_value = True
entity._validate_signatures() entity._validate_signatures()
mock_verify.assert_called_once_with("key", "obj", "sig")

Wyświetl plik

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from lxml import etree
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
import pytest import pytest
@ -196,6 +197,12 @@ class TestDiasporaEntityMappersReceive():
entities = message_to_objects(DIASPORA_POST_SIMPLE) entities = message_to_objects(DIASPORA_POST_SIMPLE)
assert entities[0]._source_protocol == "diaspora" assert entities[0]._source_protocol == "diaspora"
@patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures")
def test_source_object(self, mock_validate):
entities = message_to_objects(DIASPORA_POST_COMMENT, sender_key_fetcher=Mock())
entity = entities[0]
assert entity._source_object == etree.tostring(etree.fromstring(DIASPORA_POST_COMMENT))
@patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures") @patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures")
def test_element_to_objects_calls_sender_key_fetcher(self, mock_validate): def test_element_to_objects_calls_sender_key_fetcher(self, mock_validate):
mock_fetcher = Mock() mock_fetcher = Mock()

Wyświetl plik

@ -2,12 +2,14 @@ import datetime
import re import re
import arrow import arrow
from lxml import etree
from federation.entities.base import Post from federation.entities.base import Post
from federation.entities.diaspora.utils import get_base_attributes, get_full_xml_representation, format_dt from federation.entities.diaspora.utils import (
get_base_attributes, get_full_xml_representation, format_dt, add_element_to_doc)
class TestGetBaseAttributes(): class TestGetBaseAttributes:
def test_get_base_attributes_returns_only_intended_attributes(self): def test_get_base_attributes_returns_only_intended_attributes(self):
entity = Post() entity = Post()
attrs = get_base_attributes(entity).keys() attrs = get_base_attributes(entity).keys()
@ -17,7 +19,7 @@ class TestGetBaseAttributes():
} }
class TestGetFullXMLRepresentation(): class TestGetFullXMLRepresentation:
def test_returns_xml_document(self): def test_returns_xml_document(self):
entity = Post() entity = Post()
document = get_full_xml_representation(entity, "") document = get_full_xml_representation(entity, "")
@ -27,7 +29,26 @@ class TestGetFullXMLRepresentation():
"<provider_display_name></provider_display_name></status_message></post></XML>" "<provider_display_name></provider_display_name></status_message></post></XML>"
class TestFormatDt(): class TestFormatDt:
def test_formatted_string_returned_from_tz_aware_datetime(self): def test_formatted_string_returned_from_tz_aware_datetime(self):
dt = arrow.get(datetime.datetime(2017, 1, 28, 3, 2, 3), "Europe/Helsinki").datetime dt = arrow.get(datetime.datetime(2017, 1, 28, 3, 2, 3), "Europe/Helsinki").datetime
assert format_dt(dt) == "2017-01-28T01:02:03Z" assert format_dt(dt) == "2017-01-28T01:02:03Z"
def test_add_element_to_doc():
# Replacing value
doc = etree.fromstring("<comment><text>foobar</text><parent_author_signature>barfoo</parent_author_signature>"
"</comment>")
add_element_to_doc(doc, "parent_author_signature", "newsig")
assert etree.tostring(doc) == b"<comment><text>foobar</text><parent_author_signature>newsig" \
b"</parent_author_signature></comment>"
# Adding value to an empty tag
doc = etree.fromstring("<comment><text>foobar</text><parent_author_signature /></comment>")
add_element_to_doc(doc, "parent_author_signature", "newsig")
assert etree.tostring(doc) == b"<comment><text>foobar</text><parent_author_signature>newsig" \
b"</parent_author_signature></comment>"
# Adding missing tag
doc = etree.fromstring("<comment><text>foobar</text></comment>")
add_element_to_doc(doc, "parent_author_signature", "newsig")
assert etree.tostring(doc) == b"<comment><text>foobar</text><parent_author_signature>newsig" \
b"</parent_author_signature></comment>"

Wyświetl plik

@ -151,7 +151,7 @@ class TestDiasporaProtocol(DiasporaTestBase):
mock_create_salmon.return_value = "xmldata" mock_create_salmon.return_value = "xmldata"
protocol = self.init_protocol() protocol = self.init_protocol()
mock_entity_xml = Mock() mock_entity_xml = Mock()
entity = Mock(to_xml=Mock(return_value=mock_entity_xml)) entity = Mock(to_xml=Mock(return_value=mock_entity_xml), outbound_doc=None)
from_user = Mock(handle="foobar", private_key="barfoo") from_user = Mock(handle="foobar", private_key="barfoo")
data = protocol.build_send(entity, from_user) data = protocol.build_send(entity, from_user)
mock_init_message.assert_called_once_with(mock_entity_xml, from_user.handle, from_user.private_key) mock_init_message.assert_called_once_with(mock_entity_xml, from_user.handle, from_user.private_key)