Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Refactor event building into EventBuilder #4481

Merged
merged 8 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/4481.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add infrastructure to support different event formats
13 changes: 1 addition & 12 deletions synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,17 +550,6 @@ def is_server_admin(self, user):
"""
return self.store.is_server_admin(user)

@defer.inlineCallbacks
def add_auth_events(self, builder, context):
prev_state_ids = yield context.get_prev_state_ids(self.store)
auth_ids = yield self.compute_auth_events(builder, prev_state_ids)

auth_events_entries = yield self.store.add_event_hashes(
auth_ids
)

builder.auth_events = auth_events_entries

@defer.inlineCallbacks
def compute_auth_events(self, event, current_state_ids, for_verification=False):
if event.type == EventTypes.Create:
Expand All @@ -577,7 +566,7 @@ def compute_auth_events(self, event, current_state_ids, for_verification=False):
key = (EventTypes.JoinRules, "", )
join_rule_event_id = current_state_ids.get(key)

key = (EventTypes.Member, event.user_id, )
key = (EventTypes.Member, event.sender, )
member_event_id = current_state_ids.get(key)

key = (EventTypes.Create, "", )
Expand Down
16 changes: 6 additions & 10 deletions synapse/crypto/event_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,25 @@ def compute_event_signature(event_dict, signature_name, signing_key):
return redact_json["signatures"]


def add_hashes_and_signatures(event, signature_name, signing_key,
def add_hashes_and_signatures(event_dict, signature_name, signing_key,
hash_algorithm=hashlib.sha256):
"""Add content hash and sign the event

