WIP for new way of managing events.

This commit is contained in:
Erik Johnston 2014-12-03 16:07:21 +00:00
parent 6941a19715
commit 75b4329aaa
9 changed files with 376 additions and 104 deletions

View File

@ -351,7 +351,7 @@ class Auth(object):
return self.store.is_server_admin(user) return self.store.is_server_admin(user)
@defer.inlineCallbacks @defer.inlineCallbacks
def add_auth_events(self, event): def get_auth_events(self, event, current_state):
if event.type == RoomCreateEvent.TYPE: if event.type == RoomCreateEvent.TYPE:
event.auth_events = [] event.auth_events = []
return return
@ -359,19 +359,19 @@ class Auth(object):
auth_events = [] auth_events = []
key = (RoomPowerLevelsEvent.TYPE, "", ) key = (RoomPowerLevelsEvent.TYPE, "", )
power_level_event = event.old_state_events.get(key) power_level_event = current_state.get(key)
if power_level_event: if power_level_event:
auth_events.append(power_level_event.event_id) auth_events.append(power_level_event.event_id)
key = (RoomJoinRulesEvent.TYPE, "", ) key = (RoomJoinRulesEvent.TYPE, "", )
join_rule_event = event.old_state_events.get(key) join_rule_event = current_state.get(key)
key = (RoomMemberEvent.TYPE, event.user_id, ) key = (RoomMemberEvent.TYPE, event.user_id, )
member_event = event.old_state_events.get(key) member_event = current_state.get(key)
key = (RoomCreateEvent.TYPE, "", ) key = (RoomCreateEvent.TYPE, "", )
create_event = event.old_state_events.get(key) create_event = current_state.get(key)
if create_event: if create_event:
auth_events.append(create_event.event_id) auth_events.append(create_event.event_id)
@ -403,7 +403,8 @@ class Auth(object):
} }
for h in hashes for h in hashes
] ]
event.auth_events = zip(auth_events, hashes)
defer.returnValue(zip(auth_events, hashes))
@log_function @log_function
def _can_send_event(self, event, auth_events): def _can_send_event(self, event, auth_events):

View File

@ -59,3 +59,12 @@ class LoginType(object):
EMAIL_URL = u"m.login.email.url" EMAIL_URL = u"m.login.email.url"
EMAIL_IDENTITY = u"m.login.email.identity" EMAIL_IDENTITY = u"m.login.email.identity"
RECAPTCHA = u"m.login.recaptcha" RECAPTCHA = u"m.login.recaptcha"
class EventTypes(object):
Member = "m.room.member"
Create = "m.room.create"
JoinRules = "m.room.join_rules"
PowerLevels = "m.room.power_levels"
Aliases = "m.room.aliases"
Redaction = "m.room.redaction"

View File

@ -29,17 +29,17 @@ logger = logging.getLogger(__name__)
def check_event_content_hash(event, hash_algorithm=hashlib.sha256): def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
"""Check whether the hash for this PDU matches the contents""" """Check whether the hash for this PDU matches the contents"""
computed_hash = _compute_content_hash(event, hash_algorithm) name, expected_hash = compute_content_hash(event, hash_algorithm)
logger.debug("Expecting hash: %s", encode_base64(computed_hash.digest())) logger.debug("Expecting hash: %s", encode_base64(expected_hash))
if computed_hash.name not in event.hashes: if name not in event.hashes:
raise SynapseError( raise SynapseError(
400, 400,
"Algorithm %s not in hashes %s" % ( "Algorithm %s not in hashes %s" % (
computed_hash.name, list(event.hashes), name, list(event.hashes),
), ),
Codes.UNAUTHORIZED, Codes.UNAUTHORIZED,
) )
message_hash_base64 = event.hashes[computed_hash.name] message_hash_base64 = event.hashes[name.name]
try: try:
message_hash_bytes = decode_base64(message_hash_base64) message_hash_bytes = decode_base64(message_hash_base64)
except: except:
@ -48,10 +48,10 @@ def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
"Invalid base64: %s" % (message_hash_base64,), "Invalid base64: %s" % (message_hash_base64,),
Codes.UNAUTHORIZED, Codes.UNAUTHORIZED,
) )
return message_hash_bytes == computed_hash.digest() return message_hash_bytes == expected_hash
def _compute_content_hash(event, hash_algorithm): def compute_content_hash(event, hash_algorithm):
event_json = event.get_pdu_json() event_json = event.get_pdu_json()
event_json.pop("age_ts", None) event_json.pop("age_ts", None)
event_json.pop("unsigned", None) event_json.pop("unsigned", None)
@ -59,8 +59,11 @@ def _compute_content_hash(event, hash_algorithm):
event_json.pop("hashes", None) event_json.pop("hashes", None)
event_json.pop("outlier", None) event_json.pop("outlier", None)
event_json.pop("destinations", None) event_json.pop("destinations", None)
event_json_bytes = encode_canonical_json(event_json) event_json_bytes = encode_canonical_json(event_json)
return hash_algorithm(event_json_bytes)
hashed = hash_algorithm(event_json_bytes)
return (hashed.name, hashed.digest())
def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256): def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256):
@ -86,20 +89,20 @@ def compute_event_signature(event, signature_name, signing_key):
def add_hashes_and_signatures(event, signature_name, signing_key, def add_hashes_and_signatures(event, signature_name, signing_key,
hash_algorithm=hashlib.sha256): hash_algorithm=hashlib.sha256):
if hasattr(event, "old_state_events"): # if hasattr(event, "old_state_events"):
state_json_bytes = encode_canonical_json( # state_json_bytes = encode_canonical_json(
[e.event_id for e in event.old_state_events.values()] # [e.event_id for e in event.old_state_events.values()]
) # )
hashed = hash_algorithm(state_json_bytes) # hashed = hash_algorithm(state_json_bytes)
event.state_hash = { # event.state_hash = {
hashed.name: encode_base64(hashed.digest()) # hashed.name: encode_base64(hashed.digest())
} # }
hashed = _compute_content_hash(event, hash_algorithm=hash_algorithm) name, digest = compute_content_hash(event, hash_algorithm=hash_algorithm)
if not hasattr(event, "hashes"): if not hasattr(event, "hashes"):
event.hashes = {} event.hashes = {}
event.hashes[hashed.name] = encode_base64(hashed.digest()) event.hashes[name] = encode_base64(digest)
event.signatures = compute_event_signature( event.signatures = compute_event_signature(
event, event,

View File

@ -16,6 +16,21 @@
from frozendict import frozendict from frozendict import frozendict
def _freeze(o):
if isinstance(o, dict):
return frozendict({k: _freeze(v) for k,v in o.items()})
if isinstance(o, basestring):
return o
try:
return tuple([_freeze(i) for i in o])
except TypeError:
pass
return o
class _EventInternalMetadata(object): class _EventInternalMetadata(object):
def __init__(self, internal_metadata_dict): def __init__(self, internal_metadata_dict):
self.__dict__ = internal_metadata_dict self.__dict__ = internal_metadata_dict
@ -24,78 +39,47 @@ class _EventInternalMetadata(object):
return dict(self.__dict__) return dict(self.__dict__)
class Event(object): def _event_dict_property(key):
def __init__(self, event_dict, internal_metadata_dict={}): def getter(self):
self._signatures = event_dict.get("signatures", {}) return self._event_dict[key]
self._unsigned = event_dict.get("unsigned", {})
self._original = { def setter(self, v):
k: v self._event_dict[key] = v
for k, v in event_dict.items()
if k not in ["signatures", "unsigned"]
}
self._event_dict = frozendict(self._original) def delete(self):
del self._event_dict[key]
return property(
getter,
setter,
delete,
)
class EventBase(object):
def __init__(self, event_dict, signatures={}, unsigned={},
internal_metadata_dict={}):
self.signatures = signatures
self.unsigned = unsigned
self._event_dict = event_dict
self.internal_metadata = _EventInternalMetadata( self.internal_metadata = _EventInternalMetadata(
internal_metadata_dict internal_metadata_dict
) )
@property auth_events = _event_dict_property("auth_events")
def auth_events(self): content = _event_dict_property("content")
return self._event_dict["auth_events"] event_id = _event_dict_property("event_id")
hashes = _event_dict_property("hashes")
@property origin = _event_dict_property("origin")
def content(self): prev_events = _event_dict_property("prev_events")
return self._event_dict["content"] prev_state = _event_dict_property("prev_state")
room_id = _event_dict_property("room_id")
@property sender = _event_dict_property("sender")
def event_id(self): state_key = _event_dict_property("state_key")
return self._event_dict["event_id"] type = _event_dict_property("type")
user_id = _event_dict_property("sender")
@property
def hashes(self):
return self._event_dict["hashes"]
@property
def origin(self):
return self._event_dict["origin"]
@property
def prev_events(self):
return self._event_dict["prev_events"]
@property
def prev_state(self):
return self._event_dict["prev_state"]
@property
def room_id(self):
return self._event_dict["room_id"]
@property
def signatures(self):
return self._signatures
@property
def state_key(self):
return self._event_dict["state_key"]
@property
def type(self):
return self._event_dict["type"]
@property
def unsigned(self):
return self._unsigned
@property
def user_id(self):
return self._event_dict["sender"]
@property
def sender(self):
return self._event_dict["sender"]
def get_dict(self): def get_dict(self):
d = dict(self._original) d = dict(self._original)
@ -117,4 +101,33 @@ class Event(object):
pdu_json.setdefault("unsigned", {})["age"] = int(age) pdu_json.setdefault("unsigned", {})["age"] = int(age)
del pdu_json["unsigned"]["age_ts"] del pdu_json["unsigned"]["age_ts"]
return pdu_json return pdu_json
def __set__(self, instance, value):
raise AttributeError("Unrecognized attribute %s" % (instance,))
class FrozenEvent(EventBase):
def __init__(self, event_dict, signatures={}, unsigned={}):
event_dict = dict(event_dict)
signatures.update(event_dict.pop("signatures", {}))
unsigned.update(event_dict.pop("unsigned", {}))
frozen_dict = _freeze(event_dict)
super(FrozenEvent, self).__init__(
frozen_dict,
signatures=signatures,
unsigned=unsigned
)
@staticmethod
def from_event(event):
e = FrozenEvent(
event.event_dict()
)
e.internal_metadata = event.internal_metadata
return e

View File

@ -13,32 +13,24 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from . import Event from . import EventBase, FrozenEvent
from synapse.types import EventID from synapse.types import EventID
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
class EventBuilder(object): class EventBuilder(EventBase):
def __init__(self, key_values={}): def __init__(self, key_values={}):
self._event_dict = dict(key_values) super(FrozenEvent, self).__init__(
self._metadata = {} key_values,
)
def update_event_key(self, key, value):
self._event_dict[key] = value
def update_event_keys(self, other_dict): def update_event_keys(self, other_dict):
self._event_dict.update(other_dict) self._event_dict.update(other_dict)
def update_internal_key(self, key, value):
self._metadata[key] = value
def build(self): def build(self):
return Event( return FrozenEvent.from_event(self)
self._event_dict,
self._metadata,
)
class EventBuilderFactory(object): class EventBuilderFactory(object):

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Copyright 2014 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.internet import defer
class EventSnapshot(object):
def __init__(self, prev_events, depth, current_state,
current_state_group):
self._prev_events = prev_events
self._depth = depth
self._current_state = current_state
self._current_state_group = current_state_group
class EventCache(object):
def __init__(self, store):
self._store = store
self._cache = {}
@defer.inlineCallbacks
def load_event(self, event_id):
event = self._cache.get(event_id, None)
if not event:
event = yield self._store.get_event(
event_id,
allow_none=True
)
if event:
self._cache[event_id] = event
defer.returnValue(event)
def load_event_from_cache(self, event_id):
return self._cache.get(event_id, None)
def add_to_cache(self, *events):
self._cache.update({
event.event_id: event
for event in events
})
class EventContext(object):
def __init__(self, current_state, auth_events):
self.current_state = current_state
self.auth_events = auth_events

82
synapse/events/utils.py Normal file
View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Copyright 2014 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.api.constants import EventTypes
def prune_event(event):
""" Returns a pruned version of the given event, which removes all keys we
don't know about or think could potentially be dodgy.
This is used when we "redact" an event. We want to remove all fields that
the user has specified, but we do want to keep necessary information like
type, state_key etc.
"""
event_type = event.type
allowed_keys = [
"event_id",
"sender",
"room_id",
"hashes",
"signatures",
"content",
"type",
"state_key",
"depth",
"prev_events",
"prev_state",
"auth_events",
"origin",
"origin_server_ts",
]
new_content = {}
def add_fields(*fields):
for field in fields:
if field in event.content:
new_content[field] = event.content[field]
if event_type == EventTypes.Member:
add_fields("membership")
elif event_type == EventTypes.Create:
add_fields("creator")
elif event_type == EventTypes.JoinRules:
add_fields("join_rule")
elif event_type == EventTypes.PowerLevels:
add_fields(
"users",
"users_default",
"events",
"events_default",
"events_default",
"state_default",
"ban",
"kick",
"redact",
)
elif event_type == EventTypes.Aliases:
add_fields("aliases")
allowed_fields = {
k: v
for k, v in event.get_dict().items()
if k in allowed_keys
}
allowed_fields["content"] = new_content
return type(event)(allowed_fields)

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2014 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.types import EventID, RoomID, UserID
from synapse.api.errors import SynapseError
class EventValidator(object):
def validate(self, event):
EventID.from_string(event.event_id)
RoomID.from_string(event.room_id)
hasattr(event, "auth_events")
hasattr(event, "content")
hasattr(event, "hashes")
hasattr(event, "origin")
hasattr(event, "prev_events")
hasattr(event, "prev_events")
hasattr(event, "sender")
hasattr(event, "type")
# Check that the following keys have string values
strings = [
"origin",
"sender",
"type",
]
if hasattr(event, "state_key"):
strings.append("state_key")
for s in strings:
if not isinstance(getattr(event, s), basestring):
raise SynapseError(400, "Not '%s' a string type" % (s,))
# Check that the following keys have dictionary values
# TODO
# Check that the following keys have the correct format for DAGs
# TODO
def validate_new(self, event):
self.validate(event)
UserID.from_string(event.sender)

View File

@ -21,6 +21,8 @@ from synapse.crypto.event_signing import add_hashes_and_signatures
from synapse.api.events.room import RoomMemberEvent from synapse.api.events.room import RoomMemberEvent
from synapse.api.constants import Membership from synapse.api.constants import Membership
from synapse.events.snapshot import EventSnapshot, EventContext
import logging import logging
@ -56,6 +58,55 @@ class BaseHandler(object):
retry_after_ms=int(1000*(time_allowed - time_now)), retry_after_ms=int(1000*(time_allowed - time_now)),
) )
@defer.inlineCallbacks
def _handle_new_client_event(self, builder):
latest_ret = yield self.store.get_latest_events_in_room(
builder.room_id,
)
depth = max([d for _, _, d in latest_ret])
prev_events = [(e, h) for e, h, _ in latest_ret]
group, curr_state = yield self.state_handler.resolve_state_groups(
[e for e, _ in prev_events]
)
snapshot = EventSnapshot(
prev_events=prev_events,
depth=depth,
current_state=curr_state,
current_state_group=group,
)
builder.prev_events = prev_events
builder.depth = depth
auth_events = yield self.auth.get_event_auth(builder, curr_state)
builder.update_event_key("auth_events", auth_events)
add_hashes_and_signatures(
builder, self.server_name, self.signing_key
)
event = builder.build()
auth_ids = zip(*auth_events)[0]
curr_auth_events = {
k: v
for k, v in curr_state
if v.event_id in auth_ids
}
context = EventContext(
current_state=curr_state,
auth_events=curr_auth_events,
)
self.auth.check(event, auth_events=context.auth_events)
@defer.inlineCallbacks @defer.inlineCallbacks
def _on_new_room_event(self, event, snapshot, extra_destinations=[], def _on_new_room_event(self, event, snapshot, extra_destinations=[],
extra_users=[], suppress_auth=False, extra_users=[], suppress_auth=False,