Merge branch 'deletions' of github.com:matrix-org/synapse into develop

This commit is contained in:
Erik Johnston 2014-09-25 17:02:53 +01:00
commit 37bfe44046
17 changed files with 531 additions and 27 deletions

View file

@ -19,7 +19,9 @@ from twisted.internet import defer
from synapse.api.constants import Membership, JoinRules
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
from synapse.api.events.room import RoomMemberEvent, RoomPowerLevelsEvent
from synapse.api.events.room import (
RoomMemberEvent, RoomPowerLevelsEvent, RoomRedactionEvent,
)
from synapse.util.logutils import log_function
import logging
@ -70,6 +72,9 @@ class Auth(object):
if event.type == RoomPowerLevelsEvent.TYPE:
yield self._check_power_levels(event)
if event.type == RoomRedactionEvent.TYPE:
yield self._check_redaction(event)
defer.returnValue(True)
else:
raise AuthError(500, "Unknown event: %s" % event)
@ -170,7 +175,7 @@ class Auth(object):
event.room_id,
event.user_id,
)
_, kick_level = yield self.store.get_ops_levels(event.room_id)
_, kick_level, _ = yield self.store.get_ops_levels(event.room_id)
if kick_level:
kick_level = int(kick_level)
@ -187,7 +192,7 @@ class Auth(object):
event.user_id,
)
ban_level, _ = yield self.store.get_ops_levels(event.room_id)
ban_level, _, _ = yield self.store.get_ops_levels(event.room_id)
if ban_level:
ban_level = int(ban_level)
@ -321,6 +326,29 @@ class Auth(object):
"You don't have permission to change that state"
)
@defer.inlineCallbacks
def _check_redaction(self, event):
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
_, _, redact_level = yield self.store.get_ops_levels(event.room_id)
if not redact_level:
redact_level = 50
if user_level < redact_level:
raise AuthError(
403,
"You don't have permission to redact events"
)
@defer.inlineCallbacks
def _check_power_levels(self, event):
for k, v in event.content.items():

View file

@ -22,7 +22,8 @@ def serialize_event(hs, e):
if not isinstance(e, SynapseEvent):
return e
d = e.get_dict()
# Should this strip out None's?
d = {k: v for k, v in e.get_dict().items()}
if "age_ts" in d:
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
del d["age_ts"]
@ -58,17 +59,19 @@ class SynapseEvent(JsonEncodedObject):
"required_power_level",
"age_ts",
"prev_content",
"prev_state",
"redacted_because",
]
internal_keys = [
"is_state",
"prev_events",
"prev_state",
"depth",
"destinations",
"origin",
"outlier",
"power_level",
"redacted",
]
required_keys = [

View file

@ -17,7 +17,8 @@ from synapse.api.events.room import (
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent,
RoomRedactionEvent,
)
from synapse.util.stringutils import random_string
@ -39,6 +40,7 @@ class EventFactory(object):
RoomAddStateLevelEvent,
RoomSendEventLevelEvent,
RoomOpsPowerLevelsEvent,
RoomRedactionEvent,
]
def __init__(self, hs):

View file

@ -180,3 +180,12 @@ class RoomAliasesEvent(SynapseStateEvent):
def get_content_template(self):
return {}
class RoomRedactionEvent(SynapseEvent):
TYPE = "m.room.redaction"
valid_keys = SynapseEvent.valid_keys + ["redacts"]
def get_content_template(self):
return {}

View file

@ -0,0 +1,64 @@
# -*- 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 .room import (
RoomMemberEvent, RoomJoinRulesEvent, RoomPowerLevelsEvent,
RoomAddStateLevelEvent, RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent,
RoomAliasesEvent, RoomCreateEvent,
)
def prune_event(event):
""" Prunes the given event of 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.
"""
# Remove all extraneous fields.
event.unrecognized_keys = {}
new_content = {}
def add_fields(*fields):
for field in fields:
if field in event.content:
new_content[field] = event.content[field]
if event.type == RoomMemberEvent.TYPE:
add_fields("membership")
elif event.type == RoomCreateEvent.TYPE:
add_fields("creator")
elif event.type == RoomJoinRulesEvent.TYPE:
add_fields("join_rule")
elif event.type == RoomPowerLevelsEvent.TYPE:
# TODO: Actually check these are valid user_ids etc.
add_fields("default")
for k, v in event.content.items():
if k.startswith("@") and isinstance(v, (int, long)):
new_content[k] = v
elif event.type == RoomAddStateLevelEvent.TYPE:
add_fields("level")
elif event.type == RoomSendEventLevelEvent.TYPE:
add_fields("level")
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
add_fields("kick_level", "ban_level", "redact_level")
elif event.type == RoomAliasesEvent.TYPE:
add_fields("aliases")
event.content = new_content
return event

View file

@ -244,6 +244,7 @@ class RoomCreationHandler(BaseHandler):
etype=RoomOpsPowerLevelsEvent.TYPE,
ban_level=50,
kick_level=50,
redact_level=50,
)
return [

View file

@ -19,7 +19,7 @@ from twisted.internet import defer
from base import RestServlet, client_path_pattern
from synapse.api.errors import SynapseError, Codes
from synapse.streams.config import PaginationConfig
from synapse.api.events.room import RoomMemberEvent
from synapse.api.events.room import RoomMemberEvent, RoomRedactionEvent
from synapse.api.constants import Membership
import json
@ -431,6 +431,41 @@ class RoomMembershipRestServlet(RestServlet):
self.txns.store_client_transaction(request, txn_id, response)
defer.returnValue(response)
class RoomRedactEventRestServlet(RestServlet):
def register(self, http_server):
PATTERN = ("/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)")
register_txn_path(self, PATTERN, http_server)
@defer.inlineCallbacks
def on_POST(self, request, room_id, event_id):
user = yield self.auth.get_user_by_req(request)
content = _parse_json(request)
event = self.event_factory.create_event(
etype=RoomRedactionEvent.TYPE,
room_id=urllib.unquote(room_id),
user_id=user.to_string(),
content=content,
redacts=event_id,
)
msg_handler = self.handlers.message_handler
yield msg_handler.send_message(event)
defer.returnValue((200, {"event_id": event.event_id}))
@defer.inlineCallbacks
def on_PUT(self, request, room_id, event_id, txn_id):
try:
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
except KeyError:
pass
response = yield self.on_POST(request, room_id, event_id)
self.txns.store_client_transaction(request, txn_id, response)
defer.returnValue(response)
def _parse_json(request):
try:
@ -486,3 +521,4 @@ def register_servlets(hs, http_server):
PublicRoomListRestServlet(hs).register(http_server)
RoomStateRestServlet(hs).register(http_server)
RoomInitialSyncRestServlet(hs).register(http_server)
RoomRedactEventRestServlet(hs).register(http_server)

View file

@ -24,6 +24,7 @@ from synapse.api.events.room import (
RoomAddStateLevelEvent,
RoomSendEventLevelEvent,
RoomOpsPowerLevelsEvent,
RoomRedactionEvent,
)
from synapse.util.logutils import log_function
@ -56,12 +57,13 @@ SCHEMAS = [
"presence",
"im",
"room_aliases",
"redactions",
]
# Remember to update this number every time an incompatible change is made to
# database schema files, so the users will be informed on server restarts.
SCHEMA_VERSION = 3
SCHEMA_VERSION = 4
class _RollbackButIsFineException(Exception):
@ -182,6 +184,8 @@ class DataStore(RoomMemberStore, RoomStore,
self._store_send_event_level(txn, event)
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
self._store_ops_level(txn, event)
elif event.type == RoomRedactionEvent.TYPE:
self._store_redaction(txn, event)
vals = {
"topological_ordering": event.depth,
@ -203,7 +207,7 @@ class DataStore(RoomMemberStore, RoomStore,
unrec = {
k: v
for k, v in event.get_full_dict().items()
if k not in vals.keys()
if k not in vals.keys() and k not in ["redacted", "redacted_because"]
}
vals["unrecognized_keys"] = json.dumps(unrec)
@ -242,14 +246,28 @@ class DataStore(RoomMemberStore, RoomStore,
}
)
def _store_redaction(self, txn, event):
txn.execute(
"INSERT OR IGNORE INTO redactions "
"(event_id, redacts) VALUES (?,?)",
(event.event_id, event.redacts)
)
@defer.inlineCallbacks
def get_current_state(self, room_id, event_type=None, state_key=""):
del_sql = (
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
"LIMIT 1"
)
sql = (
"SELECT e.* FROM events as e "
"SELECT e.*, (%(redacted)s) AS redacted FROM events as e "
"INNER JOIN current_state_events as c ON e.event_id = c.event_id "
"INNER JOIN state_events as s ON e.event_id = s.event_id "
"WHERE c.room_id = ? "
)
) % {
"redacted": del_sql,
}
if event_type:
sql += " AND s.type = ? AND s.state_key = ? "

View file

@ -17,6 +17,7 @@ import logging
from twisted.internet import defer
from synapse.api.errors import StoreError
from synapse.api.events.utils import prune_event
from synapse.util.logutils import log_function
import collections
@ -345,7 +346,7 @@ class SQLBaseStore(object):
return self.runInteraction(func)
def _parse_event_from_row(self, row_dict):
d = copy.deepcopy({k: v for k, v in row_dict.items() if v})
d = copy.deepcopy({k: v for k, v in row_dict.items()})
d.pop("stream_ordering", None)
d.pop("topological_ordering", None)
@ -373,8 +374,8 @@ class SQLBaseStore(object):
sql = "SELECT * FROM events WHERE event_id = ?"
for ev in events:
if hasattr(ev, "prev_state"):
# Load previous state_content.
if hasattr(ev, "prev_state"):
# Load previous state_content.
# TODO: Should we be pulling this out above?
cursor = txn.execute(sql, (ev.prev_state,))
prevs = self.cursor_to_dict(cursor)
@ -382,8 +383,32 @@ class SQLBaseStore(object):
prev = self._parse_event_from_row(prevs[0])
ev.prev_content = prev.content
if not hasattr(ev, "redacted"):
logger.debug("Doesn't have redacted key: %s", ev)
ev.redacted = self._has_been_redacted_txn(txn, ev)
if ev.redacted:
# Get the redaction event.
sql = "SELECT * FROM events WHERE event_id = ?"
txn.execute(sql, (ev.redacted,))
del_evs = self._parse_events_txn(
txn, self.cursor_to_dict(txn)
)
if del_evs:
prune_event(ev)
ev.redacted_because = del_evs[0]
return events
def _has_been_redacted_txn(self, txn, event):
sql = "SELECT event_id FROM redactions WHERE redacts = ?"
txn.execute(sql, (event.event_id,))
result = txn.fetchone()
return result[0] if result else None
class Table(object):
""" A base class used to store information about a particular table.
"""

View file

@ -27,7 +27,7 @@ import logging
logger = logging.getLogger(__name__)
OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level"))
OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level", "redact_level"))
class RoomStore(SQLBaseStore):
@ -189,7 +189,8 @@ class RoomStore(SQLBaseStore):
def _get_ops_levels(self, txn, room_id):
sql = (
"SELECT ban_level, kick_level FROM room_ops_levels as r "
"SELECT ban_level, kick_level, redact_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 = ? "
@ -198,7 +199,7 @@ class RoomStore(SQLBaseStore):
rows = txn.execute(sql, (room_id,)).fetchall()
if len(rows) == 1:
return OpsLevel(rows[0][0], rows[0][1])
return OpsLevel(rows[0][0], rows[0][1], rows[0][2])
else:
return OpsLevel(None, None)
@ -326,6 +327,9 @@ class RoomStore(SQLBaseStore):
if "ban_level" in event.content:
content["ban_level"] = event.content["ban_level"]
if "redact_level" in event.content:
content["redact_level"] = event.content["redact_level"]
self._simple_insert_txn(
txn,
"room_ops_levels",

View file

@ -182,14 +182,22 @@ class RoomMemberStore(SQLBaseStore):
)
def _get_members_query_txn(self, txn, where_clause, where_values):
del_sql = (
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
"LIMIT 1"
)
sql = (
"SELECT e.* FROM events as e "
"SELECT e.*, (%(redacted)s) AS redacted FROM events as e "
"INNER JOIN room_memberships as m "
"ON e.event_id = m.event_id "
"INNER JOIN current_state_events as c "
"ON m.event_id = c.event_id "
"WHERE %s "
) % (where_clause,)
"WHERE %(where)s "
) % {
"redacted": del_sql,
"where": where_clause,
}
txn.execute(sql, where_values)
rows = self.cursor_to_dict(txn)

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS redactions (
event_id TEXT NOT NULL,
redacts TEXT NOT NULL,
CONSTRAINT ev_uniq UNIQUE (event_id)
);
CREATE INDEX IF NOT EXISTS redactions_event_id ON redactions (event_id);
CREATE INDEX IF NOT EXISTS redactions_redacts ON redactions (redacts);
ALTER TABLE room_ops_levels ADD COLUMN redact_level INTEGER;
PRAGMA user_version = 4;

View file

@ -150,7 +150,8 @@ 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,
redact_level INTEGER
);
CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS redactions (
event_id TEXT NOT NULL,
redacts TEXT NOT NULL,
CONSTRAINT ev_uniq UNIQUE (event_id)
);
CREATE INDEX IF NOT EXISTS redactions_event_id ON redactions (event_id);
CREATE INDEX IF NOT EXISTS redactions_redacts ON redactions (redacts);

View file

@ -157,6 +157,11 @@ class StreamStore(SQLBaseStore):
"WHERE m.user_id = ? "
)
del_sql = (
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
"LIMIT 1"
)
if limit:
limit = max(limit, MAX_STREAM_SIZE)
else:
@ -171,13 +176,14 @@ class StreamStore(SQLBaseStore):
return
sql = (
"SELECT * FROM events as e WHERE "
"SELECT *, (%(redacted)s) AS redacted FROM events AS e WHERE "
"((room_id IN (%(current)s)) OR "
"(event_id IN (%(invites)s))) "
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
"AND e.outlier = 0 "
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
) % {
"redacted": del_sql,
"current": current_room_membership_sql,
"invites": membership_sql,
"limit": limit
@ -224,11 +230,21 @@ class StreamStore(SQLBaseStore):
else:
limit_str = ""
del_sql = (
"SELECT event_id FROM redactions WHERE redacts = events.event_id "
"LIMIT 1"
)
sql = (
"SELECT * FROM events "
"SELECT *, (%(redacted)s) AS redacted FROM events "
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
"ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s "
) % {"bounds": bounds, "order": order, "limit": limit_str}
) % {
"redacted": del_sql,
"bounds": bounds,
"order": order,
"limit": limit_str
}
rows = yield self._execute_and_decode(
sql,
@ -257,11 +273,18 @@ class StreamStore(SQLBaseStore):
with_feedback=False):
# TODO (erikj): Handle compressed feedback
del_sql = (
"SELECT event_id FROM redactions WHERE redacts = events.event_id "
"LIMIT 1"
)
sql = (
"SELECT * FROM events "
"SELECT *, (%(redacted)s) AS redacted FROM events "
"WHERE room_id = ? AND stream_ordering <= ? "
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
)
) % {
"redacted": del_sql,
}
rows = yield self._execute_and_decode(
sql,

View file

@ -0,0 +1,262 @@
# -*- 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 tests import unittest
from twisted.internet import defer
from synapse.server import HomeServer
from synapse.api.constants import Membership
from synapse.api.events.room import (
RoomMemberEvent, MessageEvent, RoomRedactionEvent,
)
from tests.utils import SQLiteMemoryDbPool
class RedactionTestCase(unittest.TestCase):
@defer.inlineCallbacks
def setUp(self):
db_pool = SQLiteMemoryDbPool()
yield db_pool.prepare()
hs = HomeServer(
"test",
db_pool=db_pool,
)
self.store = hs.get_datastore()
self.event_factory = hs.get_event_factory()
self.u_alice = hs.parse_userid("@alice:test")
self.u_bob = hs.parse_userid("@bob:test")
self.room1 = hs.parse_roomid("!abc123:test")
self.depth = 1
@defer.inlineCallbacks
def inject_room_member(self, room, user, membership, prev_state=None,
extra_content={}):
self.depth += 1
event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
user_id=user.to_string(),
state_key=user.to_string(),
room_id=room.to_string(),
membership=membership,
content={"membership": membership},
depth=self.depth,
)
event.content.update(extra_content)
if prev_state:
event.prev_state = prev_state
# Have to create a join event using the eventfactory
yield self.store.persist_event(
event
)
defer.returnValue(event)
@defer.inlineCallbacks
def inject_message(self, room, user, body):
self.depth += 1
event = self.event_factory.create_event(
etype=MessageEvent.TYPE,
user_id=user.to_string(),
room_id=room.to_string(),
content={"body": body, "msgtype": u"message"},
depth=self.depth,
)
yield self.store.persist_event(
event
)
defer.returnValue(event)
@defer.inlineCallbacks
def inject_redaction(self, room, event_id, user, reason):
event = self.event_factory.create_event(
etype=RoomRedactionEvent.TYPE,
user_id=user.to_string(),
room_id=room.to_string(),
content={"reason": reason},
depth=self.depth,
redacts=event_id,
)
yield self.store.persist_event(
event
)
defer.returnValue(event)
@defer.inlineCallbacks
def test_redact(self):
yield self.inject_room_member(
self.room1, self.u_alice, Membership.JOIN
)
start = yield self.store.get_room_events_max_id()
msg_event = yield self.inject_message(self.room1, self.u_alice, u"t")
end = yield self.store.get_room_events_max_id()
results, _ = yield self.store.get_room_events_stream(
self.u_alice.to_string(),
start,
end,
None, # Is currently ignored
)
self.assertEqual(1, len(results))
# Check event has not been redacted:
event = results[0]
self.assertObjectHasAttributes(
{
"type": MessageEvent.TYPE,
"user_id": self.u_alice.to_string(),
"content": {"body": "t", "msgtype": "message"},
},
event,
)
self.assertFalse(hasattr(event, "redacted_because"))
# Redact event
reason = "Because I said so"
yield self.inject_redaction(
self.room1, msg_event.event_id, self.u_alice, reason
)
results, _ = yield self.store.get_room_events_stream(
self.u_alice.to_string(),
start,
end,
None, # Is currently ignored
)
self.assertEqual(1, len(results))
# Check redaction
event = results[0]
self.assertObjectHasAttributes(
{
"type": MessageEvent.TYPE,
"user_id": self.u_alice.to_string(),
"content": {},
},
event,
)
self.assertTrue(hasattr(event, "redacted_because"))
self.assertObjectHasAttributes(
{
"type": RoomRedactionEvent.TYPE,
"user_id": self.u_alice.to_string(),
"content": {"reason": reason},
},
event.redacted_because,
)
@defer.inlineCallbacks
def test_redact_join(self):
yield self.inject_room_member(
self.room1, self.u_alice, Membership.JOIN
)
start = yield self.store.get_room_events_max_id()
msg_event = yield self.inject_room_member(
self.room1, self.u_bob, Membership.JOIN,
extra_content={"blue": "red"},
)
end = yield self.store.get_room_events_max_id()
results, _ = yield self.store.get_room_events_stream(
self.u_alice.to_string(),
start,
end,
None, # Is currently ignored
)
self.assertEqual(1, len(results))
# Check event has not been redacted:
event = results[0]
self.assertObjectHasAttributes(
{
"type": RoomMemberEvent.TYPE,
"user_id": self.u_bob.to_string(),
"content": {"membership": Membership.JOIN, "blue": "red"},
},
event,
)
self.assertFalse(hasattr(event, "redacted_because"))
# Redact event
reason = "Because I said so"
yield self.inject_redaction(
self.room1, msg_event.event_id, self.u_alice, reason
)
results, _ = yield self.store.get_room_events_stream(
self.u_alice.to_string(),
start,
end,
None, # Is currently ignored
)
self.assertEqual(1, len(results))
# Check redaction
event = results[0]
self.assertObjectHasAttributes(
{
"type": RoomMemberEvent.TYPE,
"user_id": self.u_bob.to_string(),
"content": {"membership": Membership.JOIN},
},
event,
)
self.assertTrue(hasattr(event, "redacted_because"))
self.assertObjectHasAttributes(
{
"type": RoomRedactionEvent.TYPE,
"user_id": self.u_alice.to_string(),
"content": {"reason": reason},
},
event.redacted_because,
)

View file

@ -262,7 +262,7 @@ class MemoryDataStore(object):
return defer.succeed("invite")
def get_ops_levels(self, room_id):
return defer.succeed((5, 5))
return defer.succeed((5, 5, 5))
def _format_call(args, kwargs):