Args:
event_dict (EventBuilder): The event to add hashes to and sign
event_dict (dict): The event to add hashes to and sign
signature_name (str): The name of the entity signing the event
(typically the server's hostname).
signing_key (syutil.crypto.SigningKey): The key to sign with
hash_algorithm: A hasher from `hashlib`, e.g. hashlib.sha256, to use
to hash the event
"""

name, digest = compute_content_hash(
event.get_pdu_json(), hash_algorithm=hash_algorithm,
)
name, digest = compute_content_hash(event_dict, hash_algorithm=hash_algorithm)

if not hasattr(event, "hashes"):
event.hashes = {}
event.hashes[name] = encode_base64(digest)
event_dict.setdefault("hashes", {})[name] = encode_base64(digest)

event.signatures = compute_event_signature(
event.get_pdu_json(),
event_dict["signatures"] = compute_event_signature(
event_dict,
signature_name=signature_name,
signing_key=signing_key,
)
282 changes: 217 additions & 65 deletions synapse/events/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,79 +13,156 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import copy
import attr

from synapse.api.constants import RoomVersions
from twisted.internet import defer

from synapse.api.constants import (
KNOWN_EVENT_FORMAT_VERSIONS,
KNOWN_ROOM_VERSIONS,
MAX_DEPTH,
)
from synapse.crypto.event_signing import add_hashes_and_signatures
from synapse.types import EventID
from synapse.util.stringutils import random_string

from . import EventBase, FrozenEvent, _event_dict_property
from . import (
_EventInternalMetadata,
event_type_from_format_version,
room_version_to_event_format,
)


def get_event_builder(room_version, key_values={}, internal_metadata_dict={}):
"""Generate an event builder appropriate for the given room version
@attr.s(slots=True, cmp=False, frozen=True)
class EventBuilder(object):
"""A format independent event builder used to build up the event content
before signing the event.

Args:
room_version (str): Version of the room that we're creating an
event builder for
key_values (dict): Fields used as the basis of the new event
internal_metadata_dict (dict): Used to create the `_EventInternalMetadata`
object.
(Note that while objects of this class are frozen, the
content/unsigned/internal_metadata fields are still mutable)

Returns:
EventBuilder
Attributes:
format_version (int): Event format version
room_id (str)
type (str)
sender (str)
content (dict)
unsigned (dict)
internal_metadata (_EventInternalMetadata)

_state (StateHandler)
_auth (synapse.api.Auth)
_store (DataStore)
_clock (Clock)
_hostname (str): The hostname of the server creating the event
_signing_key: The signing key to use to sign the event as the server
"""
if room_version in {
RoomVersions.V1,
RoomVersions.V2,
RoomVersions.VDH_TEST,
RoomVersions.STATE_V2_TEST,
}:
return EventBuilder(key_values, internal_metadata_dict)
else:
raise Exception(
"No event format defined for version %r" % (room_version,)
)

_state = attr.ib()
_auth = attr.ib()
_store = attr.ib()
_clock = attr.ib()
_hostname = attr.ib()
_signing_key = attr.ib()

format_version = attr.ib()

room_id = attr.ib()
type = attr.ib()
sender = attr.ib()

content = attr.ib(default=attr.Factory(dict))
unsigned = attr.ib(default=attr.Factory(dict))

# These only exist on a subset of events, so they raise AttributeError if
# someone tries to get them when they don't exist.
_state_key = attr.ib(default=None)
_redacts = attr.ib(default=None)

internal_metadata = attr.ib(default=attr.Factory(lambda: _EventInternalMetadata({})))

@property
def state_key(self):
if self._state_key is not None:
return self._state_key

raise AttributeError("state_key")

def is_state(self):
return self._state_key is not None

class EventBuilder(EventBase):
def __init__(self, key_values={}, internal_metadata_dict={}):
signatures = copy.deepcopy(key_values.pop("signatures", {}))
unsigned = copy.deepcopy(key_values.pop("unsigned", {}))
@defer.inlineCallbacks
def build(self, prev_event_ids):
"""Transform into a fully signed and hashed event

super(EventBuilder, self).__init__(
key_values,
signatures=signatures,
unsigned=unsigned,
internal_metadata_dict=internal_metadata_dict,
Args:
prev_event_ids (list[str]): The event IDs to use as the prev events

Returns:
Deferred[FrozenEvent]
"""

state_ids = yield self._state.get_current_state_ids(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly concerned that we used to do context.get_prev_state_ids, which might return something different to this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, they're exactly the same, compute_event_context just resolves the state across the prev_events to get prev_state_ids. From a protocol perspective, auth_events should be based on the state at the event.

We could make compute_event_context work with an EventBuilder, but I was slightly shying away from doing so in the interests of not changing too much. We could probably do so though

self.room_id, prev_event_ids,
)
auth_ids = yield self._auth.compute_auth_events(
self, state_ids,
)

event_id = _event_dict_property("event_id")
state_key = _event_dict_property("state_key")
type = _event_dict_property("type")
auth_events = yield self._store.add_event_hashes(auth_ids)
prev_events = yield self._store.add_event_hashes(prev_event_ids)

def build(self):
return FrozenEvent.from_event(self)
old_depth = yield self._store.get_max_depth_of(
prev_event_ids,
)
depth = old_depth + 1
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved

# we cap depth of generated events, to ensure that they are not
# rejected by other servers (and so that they can be persisted in
# the db)
depth = min(depth, MAX_DEPTH)

class EventBuilderFactory(object):
def __init__(self, clock, hostname):
self.clock = clock
self.hostname = hostname
event_dict = {
"auth_events": auth_events,
"prev_events": prev_events,
"type": self.type,
"room_id": self.room_id,
"sender": self.sender,
"content": self.content,
"unsigned": self.unsigned,
"depth": depth,
"prev_state": [],
}

if self.is_state():
event_dict["state_key"] = self._state_key

self.event_id_count = 0
if self._redacts is not None:
event_dict["redacts"] = self._redacts

def create_event_id(self):
i = str(self.event_id_count)
self.event_id_count += 1
defer.returnValue(
create_local_event_from_event_dict(
clock=self._clock,
hostname=self._hostname,
signing_key=self._signing_key,
format_version=self.format_version,
event_dict=event_dict,
internal_metadata_dict=self.internal_metadata.get_dict(),
)
)

local_part = str(int(self.clock.time())) + i + random_string(5)

e_id = EventID(local_part, self.hostname)
class EventBuilderFactory(object):
def __init__(self, hs):
self.clock = hs.get_clock()
self.hostname = hs.hostname
self.signing_key = hs.config.signing_key[0]

return e_id.to_string()
self.store = hs.get_datastore()
richvdh marked this conversation as resolved.
Show resolved Hide resolved
self.state = hs.get_state_handler()
self.auth = hs.get_auth()

def new(self, room_version, key_values={}):
def new(self, room_version, key_values):
"""Generate an event builder appropriate for the given room version

Args:
Expand All @@ -98,27 +175,102 @@ def new(self, room_version, key_values={}):
"""

# There's currently only the one event version defined
if room_version not in {
RoomVersions.V1,
RoomVersions.V2,
RoomVersions.VDH_TEST,
RoomVersions.STATE_V2_TEST,
}:
if room_version not in KNOWN_ROOM_VERSIONS:
raise Exception(
"No event format defined for version %r" % (room_version,)
)

key_values["event_id"] = self.create_event_id()
return EventBuilder(
store=self.store,
state=self.state,
auth=self.auth,
clock=self.clock,
hostname=self.hostname,
signing_key=self.signing_key,
format_version=room_version_to_event_format(room_version),
type=key_values["type"],
state_key=key_values.get("state_key"),
room_id=key_values["room_id"],
sender=key_values["sender"],
content=key_values.get("content", {}),
unsigned=key_values.get("unsigned", {}),
redacts=key_values.get("redacts", None),
)


def create_local_event_from_event_dict(clock, hostname, signing_key,
format_version, event_dict,
internal_metadata_dict=None):
"""Takes a fully formed event dict, ensuring that fields like `origin`
and `origin_server_ts` have correct values for a locally produced event,
then signs and hashes it.

Args:
clock (Clock)
hostname (str)
signing_key
format_version (int)
event_dict (dict)
internal_metadata_dict (dict|None)

Returns:
FrozenEvent
"""

# There's currently only the one event version defined
if format_version not in KNOWN_EVENT_FORMAT_VERSIONS:
raise Exception(
"No event format defined for version %r" % (format_version,)
)

if internal_metadata_dict is None:
internal_metadata_dict = {}

time_now = int(clock.time_msec())

event_dict["event_id"] = _create_event_id(clock, hostname)

event_dict["origin"] = hostname
event_dict["origin_server_ts"] = time_now

event_dict.setdefault("unsigned", {})
age = event_dict["unsigned"].pop("age", 0)
event_dict["unsigned"].setdefault("age_ts", time_now - age)

event_dict.setdefault("signatures", {})

add_hashes_and_signatures(
event_dict,
hostname,
signing_key,
)
return event_type_from_format_version(format_version)(
event_dict, internal_metadata_dict=internal_metadata_dict,
)


# A counter used when generating new event IDs
_event_id_counter = 0


def _create_event_id(clock, hostname):
"""Create a new event ID

Args:
clock (Clock)
hostname (str): The server name for the event ID

Returns:
str
"""

time_now = int(self.clock.time_msec())
global _event_id_counter

key_values.setdefault("origin", self.hostname)
key_values.setdefault("origin_server_ts", time_now)
i = str(_event_id_counter)
_event_id_counter += 1

key_values.setdefault("unsigned", {})
age = key_values["unsigned"].pop("age", 0)
key_values["unsigned"].setdefault("age_ts", time_now - age)
local_part = str(int(clock.time())) + i + random_string(5)

key_values["signatures"] = {}
e_id = EventID(local_part, hostname)

return EventBuilder(key_values=key_values,)
return e_id.to_string()
Loading