Merge branch 'develop' into key_distribution

pull/4/merge
Mark Haines 10 years ago
commit eede182df7
  1. 34
      UPGRADE.rst
  2. 50
      docs/metrics-howto.rst
  3. 88
      synapse/api/auth.py
  4. 4
      synapse/api/errors.py
  5. 51
      synapse/handlers/presence.py
  6. 14
      synapse/push/baserules.py
  7. 14
      synapse/push/rulekinds.py
  8. 14
      synapse/python_dependencies.py
  9. 14
      synapse/rest/media/v1/identicon_resource.py
  10. 14
      synapse/storage/schema/delta/14/upgrade_appservice_db.py
  11. 14
      synapse/storage/schema/delta/14/v14.sql
  12. 65
      tests/handlers/test_presence.py

@ -1,3 +1,37 @@
Upgrading to v0.x.x
===================
Application services have had a breaking API change in this version.
They can no longer register themselves with a home server using the AS HTTP API. This
decision was made because a compromised application service with free reign to register
any regex in effect grants full read/write access to the home server if a regex of ``.*``
is used. An attack where a compromised AS re-registers itself with ``.*`` was deemed too
big of a security risk to ignore, and so the ability to register with the HS remotely has
been removed.
It has been replaced by specifying a list of application service registrations in
``homeserver.yaml``::
app_service_config_files: ["registration-01.yaml", "registration-02.yaml"]
Where ``registration-01.yaml`` looks like::
url: <String> # e.g. "https://my.application.service.com"
as_token: <String>
hs_token: <String>
sender_localpart: <String> # This is a new field which denotes the user_id localpart when using the AS token
namespaces:
users:
- exclusive: <Boolean>
regex: <String> # e.g. "@prefix_.*"
aliases:
- exclusive: <Boolean>
regex: <String>
rooms:
- exclusive: <Boolean>
regex: <String>
Upgrading to v0.8.0 Upgrading to v0.8.0
=================== ===================

@ -0,0 +1,50 @@
How to monitor Synapse metrics using Prometheus
===============================================
1: Install prometheus:
Follow instructions at http://prometheus.io/docs/introduction/install/
2: Enable synapse metrics:
Simply setting a (local) port number will enable it. Pick a port.
prometheus itself defaults to 9090, so starting just above that for
locally monitored services seems reasonable. E.g. 9092:
Add to homeserver.yaml
metrics_port: 9092
Restart synapse
3: Check out synapse-prometheus-config
https://github.com/matrix-org/synapse-prometheus-config
4: Add ``synapse.html`` and ``synapse.rules``
The ``.html`` file needs to appear in prometheus's ``consoles`` directory,
and the ``.rules`` file needs to be invoked somewhere in the main config
file. A symlink to each from the git checkout into the prometheus directory
might be easiest to ensure ``git pull`` keeps it updated.
5: Add a prometheus target for synapse
This is easiest if prometheus runs on the same machine as synapse, as it can
then just use localhost::
global: {
rule_file: "synapse.rules"
}
job: {
name: "synapse"
target_group: {
target: "http://localhost:9092/"
}
}
6: Start prometheus::
./prometheus -config.file=prometheus.conf
7: Wait a few seconds for it to start and perform the first scrape,
then visit the console:
http://server-where-prometheus-runs:9090/consoles/synapse.html

@ -183,18 +183,10 @@ class Auth(object):
else: else:
join_rule = JoinRules.INVITE join_rule = JoinRules.INVITE
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
ban_level, kick_level, redact_level = ( # FIXME (erikj): What should we do here as the default?
self._get_ops_level_from_event_state( ban_level = self._get_named_level(auth_events, "ban", 50)
event,
auth_events,
)
)
logger.debug( logger.debug(
"is_membership_change_allowed: %s", "is_membership_change_allowed: %s",
@ -210,11 +202,6 @@ class Auth(object):
} }
) )
if ban_level:
ban_level = int(ban_level)
else:
ban_level = 50 # FIXME (erikj): What should we do here?
if Membership.JOIN != membership: if Membership.JOIN != membership:
# JOIN is the only action you can perform if you're not in the room # JOIN is the only action you can perform if you're not in the room
if not caller_in_room: # caller isn't joined if not caller_in_room: # caller isn't joined
@ -259,10 +246,7 @@ class Auth(object):
403, "You cannot unban user &s." % (target_user_id,) 403, "You cannot unban user &s." % (target_user_id,)
) )
elif target_user_id != event.user_id: elif target_user_id != event.user_id:
if kick_level: kick_level = self._get_named_level(auth_events, "kick", 50)
kick_level = int(kick_level)
else:
kick_level = 50 # FIXME (erikj): What should we do here?
if user_level < kick_level: if user_level < kick_level:
raise AuthError( raise AuthError(
@ -276,34 +260,42 @@ class Auth(object):
return True return True
def _get_power_level_from_event_state(self, event, user_id, auth_events): def _get_power_level_event(self, auth_events):
key = (EventTypes.PowerLevels, "", ) key = (EventTypes.PowerLevels, "", )
power_level_event = auth_events.get(key) return auth_events.get(key)
level = None
def _get_user_power_level(self, user_id, auth_events):
power_level_event = self._get_power_level_event(auth_events)
if power_level_event: if power_level_event:
level = power_level_event.content.get("users", {}).get(user_id) level = power_level_event.content.get("users", {}).get(user_id)
if not level: if not level:
level = power_level_event.content.get("users_default", 0) level = power_level_event.content.get("users_default", 0)
if level is None:
return 0
else:
return int(level)
else: else:
key = (EventTypes.Create, "", ) key = (EventTypes.Create, "", )
create_event = auth_events.get(key) create_event = auth_events.get(key)
if (create_event is not None and if (create_event is not None and
create_event.content["creator"] == user_id): create_event.content["creator"] == user_id):
return 100 return 100
else:
return 0
return level def _get_named_level(self, auth_events, name, default):
power_level_event = self._get_power_level_event(auth_events)
def _get_ops_level_from_event_state(self, event, auth_events): if not power_level_event:
key = (EventTypes.PowerLevels, "", ) return default
power_level_event = auth_events.get(key)
if power_level_event: level = power_level_event.content.get(name, None)
return ( if level is not None:
power_level_event.content.get("ban", 50), return int(level)
power_level_event.content.get("kick", 50), else:
power_level_event.content.get("redact", 50), return default
)
return None, None, None,
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_by_req(self, request): def get_user_by_req(self, request):
@ -498,16 +490,7 @@ class Auth(object):
else: else:
send_level = 0 send_level = 0
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
if user_level:
user_level = int(user_level)
else:
user_level = 0
if user_level < send_level: if user_level < send_level:
raise AuthError( raise AuthError(
@ -539,16 +522,9 @@ class Auth(object):
return True return True
def _check_redaction(self, event, auth_events): def _check_redaction(self, event, auth_events):
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
_, _, redact_level = self._get_ops_level_from_event_state( redact_level = self._get_named_level(auth_events, "redact", 50)
event,
auth_events,
)
if user_level < redact_level: if user_level < redact_level:
raise AuthError( raise AuthError(
@ -576,11 +552,7 @@ class Auth(object):
if not current_state: if not current_state:
return return
user_level = self._get_power_level_from_event_state( user_level = self._get_user_power_level(event.user_id, auth_events)
event,
event.user_id,
auth_events,
)
# Check other levels: # Check other levels:
levels_to_check = [ levels_to_check = [

@ -35,8 +35,8 @@ class Codes(object):
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
CAPTCHA_INVALID = "M_CAPTCHA_INVALID" CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
MISSING_PARAM = "M_MISSING_PARAM", MISSING_PARAM = "M_MISSING_PARAM"
TOO_LARGE = "M_TOO_LARGE", TOO_LARGE = "M_TOO_LARGE"
EXCLUSIVE = "M_EXCLUSIVE" EXCLUSIVE = "M_EXCLUSIVE"

@ -36,6 +36,9 @@ metrics = synapse.metrics.get_metrics_for(__name__)
# Don't bother bumping "last active" time if it differs by less than 60 seconds # Don't bother bumping "last active" time if it differs by less than 60 seconds
LAST_ACTIVE_GRANULARITY = 60*1000 LAST_ACTIVE_GRANULARITY = 60*1000
# Keep no more than this number of offline serial revisions
MAX_OFFLINE_SERIALS = 1000
# TODO(paul): Maybe there's one of these I can steal from somewhere # TODO(paul): Maybe there's one of these I can steal from somewhere
def partition(l, func): def partition(l, func):
@ -135,6 +138,9 @@ class PresenceHandler(BaseHandler):
self._remote_sendmap = {} self._remote_sendmap = {}
# map remote users to sets of local users who're interested in them # map remote users to sets of local users who're interested in them
self._remote_recvmap = {} self._remote_recvmap = {}
# list of (serial, set of(userids)) tuples, ordered by serial, latest
# first
self._remote_offline_serials = []
# map any user to a UserPresenceCache # map any user to a UserPresenceCache
self._user_cachemap = {} self._user_cachemap = {}
@ -714,8 +720,24 @@ class PresenceHandler(BaseHandler):
statuscache=statuscache, statuscache=statuscache,
) )
user_id = user.to_string()
if state["presence"] == PresenceState.OFFLINE: if state["presence"] == PresenceState.OFFLINE:
self._remote_offline_serials.insert(
0,
(self._user_cachemap_latest_serial, set([user_id]))
)
while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS:
self._remote_offline_serials.pop() # remove the oldest
del self._user_cachemap[user] del self._user_cachemap[user]
else:
# Remove the user from remote_offline_serials now that they're
# no longer offline
for idx, elem in enumerate(self._remote_offline_serials):
(_, user_ids) = elem
user_ids.discard(user_id)
if not user_ids:
self._remote_offline_serials.pop(idx)
for poll in content.get("poll", []): for poll in content.get("poll", []):
user = UserID.from_string(poll) user = UserID.from_string(poll)
@ -836,6 +858,8 @@ class PresenceEventSource(object):
presence = self.hs.get_handlers().presence_handler presence = self.hs.get_handlers().presence_handler
cachemap = presence._user_cachemap cachemap = presence._user_cachemap
clock = self.clock
latest_serial = None
updates = [] updates = []
# TODO(paul): use a DeferredList ? How to limit concurrency. # TODO(paul): use a DeferredList ? How to limit concurrency.
@ -845,18 +869,31 @@ class PresenceEventSource(object):
if cached.serial <= from_key: if cached.serial <= from_key:
continue continue
if (yield self.is_visible(observer_user, observed_user)): if not (yield self.is_visible(observer_user, observed_user)):
updates.append((observed_user, cached)) continue
if latest_serial is None or cached.serial > latest_serial:
latest_serial = cached.serial
updates.append(cached.make_event(user=observed_user, clock=clock))
# TODO(paul): limit # TODO(paul): limit
if updates: for serial, user_ids in presence._remote_offline_serials:
clock = self.clock if serial < from_key:
break
latest_serial = max([x[1].serial for x in updates]) for u in user_ids:
data = [x[1].make_event(user=x[0], clock=clock) for x in updates] updates.append({
"type": "m.presence",
"content": {"user_id": u, "presence": PresenceState.OFFLINE},
})
# TODO(paul): For the v2 API we want to tell the client their from_key
# is too old if we fell off the end of the _remote_offline_serials
# list, and get them to invalidate+resync. In v1 we have no such
# concept so this is a best-effort result.
defer.returnValue((data, latest_serial)) if updates:
defer.returnValue((updates, latest_serial))
else: else:
defer.returnValue(([], presence._user_cachemap_latest_serial)) defer.returnValue(([], presence._user_cachemap_latest_serial))

@ -1,3 +1,17 @@
# Copyright 2015 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.push.rulekinds import PRIORITY_CLASS_MAP, PRIORITY_CLASS_INVERSE_MAP from synapse.push.rulekinds import PRIORITY_CLASS_MAP, PRIORITY_CLASS_INVERSE_MAP

@ -1,3 +1,17 @@
# Copyright 2015 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.
PRIORITY_CLASS_MAP = { PRIORITY_CLASS_MAP = {
'underride': 1, 'underride': 1,
'sender': 2, 'sender': 2,

@ -1,3 +1,17 @@
# Copyright 2015 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.
import logging import logging
from distutils.version import LooseVersion from distutils.version import LooseVersion

@ -1,3 +1,17 @@
# Copyright 2015 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 pydenticon import Generator from pydenticon import Generator
from twisted.web.resource import Resource from twisted.web.resource import Resource

@ -1,3 +1,17 @@
# Copyright 2015 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.
import json import json
import logging import logging

@ -1,3 +1,17 @@
/* Copyright 2015 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.
*/
CREATE TABLE IF NOT EXISTS push_rules_enable ( CREATE TABLE IF NOT EXISTS push_rules_enable (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_name TEXT NOT NULL, user_name TEXT NOT NULL,

@ -878,6 +878,71 @@ class PresencePushTestCase(MockedDatastorePresenceTestCase):
state state
) )
@defer.inlineCallbacks
def test_recv_remote_offline(self):
""" Various tests relating to SYN-261 """
potato_set = self.handler._remote_recvmap.setdefault(self.u_potato,
set())
potato_set.add(self.u_apple)
self.room_members = [self.u_banana, self.u_potato]
self.assertEquals(self.event_source.get_current_key(), 0)
yield self.mock_federation_resource.trigger("PUT",
"/_matrix/federation/v1/send/1000000/",
_make_edu_json("elsewhere", "m.presence",
content={
"push": [
{"user_id": "@potato:remote",
"presence": "offline"},
],
}
)
)
self.assertEquals(self.event_source.get_current_key(), 1)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"presence": OFFLINE,
}}
]
)
yield self.mock_federation_resource.trigger("PUT",
"/_matrix/federation/v1/send/1000001/",
_make_edu_json("elsewhere", "m.presence",
content={
"push": [
{"user_id": "@potato:remote",
"presence": "online"},
],
}
)
)
self.assertEquals(self.event_source.get_current_key(), 2)
(events, _) = yield self.event_source.get_new_events_for_user(
self.u_apple, 0, None
)
self.assertEquals(events,
[
{"type": "m.presence",
"content": {
"user_id": "@potato:remote",
"presence": ONLINE,
}}
]
)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_join_room_local(self): def test_join_room_local(self):
self.room_members = [self.u_apple, self.u_banana] self.room_members = [self.u_apple, self.u_banana]

Loading…
Cancel
Save