From d2bc5d6f29b894516190f24619baf19e216643b7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 28 Aug 2014 10:59:15 +0100 Subject: [PATCH 1/7] Create the correct events with the right configuration when creating a new room. --- synapse/api/constants.py | 7 ++++ synapse/api/events/__init__.py | 7 ++++ synapse/api/events/factory.py | 8 +++- synapse/api/events/room.py | 30 +++++++++++++- synapse/handlers/room.py | 73 +++++++++++++++++++++++++--------- 5 files changed, 104 insertions(+), 21 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index f69f2445a2..9b5b9f5936 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -42,3 +42,10 @@ class PresenceState(object): UNAVAILABLE = u"unavailable" ONLINE = u"online" FREE_FOR_CHAT = u"free_for_chat" + + +class JoinRules(object): + PUBLIC = u"public" + KNOCK = u"knock" + INVITE = u"invite" + PRIVATE = u"private" diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index f9653e0b2a..bf8d288acc 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -152,3 +152,10 @@ class SynapseEvent(JsonEncodedObject): msg = self._check_json(entry, template[key][0]) if msg: return msg + + +class SynapseStateEvent(SynapseEvent): + def __init__(self, **kwargs): + if "state_key" not in kwargs: + kwargs["state_key"] = "" + super(SynapseStateEvent, self).__init__(**kwargs) diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index c2cdcddf41..7c1259d617 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -16,6 +16,8 @@ from synapse.api.events.room import ( RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent, InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent, + RoomPowerLevelsEvent, RoomDefaultLevelEvent, RoomJoinRulesEvent, + RoomCreateEvent, ) from synapse.util.stringutils import random_string @@ -30,7 +32,11 @@ class EventFactory(object): RoomMemberEvent, FeedbackEvent, InviteJoinEvent, - RoomConfigEvent + RoomConfigEvent, + RoomPowerLevelsEvent, + RoomDefaultLevelEvent, + RoomJoinRulesEvent, + RoomCreateEvent, ] def __init__(self, hs): diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py index 9faad57ac0..b63529bb31 100644 --- a/synapse/api/events/room.py +++ b/synapse/api/events/room.py @@ -15,7 +15,7 @@ from synapse.api.constants import Feedback, Membership from synapse.api.errors import SynapseError -from . import SynapseEvent +from . import SynapseEvent, SynapseStateEvent class GenericEvent(SynapseEvent): @@ -132,3 +132,31 @@ class RoomConfigEvent(SynapseEvent): def get_content_template(self): return {} + + +class RoomCreateEvent(SynapseStateEvent): + TYPE = "m.room.create" + + def get_content_template(self): + return {} + + +class RoomJoinRulesEvent(SynapseStateEvent): + TYPE = "m.room.join_rules" + + def get_content_template(self): + return {} + + +class RoomPowerLevelsEvent(SynapseStateEvent): + TYPE = "m.room.power_levels" + + def get_content_template(self): + return {} + + +class RoomDefaultLevelEvent(SynapseStateEvent): + TYPE = "m.room.default_level" + + def get_content_template(self): + return {} diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index eb638fe50a..1ced7d0613 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -17,10 +17,11 @@ from twisted.internet import defer from synapse.types import UserID, RoomAlias, RoomID -from synapse.api.constants import Membership +from synapse.api.constants import Membership, JoinRules from synapse.api.errors import StoreError, SynapseError from synapse.api.events.room import ( - RoomMemberEvent, RoomConfigEvent + RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent, + RoomJoinRulesEvent, RoomDefaultLevelEvent, ) from synapse.util import stringutils from ._base import BaseRoomHandler @@ -62,6 +63,8 @@ class RoomCreationHandler(BaseRoomHandler): else: room_alias = None + is_public = config.get("visibility", None) == "public" + if room_id: # Ensure room_id is the correct type room_id_obj = RoomID.from_string(room_id, self.hs) @@ -71,7 +74,7 @@ class RoomCreationHandler(BaseRoomHandler): yield self.store.store_room( room_id=room_id, room_creator_user_id=user_id, - is_public=config["visibility"] == "public" + is_public=is_public ) else: # autogen room IDs and try to create it. We may clash, so just @@ -85,7 +88,7 @@ class RoomCreationHandler(BaseRoomHandler): yield self.store.store_room( room_id=gen_room_id.to_string(), room_creator_user_id=user_id, - is_public=config["visibility"] == "public" + is_public=is_public ) room_id = gen_room_id.to_string() break @@ -94,18 +97,10 @@ class RoomCreationHandler(BaseRoomHandler): if not room_id: raise StoreError(500, "Couldn't generate a room ID.") - config_event = self.event_factory.create_event( - etype=RoomConfigEvent.TYPE, - room_id=room_id, - user_id=user_id, - content=config, - ) - snapshot = yield self.store.snapshot_room( - room_id=room_id, - user_id=user_id, - state_type=RoomConfigEvent.TYPE, - state_key="", + user = self.hs.parse_userid(user_id) + creation_events = self._create_events_for_new_room( + user, room_id, is_public=is_public ) if room_alias: @@ -115,11 +110,18 @@ class RoomCreationHandler(BaseRoomHandler): servers=[self.hs.hostname], ) - yield self.state_handler.handle_new_event(config_event, snapshot) - # store_id = persist... - federation_handler = self.hs.get_handlers().federation_handler - yield federation_handler.handle_new_event(config_event, snapshot) + + for event in creation_events: + snapshot = yield self.store.snapshot_room( + room_id=room_id, + user_id=user_id, + ) + + logger.debug("Event: %s", event) + + yield self.state_handler.handle_new_event(event, snapshot) + yield self._on_new_room_event(event, snapshot, extra_users=[user]) content = {"membership": Membership.JOIN} join_event = self.event_factory.create_event( @@ -142,6 +144,39 @@ class RoomCreationHandler(BaseRoomHandler): defer.returnValue(result) + def _create_events_for_new_room(self, creator, room_id, is_public=False): + event_keys = { + "room_id": room_id, + "user_id": creator.to_string(), + } + + creation_event = self.event_factory.create_event( + etype=RoomCreateEvent.TYPE, + content={"creator": creator.to_string()}, + **event_keys + ) + + power_levels_event = self.event_factory.create_event( + etype=RoomPowerLevelsEvent.TYPE, + content={creator.to_string(): 10}, + **event_keys + ) + + default_level_event = self.event_factory.create_event( + etype=RoomDefaultLevelEvent.TYPE, + content={"default_level": 0}, + **event_keys + ) + + join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE + join_rules_event = self.event_factory.create_event( + etype=RoomJoinRulesEvent.TYPE, + content={"join_rule": join_rule}, + **event_keys + ) + + return [creation_event, power_levels_event, default_level_event, join_rules_event] + class RoomMemberHandler(BaseRoomHandler): # TODO(paul): This handler currently contains a messy conflation of From 1118f02689032824122fd5d3f71c45811a70dd71 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 29 Aug 2014 15:18:30 +0100 Subject: [PATCH 2/7] Start adding storage for new events. --- synapse/storage/__init__.py | 9 ++++ synapse/storage/room.py | 80 +++++++++++++++++++++++++++++++++++ synapse/storage/schema/im.sql | 32 ++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index e8faba3eeb..204a243560 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -19,6 +19,9 @@ from synapse.api.events.room import ( RoomMemberEvent, RoomTopicEvent, FeedbackEvent, # RoomConfigEvent, RoomNameEvent, + RoomJoinRulesEvent, + RoomPowerLevelsEvent, + RoomDefaultLevelEvent, ) from synapse.util.logutils import log_function @@ -129,6 +132,12 @@ class DataStore(RoomMemberStore, RoomStore, self._store_room_name_txn(txn, event) elif event.type == RoomTopicEvent.TYPE: self._store_room_topic_txn(txn, event) + elif event.type == RoomJoinRulesEvent.TYPE: + self._store_join_rule(txn, event) + elif event.type == RoomPowerLevelsEvent.TYPE: + self._store_power_levels(txn, event) + elif event.type == RoomDefaultLevelEvent.TYPE: + self._store_default_level(txn, event) vals = { "topological_ordering": event.depth, diff --git a/synapse/storage/room.py b/synapse/storage/room.py index d1f1a232f8..8946ce99f7 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -129,6 +129,51 @@ class RoomStore(SQLBaseStore): defer.returnValue(ret) + @defer.inlineCallbacks + def get_room_join_rule(self, room_id): + sql = ( + "SELECT join_rule FROM room_join_rules as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) + + rows = yield self._execute(None, sql, room_id) + + if len(rows) == 1: + defer.returnValue(rows[0][0]) + else: + defer.returnValue(None) + + @defer.inlineCallbacks + def get_power_level(self, room_id, user_id): + sql = ( + "SELECT level FROM room_power_levels as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? AND r.user_id = ? " + ) + + rows = yield self._execute(None, sql, room_id, user_id) + + if len(rows) == 1: + defer.returnValue(rows[0][0]) + return + + sql = ( + "SELECT level FROM room_default_levels as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) + + rows = yield self._execute(None, sql, room_id) + + if len(rows) == 1: + defer.returnValue(rows[0][0]) + else: + defer.returnValue(None) + def _store_room_topic_txn(self, txn, event): self._simple_insert_txn( txn, @@ -151,6 +196,41 @@ class RoomStore(SQLBaseStore): } ) + def _store_join_rule(txn, event): + self._simple_insert_txn( + txn, + "room_join_rules", + { + "event_id": event.event_id, + "room_id": event.room_id, + "join_rule": event.join_rule, + }, + ) + + def _store_power_levels(txn, event): + for user_id, level in event.content: + self._simple_insert_txn( + txn, + "room_power_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "user_id": user_id, + "level": level + }, + ) + + def _store_default_level(txn, event): + self._simple_insert_txn( + txn, + "room_default_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": level + }, + ) + class RoomsTable(Table): table_name = "rooms" diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index e92f21ef3b..c20516b7fa 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -96,8 +96,40 @@ CREATE TABLE IF NOT EXISTS rooms( creator TEXT ); +CREATE TABLE IF NOT EXISTS room_join_rules( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + join_rule TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id); +CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id); + + +CREATE TABLE IF NOT EXISTS room_power_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + user_id TEXT NOT NULL, + level INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id); +CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id); +CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id); + + +CREATE TABLE IF NOT EXISTS room_default_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id); +CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id); + + CREATE TABLE IF NOT EXISTS room_hosts( room_id TEXT NOT NULL, host TEXT NOT NULL, CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE ); + +CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id); From 865469f233aa37d3fa0de9a77dfb6bc597c569d7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 1 Sep 2014 13:44:19 +0100 Subject: [PATCH 3/7] Implement power level lists, default power levels and send_evnet_level/add_state_level events. --- synapse/api/auth.py | 96 ++++++++++++++++++++++++++++++----- synapse/api/events/factory.py | 7 +-- synapse/api/events/room.py | 11 +++- synapse/handlers/room.py | 31 +++++++---- synapse/storage/__init__.py | 12 +++-- synapse/storage/room.py | 87 +++++++++++++++++++++++++------ synapse/storage/schema/im.sql | 20 ++++++++ 7 files changed, 217 insertions(+), 47 deletions(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 2473a2b2bb..c77f52dc30 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -17,9 +17,10 @@ from twisted.internet import defer -from synapse.api.constants import Membership +from synapse.api.constants import Membership, JoinRules from synapse.api.errors import AuthError, StoreError, Codes from synapse.api.events.room import RoomMemberEvent +from synapse.util.logutils import log_function import logging @@ -47,13 +48,20 @@ class Auth(object): if event.type == RoomMemberEvent.TYPE: allowed = yield self.is_membership_change_allowed(event) defer.returnValue(allowed) + return + + self._check_joined_room( + member=snapshot.membership_state, + user_id=snapshot.user_id, + room_id=snapshot.room_id, + ) + + if hasattr(event, "state_key"): + yield self._can_add_state(event) else: - self._check_joined_room( - member=snapshot.membership_state, - user_id=snapshot.user_id, - room_id=snapshot.room_id, - ) - defer.returnValue(True) + yield self._can_send_event(event) + + defer.returnValue(True) else: raise AuthError(500, "Unknown event: %s" % event) except AuthError as e: @@ -111,7 +119,14 @@ class Auth(object): membership = event.content["membership"] + join_rule = yield self.store.get_room_join_rule(event.room_id) + if not join_rule: + join_rule = JoinRules.INVITE + if Membership.INVITE == membership: + # TODO (erikj): We should probably handle this more intelligently + # PRIVATE join rules. + # Invites are valid iff caller is in the room and target isn't. if not caller_in_room: # caller isn't joined raise AuthError(403, "You are not in room %s." % event.room_id) @@ -124,11 +139,18 @@ class Auth(object): # joined: It's a NOOP if event.user_id != target_user_id: raise AuthError(403, "Cannot force another user to join.") - elif room.is_public: - pass # anyone can join public rooms. - elif (not caller or caller.membership not in - [Membership.INVITE, Membership.JOIN]): - raise AuthError(403, "You are not invited to this room.") + elif join_rule == JoinRules.PUBLIC or room.is_public: + pass + elif join_rule == JoinRules.INVITE: + if ( + not caller or caller.membership not in + [Membership.INVITE, Membership.JOIN] + ): + raise AuthError(403, "You are not invited to this room.") + else: + # TODO (erikj): may_join list + # TODO (erikj): private rooms + raise AuthError(403, "You are not allowed to join this room") elif Membership.LEAVE == membership: if not caller_in_room: # trying to leave a room you aren't joined raise AuthError(403, "You are not in room %s." % event.room_id) @@ -176,3 +198,53 @@ class Auth(object): except StoreError: raise AuthError(403, "Unrecognised access token.", errcode=Codes.UNKNOWN_TOKEN) + + @defer.inlineCallbacks + @log_function + def _can_send_event(self, event): + send_level = yield self.store.get_send_event_level(event.room_id) + + if send_level: + send_level = int(send_level) + else: + send_level = 0 + + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + if user_level: + user_level = int(user_level) + else: + user_level = 0 + + if user_level < send_level: + raise AuthError( + 403, "You don't have permission to post to the room" + ) + + defer.returnValue(True) + + @defer.inlineCallbacks + def _can_add_state(self, event): + add_level = yield self.store.get_add_state_level(event.room_id) + + if not add_level: + defer.returnValue(True) + + add_level = int(add_level) + + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + user_level = int(user_level) + + if user_level < add_level: + raise AuthError( + 403, "You don't have permission to add state to the room" + ) + + defer.returnValue(True) diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index 7c1259d617..56180899b2 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -16,8 +16,8 @@ from synapse.api.events.room import ( RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent, InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent, - RoomPowerLevelsEvent, RoomDefaultLevelEvent, RoomJoinRulesEvent, - RoomCreateEvent, + RoomPowerLevelsEvent, RoomJoinRulesEvent, + RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent ) from synapse.util.stringutils import random_string @@ -34,9 +34,10 @@ class EventFactory(object): InviteJoinEvent, RoomConfigEvent, RoomPowerLevelsEvent, - RoomDefaultLevelEvent, RoomJoinRulesEvent, RoomCreateEvent, + RoomAddStateLevelEvent, + RoomSendEventLevelEvent, ] def __init__(self, hs): diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py index b63529bb31..6b431e24ea 100644 --- a/synapse/api/events/room.py +++ b/synapse/api/events/room.py @@ -155,8 +155,15 @@ class RoomPowerLevelsEvent(SynapseStateEvent): return {} -class RoomDefaultLevelEvent(SynapseStateEvent): - TYPE = "m.room.default_level" +class RoomAddStateLevelEvent(SynapseStateEvent): + TYPE = "m.room.add_state_level" + + def get_content_template(self): + return {} + + +class RoomSendEventLevelEvent(SynapseStateEvent): + TYPE = "m.room.send_event_level" def get_content_template(self): return {} diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 11afd34ae2..dace364eae 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -21,7 +21,8 @@ from synapse.api.constants import Membership, JoinRules from synapse.api.errors import StoreError, SynapseError from synapse.api.events.room import ( RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent, - RoomJoinRulesEvent, RoomDefaultLevelEvent, + RoomJoinRulesEvent, RoomAddStateLevelEvent, + RoomSendEventLevelEvent, ) from synapse.util import stringutils from ._base import BaseRoomHandler @@ -152,7 +153,7 @@ class RoomCreationHandler(BaseRoomHandler): creation_event = self.event_factory.create_event( etype=RoomCreateEvent.TYPE, - content={"creator": creator.to_string()}, + content={"creator": creator.to_string(), "default": 0}, **event_keys ) @@ -162,12 +163,6 @@ class RoomCreationHandler(BaseRoomHandler): **event_keys ) - default_level_event = self.event_factory.create_event( - etype=RoomDefaultLevelEvent.TYPE, - content={"default_level": 0}, - **event_keys - ) - join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE join_rules_event = self.event_factory.create_event( etype=RoomJoinRulesEvent.TYPE, @@ -175,7 +170,25 @@ class RoomCreationHandler(BaseRoomHandler): **event_keys ) - return [creation_event, power_levels_event, default_level_event, join_rules_event] + add_state_event = self.event_factory.create_event( + etype=RoomAddStateLevelEvent.TYPE, + content={"level": 10}, + **event_keys + ) + + send_event = self.event_factory.create_event( + etype=RoomSendEventLevelEvent.TYPE, + content={"level": 0}, + **event_keys + ) + + return [ + creation_event, + power_levels_event, + join_rules_event, + add_state_event, + send_event, + ] class RoomMemberHandler(BaseRoomHandler): diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 204a243560..3d5e5049fa 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -21,7 +21,8 @@ from synapse.api.events.room import ( RoomNameEvent, RoomJoinRulesEvent, RoomPowerLevelsEvent, - RoomDefaultLevelEvent, + RoomAddStateLevelEvent, + RoomSendEventLevelEvent, ) from synapse.util.logutils import log_function @@ -125,7 +126,7 @@ class DataStore(RoomMemberStore, RoomStore, if event.type == RoomMemberEvent.TYPE: self._store_room_member_txn(txn, event) elif event.type == FeedbackEvent.TYPE: - self._store_feedback_txn(txn,event) + self._store_feedback_txn(txn, event) # elif event.type == RoomConfigEvent.TYPE: # self._store_room_config_txn(txn, event) elif event.type == RoomNameEvent.TYPE: @@ -136,8 +137,10 @@ class DataStore(RoomMemberStore, RoomStore, self._store_join_rule(txn, event) elif event.type == RoomPowerLevelsEvent.TYPE: self._store_power_levels(txn, event) - elif event.type == RoomDefaultLevelEvent.TYPE: - self._store_default_level(txn, event) + elif event.type == RoomAddStateLevelEvent.TYPE: + self._store_add_state_level(txn, event) + elif event.type == RoomSendEventLevelEvent.TYPE: + self._store_send_event_level(txn, event) vals = { "topological_ordering": event.depth, @@ -231,7 +234,6 @@ class DataStore(RoomMemberStore, RoomStore, defer.returnValue(self.min_token) - def snapshot_room(self, room_id, user_id, state_type=None, state_key=None): """Snapshot the room for an update by a user Args: diff --git a/synapse/storage/room.py b/synapse/storage/room.py index 8946ce99f7..f8aa8bd2a1 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -174,6 +174,28 @@ class RoomStore(SQLBaseStore): else: defer.returnValue(None) + def get_add_state_level(self, room_id): + return self._get_level_from_table("room_add_state_levels", room_id) + + def get_send_event_level(self, room_id): + return self._get_level_from_table("room_send_event_levels", room_id) + + @defer.inlineCallbacks + def _get_level_from_table(self, table, room_id): + sql = ( + "SELECT level FROM %(table)s as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) % {"table": table} + + rows = yield self._execute(None, sql, room_id) + + if len(rows) == 1: + defer.returnValue(rows[0][0]) + else: + defer.returnValue(None) + def _store_room_topic_txn(self, txn, event): self._simple_insert_txn( txn, @@ -196,38 +218,71 @@ class RoomStore(SQLBaseStore): } ) - def _store_join_rule(txn, event): + def _store_join_rule(self, txn, event): self._simple_insert_txn( txn, "room_join_rules", { "event_id": event.event_id, "room_id": event.room_id, - "join_rule": event.join_rule, + "join_rule": event.content["join_rule"], }, ) - def _store_power_levels(txn, event): - for user_id, level in event.content: - self._simple_insert_txn( - txn, - "room_power_levels", - { - "event_id": event.event_id, - "room_id": event.room_id, - "user_id": user_id, - "level": level - }, - ) + def _store_power_levels(self, txn, event): + for user_id, level in event.content.items(): + if user_id == "default": + self._simple_insert_txn( + txn, + "room_default_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": level, + }, + ) + else: + self._simple_insert_txn( + txn, + "room_power_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "user_id": user_id, + "level": level + }, + ) - def _store_default_level(txn, event): + def _store_default_level(self, txn, event): self._simple_insert_txn( txn, "room_default_levels", { "event_id": event.event_id, "room_id": event.room_id, - "level": level + "level": event.content["default_level"], + }, + ) + + def _store_add_state_level(self, txn, event): + self._simple_insert_txn( + txn, + "room_add_state_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": event.content["level"], + }, + ) + + def _store_send_event_level(self, txn, event): + self._simple_insert_txn( + txn, + "room_send_event_levels", + { + "event_id": event.event_id, + "room_id": event.room_id, + "level": event.content["level"], }, ) diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index c20516b7fa..447c1d29a2 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -126,6 +126,26 @@ CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(e CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id); +CREATE TABLE IF NOT EXISTS room_add_state_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id); +CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id); + + +CREATE TABLE IF NOT EXISTS room_send_event_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + level INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id); +CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id); + + CREATE TABLE IF NOT EXISTS room_hosts( room_id TEXT NOT NULL, host TEXT NOT NULL, From db7109c43b298a0b647188dde45724bfc7300915 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 1 Sep 2014 16:15:34 +0100 Subject: [PATCH 4/7] Add beginnings of ban support. --- synapse/api/auth.py | 19 ++++++++++++ synapse/api/constants.py | 3 +- synapse/api/events/factory.py | 3 +- synapse/api/events/room.py | 7 +++++ synapse/handlers/room.py | 49 ++++++++++++++++++------------ synapse/storage/__init__.py | 2 -- synapse/storage/room.py | 57 ++++++++++++++++++++++++++++++++--- synapse/storage/schema/im.sql | 11 +++++++ 8 files changed, 122 insertions(+), 29 deletions(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index c77f52dc30..0e8973e823 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -57,6 +57,8 @@ class Auth(object): ) if hasattr(event, "state_key"): + # TODO (erikj): This really only should be called for *new* + # state yield self._can_add_state(event) else: yield self._can_send_event(event) @@ -152,12 +154,29 @@ class Auth(object): # TODO (erikj): private rooms raise AuthError(403, "You are not allowed to join this room") elif Membership.LEAVE == membership: + # TODO (erikj): Implement kicks. + if not caller_in_room: # trying to leave a room you aren't joined raise AuthError(403, "You are not in room %s." % event.room_id) elif target_user_id != event.user_id: # trying to force another user to leave raise AuthError(403, "Cannot force %s to leave." % target_user_id) + elif Membership.BAN == membership: + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + ban_level, _ = yield self.store.get_ops_levels(event.room_id) + + if ban_level: + ban_level = int(ban_level) + else: + ban_level = 5 # FIXME (erikj): What should we do here? + + if ban_level < user_level: + raise AuthError(403, "You don't have permission to ban") else: raise AuthError(500, "Unknown membership %s" % membership) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 9b5b9f5936..668ffa07ca 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -23,7 +23,8 @@ class Membership(object): JOIN = u"join" KNOCK = u"knock" LEAVE = u"leave" - LIST = (INVITE, JOIN, KNOCK, LEAVE) + BAN = u"ban" + LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN) class Feedback(object): diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index 56180899b2..159728b2d2 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -16,7 +16,7 @@ from synapse.api.events.room import ( RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent, InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent, - RoomPowerLevelsEvent, RoomJoinRulesEvent, + RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent, RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent ) @@ -38,6 +38,7 @@ class EventFactory(object): RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent, + RoomOpsPowerLevelsEvent, ] def __init__(self, hs): diff --git a/synapse/api/events/room.py b/synapse/api/events/room.py index 6b431e24ea..f6d3c59a9a 100644 --- a/synapse/api/events/room.py +++ b/synapse/api/events/room.py @@ -167,3 +167,10 @@ class RoomSendEventLevelEvent(SynapseStateEvent): def get_content_template(self): return {} + + +class RoomOpsPowerLevelsEvent(SynapseStateEvent): + TYPE = "m.room.ops_levels" + + def get_content_template(self): + return {} diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index dace364eae..9262afb474 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -22,7 +22,7 @@ from synapse.api.errors import StoreError, SynapseError from synapse.api.events.room import ( RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomAddStateLevelEvent, - RoomSendEventLevelEvent, + RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, ) from synapse.util import stringutils from ._base import BaseRoomHandler @@ -151,35 +151,44 @@ class RoomCreationHandler(BaseRoomHandler): "user_id": creator.to_string(), } - creation_event = self.event_factory.create_event( + def create(etype, **content): + return self.event_factory.create_event( + etype=etype, + content=content, + **event_keys + ) + + creation_event = create( etype=RoomCreateEvent.TYPE, - content={"creator": creator.to_string(), "default": 0}, - **event_keys + creator=creator.to_string(), + default=0, ) - power_levels_event = self.event_factory.create_event( + power_levels_event = create( etype=RoomPowerLevelsEvent.TYPE, - content={creator.to_string(): 10}, - **event_keys + **{creator.to_string(): 10} ) join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE - join_rules_event = self.event_factory.create_event( + join_rules_event = create( etype=RoomJoinRulesEvent.TYPE, - content={"join_rule": join_rule}, - **event_keys + join_rule=join_rule, ) - add_state_event = self.event_factory.create_event( + add_state_event = create( etype=RoomAddStateLevelEvent.TYPE, - content={"level": 10}, - **event_keys + level=10, ) - send_event = self.event_factory.create_event( + send_event = create( etype=RoomSendEventLevelEvent.TYPE, - content={"level": 0}, - **event_keys + level=0, + ) + + ops = create( + etype=RoomOpsPowerLevelsEvent.TYPE, + ban_level=5, + kick_level=5, ) return [ @@ -188,6 +197,7 @@ class RoomCreationHandler(BaseRoomHandler): join_rules_event, add_state_event, send_event, + ops, ] @@ -493,10 +503,9 @@ class RoomMemberHandler(BaseRoomHandler): host = target_user.domain destinations.append(host) - # If we are joining a remote HS, include that. - if membership == Membership.JOIN: - host = target_user.domain - destinations.append(host) + # Always include target domain + host = target_user.domain + destinations.append(host) return self._on_new_room_event( event, snapshot, extra_destinations=destinations, diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 3d5e5049fa..cc8b59f8da 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -127,8 +127,6 @@ class DataStore(RoomMemberStore, RoomStore, self._store_room_member_txn(txn, event) elif event.type == FeedbackEvent.TYPE: self._store_feedback_txn(txn, event) -# elif event.type == RoomConfigEvent.TYPE: -# self._store_room_config_txn(txn, event) elif event.type == RoomNameEvent.TYPE: self._store_room_name_txn(txn, event) elif event.type == RoomTopicEvent.TYPE: diff --git a/synapse/storage/room.py b/synapse/storage/room.py index f8aa8bd2a1..3b2d1a8ecd 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -27,6 +27,9 @@ import logging logger = logging.getLogger(__name__) +OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level")) + + class RoomStore(SQLBaseStore): @defer.inlineCallbacks @@ -145,8 +148,13 @@ class RoomStore(SQLBaseStore): else: defer.returnValue(None) - @defer.inlineCallbacks def get_power_level(self, room_id, user_id): + return self._db_pool.runInteraction( + self._get_power_level, + room_id, user_id, + ) + + def _get_power_level(self, txn, room_id, user_id): sql = ( "SELECT level FROM room_power_levels as r " "INNER JOIN current_state_events as c " @@ -154,7 +162,7 @@ class RoomStore(SQLBaseStore): "WHERE c.room_id = ? AND r.user_id = ? " ) - rows = yield self._execute(None, sql, room_id, user_id) + rows = txn.execute(sql, (room_id, user_id,)).fetchall() if len(rows) == 1: defer.returnValue(rows[0][0]) @@ -167,12 +175,33 @@ class RoomStore(SQLBaseStore): "WHERE c.room_id = ? " ) - rows = yield self._execute(None, sql, room_id) + rows = txn.execute(sql, (room_id,)).fetchall() if len(rows) == 1: - defer.returnValue(rows[0][0]) + return rows[0][0] else: - defer.returnValue(None) + return None + + def get_ops_levels(self, room_id): + return self._db_pool.runInteraction( + self._get_ops_levels, + room_id, + ) + + def _get_ops_levels(self, txn, room_id): + sql = ( + "SELECT ban_level, kick_level FROM room_ops_levels as r " + "INNER JOIN current_state_events as c " + "ON r.event_id = c.event_id " + "WHERE c.room_id = ? " + ) + + rows = txn.execute(sql, (room_id,)).fetchall() + + if len(rows) == 1: + return OpsLevel(rows[0][0], rows[0][1]) + else: + return OpsLevel(None, None) def get_add_state_level(self, room_id): return self._get_level_from_table("room_add_state_levels", room_id) @@ -286,6 +315,24 @@ class RoomStore(SQLBaseStore): }, ) + def _store_ops_level(self, txn, event): + content = { + "event_id": event.event_id, + "room_id": event.room_id, + } + + if "kick_level" in event.content: + content["kick_level"] = event.content["kick_level"] + + if "ban_level" in event.content: + content["ban_level"] = event.content["ban_level"] + + self._simple_insert_txn( + txn, + "room_send_event_levels", + content, + ) + class RoomsTable(Table): table_name = "rooms" diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index 447c1d29a2..1de0d59066 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -146,6 +146,17 @@ CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_le CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id); +CREATE TABLE IF NOT EXISTS room_ops_levels( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + ban_level INTEGER, + kick_level INTEGER, +); + +CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id); +CREATE INDEX IF NOT EXISTS room_ops_levels_room_id ON room_ops_levels(room_id); + + CREATE TABLE IF NOT EXISTS room_hosts( room_id TEXT NOT NULL, host TEXT NOT NULL, From b8ab9f1c0a93c2c52f2990c42e5cb27167281694 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 1 Sep 2014 18:24:56 +0100 Subject: [PATCH 5/7] Add all the necessary checks to make banning work. --- synapse/api/auth.py | 40 ++++++++++++++++++++++++++++++++-- synapse/api/events/__init__.py | 2 ++ synapse/federation/units.py | 1 + synapse/handlers/room.py | 12 +++++++++- synapse/storage/room.py | 3 +-- synapse/storage/schema/im.sql | 2 +- 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 0e8973e823..abd7d73b0a 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -45,7 +45,10 @@ class Auth(object): """ try: if hasattr(event, "room_id"): + is_state = hasattr(event, "state_key") + if event.type == RoomMemberEvent.TYPE: + yield self._can_replace_state(event) allowed = yield self.is_membership_change_allowed(event) defer.returnValue(allowed) return @@ -56,10 +59,11 @@ class Auth(object): room_id=snapshot.room_id, ) - if hasattr(event, "state_key"): + if is_state: # TODO (erikj): This really only should be called for *new* # state yield self._can_add_state(event) + yield self._can_replace_state(event) else: yield self._can_send_event(event) @@ -175,7 +179,7 @@ class Auth(object): else: ban_level = 5 # FIXME (erikj): What should we do here? - if ban_level < user_level: + if user_level < ban_level: raise AuthError(403, "You don't have permission to ban") else: raise AuthError(500, "Unknown membership %s" % membership) @@ -267,3 +271,35 @@ class Auth(object): ) defer.returnValue(True) + + @defer.inlineCallbacks + def _can_replace_state(self, event): + current_state = yield self.store.get_current_state( + event.room_id, + event.type, + event.state_key, + ) + + if current_state: + current_state = current_state[0] + + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + + if user_level: + user_level = int(user_level) + else: + user_level = 0 + + logger.debug("Checking power level for %s, %s", event.user_id, user_level) + if current_state and hasattr(current_state, "required_power_level"): + req = current_state.required_power_level + + logger.debug("Checked power level for %s, %s", event.user_id, req) + if user_level < req: + raise AuthError( + 403, + "You don't have permission to change that state" + ) diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index bf8d288acc..9502f5df8f 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -42,6 +42,7 @@ class SynapseEvent(JsonEncodedObject): "user_id", # sender/initiator "content", # HTTP body, JSON "state_key", + "required_power_level", ] internal_keys = [ @@ -52,6 +53,7 @@ class SynapseEvent(JsonEncodedObject): "destinations", "origin", "outlier", + "power_level", ] required_keys = [ diff --git a/synapse/federation/units.py b/synapse/federation/units.py index 2b2f11f36a..b468f70546 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -68,6 +68,7 @@ class Pdu(JsonEncodedObject): "power_level", "prev_state_id", "prev_state_origin", + "required_power_level", ] internal_keys = [ diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 9262afb474..f33bec9cc1 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -166,7 +166,7 @@ class RoomCreationHandler(BaseRoomHandler): power_levels_event = create( etype=RoomPowerLevelsEvent.TYPE, - **{creator.to_string(): 10} + **{creator.to_string(): 10, "default": 0} ) join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE @@ -343,6 +343,16 @@ class RoomMemberHandler(BaseRoomHandler): if do_auth: yield self.auth.check(event, snapshot, raises=True) + # If we're banning someone, set a req power level + if event.membership == Membership.BAN: + if not hasattr(event, "required_power_level") or event.required_power_level is None: + # Add some default required_power_level + user_level = yield self.store.get_power_level( + event.room_id, + event.user_id, + ) + event.required_power_level = user_level + if prev_state and prev_state.membership == event.membership: # double same action, treat this event as a NOOP. defer.returnValue({}) diff --git a/synapse/storage/room.py b/synapse/storage/room.py index 3b2d1a8ecd..3ca07f4350 100644 --- a/synapse/storage/room.py +++ b/synapse/storage/room.py @@ -165,8 +165,7 @@ class RoomStore(SQLBaseStore): rows = txn.execute(sql, (room_id, user_id,)).fetchall() if len(rows) == 1: - defer.returnValue(rows[0][0]) - return + return rows[0][0] sql = ( "SELECT level FROM room_default_levels as r " diff --git a/synapse/storage/schema/im.sql b/synapse/storage/schema/im.sql index 1de0d59066..dbefbbda31 100644 --- a/synapse/storage/schema/im.sql +++ b/synapse/storage/schema/im.sql @@ -150,7 +150,7 @@ CREATE TABLE IF NOT EXISTS room_ops_levels( event_id TEXT NOT NULL, room_id TEXT NOT NULL, ban_level INTEGER, - kick_level INTEGER, + kick_level INTEGER ); CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id); From 3f5ebccbffda3a5599378b5000a8e78b47e7df7a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 1 Sep 2014 19:57:17 +0100 Subject: [PATCH 6/7] Fix the tests to include new db calls --- tests/handlers/test_room.py | 12 ++++++------ tests/utils.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index 09d2a92e16..219a53c426 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -330,6 +330,8 @@ class RoomCreationTest(unittest.TestCase): datastore=NonCallableMock(spec_set=[ "store_room", "snapshot_room", + "persist_event", + "get_joined_hosts_for_room", ]), http_client=NonCallableMock(spec_set=[]), notifier=NonCallableMock(spec_set=["on_new_room_event"]), @@ -362,6 +364,10 @@ class RoomCreationTest(unittest.TestCase): ]) self.room_member_handler = self.handlers.room_member_handler + def hosts(room): + return defer.succeed([]) + self.datastore.get_joined_hosts_for_room.side_effect = hosts + @defer.inlineCallbacks def test_room_creation(self): user_id = "@foo:red" @@ -385,9 +391,3 @@ class RoomCreationTest(unittest.TestCase): self.assertTrue(self.state_handler.handle_new_event.called) self.assertTrue(self.federation.handle_new_event.called) - config_event = self.federation.handle_new_event.call_args[0][0] - - self.assertEquals(RoomConfigEvent.TYPE, config_event.type) - self.assertEquals(room_id, config_event.room_id) - self.assertEquals(user_id, config_event.user_id) - self.assertEquals(config, config_event.content) diff --git a/tests/utils.py b/tests/utils.py index ea7d6893c6..2c146eab55 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -234,6 +234,20 @@ class MemoryDataStore(object): def get_room_events_max_id(self): return 0 # TODO (erikj) + def get_send_event_level(self, room_id): + return defer.succeed(0) + + def get_power_level(self, room_id, user_id): + return defer.succeed(0) + + def get_add_state_level(self, room_id): + return defer.succeed(0) + + def get_room_join_rule(self, room_id): + # TODO (erikj): This should be configurable + return defer.succeed("invite") + + def _format_call(args, kwargs): return ", ".join( ["%r" % (a) for a in args] + From 6d285606264928b92eab759bbc42192553ca20e6 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 1 Sep 2014 20:27:45 +0100 Subject: [PATCH 7/7] Don't put required power levels on permission state events --- synapse/handlers/room.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index f33bec9cc1..9858d1af50 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -149,6 +149,7 @@ class RoomCreationHandler(BaseRoomHandler): event_keys = { "room_id": room_id, "user_id": creator.to_string(), + "required_power_level": 10, } def create(etype, **content): @@ -164,9 +165,10 @@ class RoomCreationHandler(BaseRoomHandler): default=0, ) - power_levels_event = create( + power_levels_event = self.event_factory.create_event( etype=RoomPowerLevelsEvent.TYPE, - **{creator.to_string(): 10, "default": 0} + content={creator.to_string(): 10, "default": 0}, + **event_keys ) join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE