diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 9e445cd808..06ade25674 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -14,6 +14,7 @@
# limitations under the License.
import logging
+from typing import Optional
from six import itervalues
@@ -21,21 +22,23 @@ import pymacaroons
from netaddr import IPAddress
from twisted.internet import defer
+from twisted.web.server import Request
import synapse.logging.opentracing as opentracing
import synapse.types
from synapse import event_auth
-from synapse.api.constants import EventTypes, JoinRules, Membership, UserTypes
+from synapse.api.auth_blocking import AuthBlocking
+from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import (
AuthError,
Codes,
InvalidClientTokenError,
MissingClientTokenError,
- ResourceLimitError,
)
-from synapse.config.server import is_threepid_reserved
-from synapse.types import UserID
-from synapse.util.caches import CACHE_SIZE_FACTOR, register_cache
+from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
+from synapse.events import EventBase
+from synapse.types import StateMap, UserID
+from synapse.util.caches import register_cache
from synapse.util.caches.lrucache import LruCache
from synapse.util.metrics import Measure
@@ -71,55 +74,58 @@ class Auth(object):
self.store = hs.get_datastore()
self.state = hs.get_state_handler()
- self.token_cache = LruCache(CACHE_SIZE_FACTOR * 10000)
+ self.token_cache = LruCache(10000)
register_cache("cache", "token_cache", self.token_cache)
+ self._auth_blocking = AuthBlocking(self.hs)
+
self._account_validity = hs.config.account_validity
+ self._track_appservice_user_ips = hs.config.track_appservice_user_ips
+ self._macaroon_secret_key = hs.config.macaroon_secret_key
@defer.inlineCallbacks
- def check_from_context(self, room_version, event, context, do_sig_check=True):
- prev_state_ids = yield context.get_prev_state_ids(self.store)
+ def check_from_context(self, room_version: str, event, context, do_sig_check=True):
+ prev_state_ids = yield context.get_prev_state_ids()
auth_events_ids = yield self.compute_auth_events(
event, prev_state_ids, for_verification=True
)
auth_events = yield self.store.get_events(auth_events_ids)
auth_events = {(e.type, e.state_key): e for e in itervalues(auth_events)}
- self.check(
- room_version, event, auth_events=auth_events, do_sig_check=do_sig_check
- )
- def check(self, room_version, event, auth_events, do_sig_check=True):
- """ Checks if this event is correctly authed.
+ room_version_obj = KNOWN_ROOM_VERSIONS[room_version]
+ event_auth.check(
+ room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check
+ )
+ @defer.inlineCallbacks
+ def check_user_in_room(
+ self,
+ room_id: str,
+ user_id: str,
+ current_state: Optional[StateMap[EventBase]] = None,
+ allow_departed_users: bool = False,
+ ):
+ """Check if the user is in the room, or was at some point.
Args:
- room_version (str): version of the room
- event: the event being checked.
- auth_events (dict: event-key -> event): the existing room state.
+ room_id: The room to check.
+ user_id: The user to check.
- Returns:
- True if the auth checks pass.
- """
- with Measure(self.clock, "auth.check"):
- event_auth.check(
- room_version, event, auth_events, do_sig_check=do_sig_check
- )
-
- @defer.inlineCallbacks
- def check_joined_room(self, room_id, user_id, current_state=None):
- """Check if the user is currently joined in the room
- Args:
- room_id(str): The room to check.
- user_id(str): The user to check.
- current_state(dict): Optional map of the current state of the room.
+ current_state: Optional map of the current state of the room.
If provided then that map is used to check whether they are a
member of the room. Otherwise the current membership is
loaded from the database.
+
+ allow_departed_users: if True, accept users that were previously
+ members but have now departed.
+
Raises:
- AuthError if the user is not in the room.
+ AuthError if the user is/was not in the room.
Returns:
- A deferred membership event for the user if the user is in
- the room.
+ Deferred[Optional[EventBase]]:
+ Membership event for the user if the user was in the
+ room. This will be the join event if they are currently joined to
+ the room. This will be the leave event if they have left the room.
"""
if current_state:
member = current_state.get((EventTypes.Member, user_id), None)
@@ -127,37 +133,19 @@ class Auth(object):
member = yield self.state.get_current_state(
room_id=room_id, event_type=EventTypes.Member, state_key=user_id
)
-
- self._check_joined_room(member, user_id, room_id)
- return member
-
- @defer.inlineCallbacks
- def check_user_was_in_room(self, room_id, user_id):
- """Check if the user was in the room at some point.
- Args:
- room_id(str): The room to check.
- user_id(str): The user to check.
- Raises:
- AuthError if the user was never in the room.
- Returns:
- A deferred membership event for the user if the user was in the
- room. This will be the join event if they are currently joined to
- the room. This will be the leave event if they have left the room.
- """
- member = yield self.state.get_current_state(
- room_id=room_id, event_type=EventTypes.Member, state_key=user_id
- )
membership = member.membership if member else None
- if membership not in (Membership.JOIN, Membership.LEAVE):
- raise AuthError(403, "User %s not in room %s" % (user_id, room_id))
+ if membership == Membership.JOIN:
+ return member
- if membership == Membership.LEAVE:
+ # XXX this looks totally bogus. Why do we not allow users who have been banned,
+ # or those who were members previously and have been re-invited?
+ if allow_departed_users and membership == Membership.LEAVE:
forgot = yield self.store.did_forget(user_id, room_id)
- if forgot:
- raise AuthError(403, "User %s not in room %s" % (user_id, room_id))
+ if not forgot:
+ return member
- return member
+ raise AuthError(403, "User %s not in room %s" % (user_id, room_id))
@defer.inlineCallbacks
def check_host_in_room(self, room_id, host):
@@ -165,12 +153,6 @@ class Auth(object):
latest_event_ids = yield self.store.is_host_joined(room_id, host)
return latest_event_ids
- def _check_joined_room(self, member, user_id, room_id):
- if not member or member.membership != Membership.JOIN:
- raise AuthError(
- 403, "User %s not in room %s (%s)" % (user_id, room_id, repr(member))
- )
-
def can_federate(self, event, auth_events):
creation_event = auth_events.get((EventTypes.Create, ""))
@@ -179,22 +161,27 @@ class Auth(object):
def get_public_keys(self, invite_event):
return event_auth.get_public_keys(invite_event)
- @opentracing.trace
@defer.inlineCallbacks
def get_user_by_req(
- self, request, allow_guest=False, rights="access", allow_expired=False
+ self,
+ request: Request,
+ allow_guest: bool = False,
+ rights: str = "access",
+ allow_expired: bool = False,
):
""" Get a registered user's ID.
Args:
- request - An HTTP request with an access_token query parameter.
- allow_expired - Whether to allow the request through even if the account is
- expired. If true, Synapse will still require an access token to be
- provided but won't check if the account it belongs to has expired. This
- works thanks to /login delivering access tokens regardless of accounts'
- expiration.
+ request: An HTTP request with an access_token query parameter.
+ allow_guest: If False, will raise an AuthError if the user making the
+ request is a guest.
+ rights: The operation being performed; the access token must allow this
+ allow_expired: If True, allow the request through even if the account
+ is expired, or session token lifetime has ended. Note that
+ /login will deliver access tokens regardless of expiration.
+
Returns:
- defer.Deferred: resolves to a ``synapse.types.Requester`` object
+ defer.Deferred: resolves to a `synapse.types.Requester` object
Raises:
InvalidClientCredentialsError if no user by that token exists or the token
is invalid.
@@ -212,8 +199,9 @@ class Auth(object):
if user_id:
request.authenticated_entity = user_id
opentracing.set_tag("authenticated_entity", user_id)
+ opentracing.set_tag("appservice_id", app_service.id)
- if ip_addr and self.hs.config.track_appservice_user_ips:
+ if ip_addr and self._track_appservice_user_ips:
yield self.store.insert_client_ip(
user_id=user_id,
access_token=access_token,
@@ -224,7 +212,9 @@ class Auth(object):
return synapse.types.create_requester(user_id, app_service=app_service)
- user_info = yield self.get_user_by_access_token(access_token, rights)
+ user_info = yield self.get_user_by_access_token(
+ access_token, rights, allow_expired=allow_expired
+ )
user = user_info["user"]
token_id = user_info["token_id"]
is_guest = user_info["is_guest"]
@@ -263,6 +253,8 @@ class Auth(object):
request.authenticated_entity = user.to_string()
opentracing.set_tag("authenticated_entity", user.to_string())
+ if device_id:
+ opentracing.set_tag("device_id", device_id)
return synapse.types.create_requester(
user, token_id, is_guest, device_id, app_service=app_service
@@ -297,13 +289,17 @@ class Auth(object):
return user_id, app_service
@defer.inlineCallbacks
- def get_user_by_access_token(self, token, rights="access"):
+ def get_user_by_access_token(
+ self, token: str, rights: str = "access", allow_expired: bool = False,
+ ):
""" Validate access token and get user_id from it
Args:
- token (str): The access token to get the user by.
- rights (str): The operation being performed; the access token must
- allow this.
+ token: The access token to get the user by
+ rights: The operation being performed; the access token must
+ allow this
+ allow_expired: If False, raises an InvalidClientTokenError
+ if the token is expired
Returns:
Deferred[dict]: dict that includes:
`user` (UserID)
@@ -311,8 +307,10 @@ class Auth(object):
`token_id` (int|None): access token id. May be None if guest
`device_id` (str|None): device corresponding to access token
Raises:
+ InvalidClientTokenError if a user by that token exists, but the token is
+ expired
InvalidClientCredentialsError if no user by that token exists or the token
- is invalid.
+ is invalid
"""
if rights == "access":
@@ -321,7 +319,8 @@ class Auth(object):
if r:
valid_until_ms = r["valid_until_ms"]
if (
- valid_until_ms is not None
+ not allow_expired
+ and valid_until_ms is not None
and valid_until_ms < self.clock.time_msec()
):
# there was a valid access token, but it has expired.
@@ -474,7 +473,7 @@ class Auth(object):
# access_tokens include a nonce for uniqueness: any value is acceptable
v.satisfy_general(lambda c: c.startswith("nonce = "))
- v.verify(macaroon, self.hs.config.macaroon_secret_key)
+ v.verify(macaroon, self._macaroon_secret_key)
def _verify_expiry(self, caveat):
prefix = "time < "
@@ -506,109 +505,77 @@ class Auth(object):
token = self.get_access_token_from_request(request)
service = self.store.get_app_service_by_token(token)
if not service:
- logger.warn("Unrecognised appservice access token.")
+ logger.warning("Unrecognised appservice access token.")
raise InvalidClientTokenError()
request.authenticated_entity = service.sender
return defer.succeed(service)
- def is_server_admin(self, user):
+ async def is_server_admin(self, user: UserID) -> bool:
""" Check if the given user is a local server admin.
Args:
- user (UserID): user to check
+ user: user to check
Returns:
- bool: True if the user is an admin
+ True if the user is an admin
"""
- return self.store.is_server_admin(user)
-
- @defer.inlineCallbacks
- def compute_auth_events(self, event, current_state_ids, for_verification=False):
- if event.type == EventTypes.Create:
- return []
-
- auth_ids = []
+ return await self.store.is_server_admin(user)
- key = (EventTypes.PowerLevels, "")
- power_level_event_id = current_state_ids.get(key)
-
- if power_level_event_id:
- auth_ids.append(power_level_event_id)
-
- key = (EventTypes.JoinRules, "")
- join_rule_event_id = current_state_ids.get(key)
-
- key = (EventTypes.Member, event.sender)
- member_event_id = current_state_ids.get(key)
+ def compute_auth_events(
+ self, event, current_state_ids: StateMap[str], for_verification: bool = False,
+ ):
+ """Given an event and current state return the list of event IDs used
+ to auth an event.
- key = (EventTypes.Create, "")
- create_event_id = current_state_ids.get(key)
- if create_event_id:
- auth_ids.append(create_event_id)
+ If `for_verification` is False then only return auth events that
+ should be added to the event's `auth_events`.
- if join_rule_event_id:
- join_rule_event = yield self.store.get_event(join_rule_event_id)
- join_rule = join_rule_event.content.get("join_rule")
- is_public = join_rule == JoinRules.PUBLIC if join_rule else False
- else:
- is_public = False
+ Returns:
+ defer.Deferred(list[str]): List of event IDs.
+ """
- if event.type == EventTypes.Member:
- e_type = event.content["membership"]
- if e_type in [Membership.JOIN, Membership.INVITE]:
- if join_rule_event_id:
- auth_ids.append(join_rule_event_id)
+ if event.type == EventTypes.Create:
+ return defer.succeed([])
+
+ # Currently we ignore the `for_verification` flag even though there are
+ # some situations where we can drop particular auth events when adding
+ # to the event's `auth_events` (e.g. joins pointing to previous joins
+ # when room is publically joinable). Dropping event IDs has the
+ # advantage that the auth chain for the room grows slower, but we use
+ # the auth chain in state resolution v2 to order events, which means
+ # care must be taken if dropping events to ensure that it doesn't
+ # introduce undesirable "state reset" behaviour.
+ #
+ # All of which sounds a bit tricky so we don't bother for now.
- if e_type == Membership.JOIN:
- if member_event_id and not is_public:
- auth_ids.append(member_event_id)
- else:
- if member_event_id:
- auth_ids.append(member_event_id)
-
- if for_verification:
- key = (EventTypes.Member, event.state_key)
- existing_event_id = current_state_ids.get(key)
- if existing_event_id:
- auth_ids.append(existing_event_id)
-
- if e_type == Membership.INVITE:
- if "third_party_invite" in event.content:
- key = (
- EventTypes.ThirdPartyInvite,
- event.content["third_party_invite"]["signed"]["token"],
- )
- third_party_invite_id = current_state_ids.get(key)
- if third_party_invite_id:
- auth_ids.append(third_party_invite_id)
- elif member_event_id:
- member_event = yield self.store.get_event(member_event_id)
- if member_event.content["membership"] == Membership.JOIN:
- auth_ids.append(member_event.event_id)
+ auth_ids = []
+ for etype, state_key in event_auth.auth_types_for_event(event):
+ auth_ev_id = current_state_ids.get((etype, state_key))
+ if auth_ev_id:
+ auth_ids.append(auth_ev_id)
- return auth_ids
+ return defer.succeed(auth_ids)
- @defer.inlineCallbacks
- def check_can_change_room_list(self, room_id, user):
- """Check if the user is allowed to edit the room's entry in the
+ async def check_can_change_room_list(self, room_id: str, user: UserID):
+ """Determine whether the user is allowed to edit the room's entry in the
published room list.
Args:
- room_id (str)
- user (UserID)
+ room_id
+ user
"""
- is_admin = yield self.is_server_admin(user)
+ is_admin = await self.is_server_admin(user)
if is_admin:
return True
user_id = user.to_string()
- yield self.check_joined_room(room_id, user_id)
+ await self.check_user_in_room(room_id, user_id)
# We currently require the user is a "moderator" in the room. We do this
# by checking if they would (theoretically) be able to change the
- # m.room.aliases events
- power_level_event = yield self.state.get_current_state(
+ # m.room.canonical_alias events
+ power_level_event = await self.state.get_current_state(
room_id, EventTypes.PowerLevels, ""
)
@@ -617,19 +584,14 @@ class Auth(object):
auth_events[(EventTypes.PowerLevels, "")] = power_level_event
send_level = event_auth.get_send_level(
- EventTypes.Aliases, "", power_level_event
+ EventTypes.CanonicalAlias, "", power_level_event
)
user_level = event_auth.get_user_power_level(user_id, auth_events)
- if user_level < send_level:
- raise AuthError(
- 403,
- "This server requires you to be a moderator in the room to"
- " edit its room list entry",
- )
+ return user_level >= send_level
@staticmethod
- def has_access_token(request):
+ def has_access_token(request: Request):
"""Checks if the request has an access_token.
Returns:
@@ -640,7 +602,7 @@ class Auth(object):
return bool(query_params) or bool(auth_headers)
@staticmethod
- def get_access_token_from_request(request):
+ def get_access_token_from_request(request: Request):
"""Extracts the access_token from the request.
Args:
@@ -676,10 +638,18 @@ class Auth(object):
return query_params[0].decode("ascii")
@defer.inlineCallbacks
- def check_in_room_or_world_readable(self, room_id, user_id):
+ def check_user_in_room_or_world_readable(
+ self, room_id: str, user_id: str, allow_departed_users: bool = False
+ ):
"""Checks that the user is or was in the room or the room is world
readable. If it isn't then an exception is raised.
+ Args:
+ room_id: room to check
+ user_id: user to check
+ allow_departed_users: if True, accept users that were previously
+ members but have now departed
+
Returns:
Deferred[tuple[str, str|None]]: Resolves to the current membership of
the user in the room and the membership event ID of the user. If
@@ -688,12 +658,14 @@ class Auth(object):
"""
try:
- # check_user_was_in_room will return the most recent membership
+ # check_user_in_room will return the most recent membership
# event for the user if:
# * The user is a non-guest user, and was ever in the room
# * The user is a guest user, and has joined the room
# else it will throw.
- member_event = yield self.check_user_was_in_room(room_id, user_id)
+ member_event = yield self.check_user_in_room(
+ room_id, user_id, allow_departed_users=allow_departed_users
+ )
return member_event.membership, member_event.event_id
except AuthError:
visibility = yield self.state.get_current_state(
@@ -705,74 +677,10 @@ class Auth(object):
):
return Membership.JOIN, None
raise AuthError(
- 403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN
- )
-
- @defer.inlineCallbacks
- def check_auth_blocking(self, user_id=None, threepid=None, user_type=None):
- """Checks if the user should be rejected for some external reason,
- such as monthly active user limiting or global disable flag
-
- Args:
- user_id(str|None): If present, checks for presence against existing
- MAU cohort
-
- threepid(dict|None): If present, checks for presence against configured
- reserved threepid. Used in cases where the user is trying register
- with a MAU blocked server, normally they would be rejected but their
- threepid is on the reserved list. user_id and
- threepid should never be set at the same time.
-
- user_type(str|None): If present, is used to decide whether to check against
- certain blocking reasons like MAU.
- """
-
- # Never fail an auth check for the server notices users or support user
- # This can be a problem where event creation is prohibited due to blocking
- if user_id is not None:
- if user_id == self.hs.config.server_notices_mxid:
- return
- if (yield self.store.is_support_user(user_id)):
- return
-
- if self.hs.config.hs_disabled:
- raise ResourceLimitError(
403,
- self.hs.config.hs_disabled_message,
- errcode=Codes.RESOURCE_LIMIT_EXCEEDED,
- admin_contact=self.hs.config.admin_contact,
- limit_type=self.hs.config.hs_disabled_limit_type,
+ "User %s not in room %s, and room previews are disabled"
+ % (user_id, room_id),
)
- if self.hs.config.limit_usage_by_mau is True:
- assert not (user_id and threepid)
- # If the user is already part of the MAU cohort or a trial user
- if user_id:
- timestamp = yield self.store.user_last_seen_monthly_active(user_id)
- if timestamp:
- return
-
- is_trial = yield self.store.is_trial_user(user_id)
- if is_trial:
- return
- elif threepid:
- # If the user does not exist yet, but is signing up with a
- # reserved threepid then pass auth check
- if is_threepid_reserved(
- self.hs.config.mau_limits_reserved_threepids, threepid
- ):
- return
- elif user_type == UserTypes.SUPPORT:
- # If the user does not exist yet and is of type "support",
- # allow registration. Support users are excluded from MAU checks.
- return
- # Else if there is no room in the MAU bucket, bail
- current_mau = yield self.store.get_monthly_active_count()
- if current_mau >= self.hs.config.max_mau_value:
- raise ResourceLimitError(
- 403,
- "Monthly Active User Limit Exceeded",
- admin_contact=self.hs.config.admin_contact,
- errcode=Codes.RESOURCE_LIMIT_EXCEEDED,
- limit_type="monthly_active_user",
- )
+ def check_auth_blocking(self, *args, **kwargs):
+ return self._auth_blocking.check_auth_blocking(*args, **kwargs)
diff --git a/synapse/api/auth_blocking.py b/synapse/api/auth_blocking.py
new file mode 100644
index 0000000000..5c499b6b4e
--- /dev/null
+++ b/synapse/api/auth_blocking.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Matrix.org Foundation C.I.C.
+#
+# 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
+
+from twisted.internet import defer
+
+from synapse.api.constants import LimitBlockingTypes, UserTypes
+from synapse.api.errors import Codes, ResourceLimitError
+from synapse.config.server import is_threepid_reserved
+
+logger = logging.getLogger(__name__)
+
+
+class AuthBlocking(object):
+ def __init__(self, hs):
+ self.store = hs.get_datastore()
+
+ self._server_notices_mxid = hs.config.server_notices_mxid
+ self._hs_disabled = hs.config.hs_disabled
+ self._hs_disabled_message = hs.config.hs_disabled_message
+ self._admin_contact = hs.config.admin_contact
+ self._max_mau_value = hs.config.max_mau_value
+ self._limit_usage_by_mau = hs.config.limit_usage_by_mau
+ self._mau_limits_reserved_threepids = hs.config.mau_limits_reserved_threepids
+
+ @defer.inlineCallbacks
+ def check_auth_blocking(self, user_id=None, threepid=None, user_type=None):
+ """Checks if the user should be rejected for some external reason,
+ such as monthly active user limiting or global disable flag
+
+ Args:
+ user_id(str|None): If present, checks for presence against existing
+ MAU cohort
+
+ threepid(dict|None): If present, checks for presence against configured
+ reserved threepid. Used in cases where the user is trying register
+ with a MAU blocked server, normally they would be rejected but their
+ threepid is on the reserved list. user_id and
+ threepid should never be set at the same time.
+
+ user_type(str|None): If present, is used to decide whether to check against
+ certain blocking reasons like MAU.
+ """
+
+ # Never fail an auth check for the server notices users or support user
+ # This can be a problem where event creation is prohibited due to blocking
+ if user_id is not None:
+ if user_id == self._server_notices_mxid:
+ return
+ if (yield self.store.is_support_user(user_id)):
+ return
+
+ if self._hs_disabled:
+ raise ResourceLimitError(
+ 403,
+ self._hs_disabled_message,
+ errcode=Codes.RESOURCE_LIMIT_EXCEEDED,
+ admin_contact=self._admin_contact,
+ limit_type=LimitBlockingTypes.HS_DISABLED,
+ )
+ if self._limit_usage_by_mau is True:
+ assert not (user_id and threepid)
+
+ # If the user is already part of the MAU cohort or a trial user
+ if user_id:
+ timestamp = yield self.store.user_last_seen_monthly_active(user_id)
+ if timestamp:
+ return
+
+ is_trial = yield self.store.is_trial_user(user_id)
+ if is_trial:
+ return
+ elif threepid:
+ # If the user does not exist yet, but is signing up with a
+ # reserved threepid then pass auth check
+ if is_threepid_reserved(self._mau_limits_reserved_threepids, threepid):
+ return
+ elif user_type == UserTypes.SUPPORT:
+ # If the user does not exist yet and is of type "support",
+ # allow registration. Support users are excluded from MAU checks.
+ return
+ # Else if there is no room in the MAU bucket, bail
+ current_mau = yield self.store.get_monthly_active_count()
+ if current_mau >= self._max_mau_value:
+ raise ResourceLimitError(
+ 403,
+ "Monthly Active User Limit Exceeded",
+ admin_contact=self._admin_contact,
+ errcode=Codes.RESOURCE_LIMIT_EXCEEDED,
+ limit_type=LimitBlockingTypes.MONTHLY_ACTIVE_USER,
+ )
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index f29bce560c..5ec4a77ccd 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd
-# Copyright 2018 New Vector Ltd
+# Copyright 2018-2019 New Vector Ltd
+# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -60,12 +61,9 @@ class LoginType(object):
MSISDN = "m.login.msisdn"
RECAPTCHA = "m.login.recaptcha"
TERMS = "m.login.terms"
+ SSO = "m.login.sso"
DUMMY = "m.login.dummy"
- # Only for C/S API v1
- APPLICATION_SERVICE = "m.login.application_service"
- SHARED_SECRET = "org.matrix.login.shared_secret"
-
class EventTypes(object):
Member = "m.room.member"
@@ -76,12 +74,11 @@ class EventTypes(object):
Aliases = "m.room.aliases"
Redaction = "m.room.redaction"
ThirdPartyInvite = "m.room.third_party_invite"
- Encryption = "m.room.encryption"
RelatedGroups = "m.room.related_groups"
RoomHistoryVisibility = "m.room.history_visibility"
CanonicalAlias = "m.room.canonical_alias"
- Encryption = "m.room.encryption"
+ Encrypted = "m.room.encrypted"
RoomAvatar = "m.room.avatar"
RoomEncryption = "m.room.encryption"
GuestAccess = "m.room.guest_access"
@@ -94,11 +91,13 @@ class EventTypes(object):
ServerACL = "m.room.server_acl"
Pinned = "m.room.pinned_events"
+ Retention = "m.room.retention"
+
+ Presence = "m.presence"
+
class RejectedReason(object):
AUTH_ERROR = "auth_error"
- REPLACED = "replaced"
- NOT_ANCESTOR = "not_ancestor"
class RoomCreationPreset(object):
@@ -133,3 +132,21 @@ class RelationTypes(object):
ANNOTATION = "m.annotation"
REPLACE = "m.replace"
REFERENCE = "m.reference"
+
+
+class LimitBlockingTypes(object):
+ """Reasons that a server may be blocked"""
+
+ MONTHLY_ACTIVE_USER = "monthly_active_user"
+ HS_DISABLED = "hs_disabled"
+
+
+class EventContentFields(object):
+ """Fields found in events' content, regardless of type."""
+
+ # Labels for the event, cf https://github.com/matrix-org/matrix-doc/pull/2326
+ LABELS = "org.matrix.labels"
+
+ # Timestamp to delete the event after
+ # cf https://github.com/matrix-org/matrix-doc/pull/2228
+ SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after"
diff --git a/synapse/api/errors.py b/synapse/api/errors.py
index cf1ebf1af2..d54dfb385d 100644
--- a/synapse/api/errors.py
+++ b/synapse/api/errors.py
@@ -17,12 +17,15 @@
"""Contains exceptions and error codes."""
import logging
+from typing import Dict, List
from six import iteritems
from six.moves import http_client
from canonicaljson import json
+from twisted.web import http
+
logger = logging.getLogger(__name__)
@@ -61,7 +64,16 @@ class Codes(object):
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
+ PASSWORD_TOO_SHORT = "M_PASSWORD_TOO_SHORT"
+ PASSWORD_NO_DIGIT = "M_PASSWORD_NO_DIGIT"
+ PASSWORD_NO_UPPERCASE = "M_PASSWORD_NO_UPPERCASE"
+ PASSWORD_NO_LOWERCASE = "M_PASSWORD_NO_LOWERCASE"
+ PASSWORD_NO_SYMBOL = "M_PASSWORD_NO_SYMBOL"
+ PASSWORD_IN_DICTIONARY = "M_PASSWORD_IN_DICTIONARY"
+ WEAK_PASSWORD = "M_WEAK_PASSWORD"
+ INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
USER_DEACTIVATED = "M_USER_DEACTIVATED"
+ BAD_ALIAS = "M_BAD_ALIAS"
class CodeMessageException(RuntimeError):
@@ -74,10 +86,40 @@ class CodeMessageException(RuntimeError):
def __init__(self, code, msg):
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
- self.code = code
+
+ # Some calls to this method pass instances of http.HTTPStatus for `code`.
+ # While HTTPStatus is a subclass of int, it has magic __str__ methods
+ # which emit `HTTPStatus.FORBIDDEN` when converted to a str, instead of `403`.
+ # This causes inconsistency in our log lines.
+ #
+ # To eliminate this behaviour, we convert them to their integer equivalents here.
+ self.code = int(code)
self.msg = msg
+class RedirectException(CodeMessageException):
+ """A pseudo-error indicating that we want to redirect the client to a different
+ location
+
+ Attributes:
+ cookies: a list of set-cookies values to add to the response. For example:
+ b"sessionId=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT"
+ """
+
+ def __init__(self, location: bytes, http_code: int = http.FOUND):
+ """
+
+ Args:
+ location: the URI to redirect to
+ http_code: the HTTP response code
+ """
+ msg = "Redirect to %s" % (location.decode("utf-8"),)
+ super().__init__(code=http_code, msg=msg)
+ self.location = location
+
+ self.cookies = [] # type: List[bytes]
+
+
class SynapseError(CodeMessageException):
"""A base exception type for matrix errors which have an errcode and error
message (as well as an HTTP status code).
@@ -111,7 +153,7 @@ class ProxiedRequestError(SynapseError):
def __init__(self, code, msg, errcode=Codes.UNKNOWN, additional_fields=None):
super(ProxiedRequestError, self).__init__(code, msg, errcode)
if additional_fields is None:
- self._additional_fields = {}
+ self._additional_fields = {} # type: Dict
else:
self._additional_fields = dict(additional_fields)
@@ -156,12 +198,6 @@ class UserDeactivatedError(SynapseError):
)
-class RegistrationError(SynapseError):
- """An error raised when a registration event fails."""
-
- pass
-
-
class FederationDeniedError(SynapseError):
"""An error raised when the server tries to federate with a server which
is not on its federation whitelist.
@@ -381,11 +417,9 @@ class UnsupportedRoomVersionError(SynapseError):
"""The client's request to create a room used a room version that the server does
not support."""
- def __init__(self):
+ def __init__(self, msg="Homeserver does not support this room version"):
super(UnsupportedRoomVersionError, self).__init__(
- code=400,
- msg="Homeserver does not support this room version",
- errcode=Codes.UNSUPPORTED_ROOM_VERSION,
+ code=400, msg=msg, errcode=Codes.UNSUPPORTED_ROOM_VERSION,
)
@@ -419,6 +453,20 @@ class IncompatibleRoomVersionError(SynapseError):
return cs_error(self.msg, self.errcode, room_version=self._room_version)
+class PasswordRefusedError(SynapseError):
+ """A password has been refused, either during password reset/change or registration.
+ """
+
+ def __init__(
+ self,
+ msg="This password doesn't comply with the server's policy",
+ errcode=Codes.WEAK_PASSWORD,
+ ):
+ super(PasswordRefusedError, self).__init__(
+ code=400, msg=msg, errcode=errcode,
+ )
+
+
class RequestSendFailed(RuntimeError):
"""Sending a HTTP request over federation failed due to not being able to
talk to the remote server for some reason.
@@ -455,7 +503,7 @@ def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
class FederationError(RuntimeError):
- """ This class is used to inform remote home servers about erroneous
+ """ This class is used to inform remote homeservers about erroneous
PDUs they sent us.
FATAL: The remote server could not interpret the source event.
diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py
index 9f06556bd2..8b64d0a285 100644
--- a/synapse/api/filtering.py
+++ b/synapse/api/filtering.py
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
+# Copyright 2017 Vector Creations Ltd
+# Copyright 2018-2019 New Vector Ltd
+# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,6 +15,8 @@
# 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 typing import List
+
from six import text_type
import jsonschema
@@ -20,6 +25,7 @@ from jsonschema import FormatChecker
from twisted.internet import defer
+from synapse.api.constants import EventContentFields
from synapse.api.errors import SynapseError
from synapse.storage.presence import UserPresenceState
from synapse.types import RoomID, UserID
@@ -66,6 +72,10 @@ ROOM_EVENT_FILTER_SCHEMA = {
"contains_url": {"type": "boolean"},
"lazy_load_members": {"type": "boolean"},
"include_redundant_members": {"type": "boolean"},
+ # Include or exclude events with the provided labels.
+ # cf https://github.com/matrix-org/matrix-doc/pull/2326
+ "org.matrix.labels": {"type": "array", "items": {"type": "string"}},
+ "org.matrix.not_labels": {"type": "array", "items": {"type": "string"}},
},
}
@@ -259,6 +269,9 @@ class Filter(object):
self.contains_url = self.filter_json.get("contains_url", None)
+ self.labels = self.filter_json.get("org.matrix.labels", None)
+ self.not_labels = self.filter_json.get("org.matrix.not_labels", [])
+
def filters_all_types(self):
return "*" in self.not_types
@@ -282,6 +295,7 @@ class Filter(object):
room_id = None
ev_type = "m.presence"
contains_url = False
+ labels = [] # type: List[str]
else:
sender = event.get("sender", None)
if not sender:
@@ -300,10 +314,11 @@ class Filter(object):
content = event.get("content", {})
# check if there is a string url field in the content for filtering purposes
contains_url = isinstance(content.get("url"), text_type)
+ labels = content.get(EventContentFields.LABELS, [])
- return self.check_fields(room_id, sender, ev_type, contains_url)
+ return self.check_fields(room_id, sender, ev_type, labels, contains_url)
- def check_fields(self, room_id, sender, event_type, contains_url):
+ def check_fields(self, room_id, sender, event_type, labels, contains_url):
"""Checks whether the filter matches the given event fields.
Returns:
@@ -313,6 +328,7 @@ class Filter(object):
"rooms": lambda v: room_id == v,
"senders": lambda v: sender == v,
"types": lambda v: _matches_wildcard(event_type, v),
+ "labels": lambda v: v in labels,
}
for name, match_func in literal_keys.items():
diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py
index 172841f595..ec6b3a69a2 100644
--- a/synapse/api/ratelimiting.py
+++ b/synapse/api/ratelimiting.py
@@ -1,4 +1,5 @@
# Copyright 2014-2016 OpenMarket Ltd
+# Copyright 2020 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,76 +13,161 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import collections
+from collections import OrderedDict
+from typing import Any, Optional, Tuple
from synapse.api.errors import LimitExceededError
+from synapse.util import Clock
class Ratelimiter(object):
"""
- Ratelimit message sending by user.
+ Ratelimit actions marked by arbitrary keys.
+
+ Args:
+ clock: A homeserver clock, for retrieving the current time
+ rate_hz: The long term number of actions that can be performed in a second.
+ burst_count: How many actions that can be performed before being limited.
"""
- def __init__(self):
- self.message_counts = collections.OrderedDict()
+ def __init__(self, clock: Clock, rate_hz: float, burst_count: int):
+ self.clock = clock
+ self.rate_hz = rate_hz
+ self.burst_count = burst_count
+
+ # A ordered dictionary keeping track of actions, when they were last
+ # performed and how often. Each entry is a mapping from a key of arbitrary type
+ # to a tuple representing:
+ # * How many times an action has occurred since a point in time
+ # * The point in time
+ # * The rate_hz of this particular entry. This can vary per request
+ self.actions = OrderedDict() # type: OrderedDict[Any, Tuple[float, int, float]]
- def can_do_action(self, key, time_now_s, rate_hz, burst_count, update=True):
+ def can_do_action(
+ self,
+ key: Any,
+ rate_hz: Optional[float] = None,
+ burst_count: Optional[int] = None,
+ update: bool = True,
+ _time_now_s: Optional[int] = None,
+ ) -> Tuple[bool, float]:
"""Can the entity (e.g. user or IP address) perform the action?
+
Args:
key: The key we should use when rate limiting. Can be a user ID
(when sending events), an IP address, etc.
- time_now_s: The time now.
- rate_hz: The long term number of messages a user can send in a
- second.
- burst_count: How many messages the user can send before being
- limited.
- update (bool): Whether to update the message rates or not. This is
- useful to check if a message would be allowed to be sent before
- its ready to be actually sent.
+ rate_hz: The long term number of actions that can be performed in a second.
+ Overrides the value set during instantiation if set.
+ burst_count: How many actions that can be performed before being limited.
+ Overrides the value set during instantiation if set.
+ update: Whether to count this check as performing the action
+ _time_now_s: The current time. Optional, defaults to the current time according
+ to self.clock. Only used by tests.
+
Returns:
- A pair of a bool indicating if they can send a message now and a
- time in seconds of when they can next send a message.
+ A tuple containing:
+ * A bool indicating if they can perform the action now
+ * The reactor timestamp for when the action can be performed next.
+ -1 if rate_hz is less than or equal to zero
"""
- self.prune_message_counts(time_now_s)
- message_count, time_start, _ignored = self.message_counts.get(
- key, (0.0, time_now_s, None)
- )
+ # Override default values if set
+ time_now_s = _time_now_s if _time_now_s is not None else self.clock.time()
+ rate_hz = rate_hz if rate_hz is not None else self.rate_hz
+ burst_count = burst_count if burst_count is not None else self.burst_count
+
+ # Remove any expired entries
+ self._prune_message_counts(time_now_s)
+
+ # Check if there is an existing count entry for this key
+ action_count, time_start, _ = self.actions.get(key, (0.0, time_now_s, 0.0))
+
+ # Check whether performing another action is allowed
time_delta = time_now_s - time_start
- sent_count = message_count - time_delta * rate_hz
- if sent_count < 0:
+ performed_count = action_count - time_delta * rate_hz
+ if performed_count < 0:
+ # Allow, reset back to count 1
allowed = True
time_start = time_now_s
- message_count = 1.0
- elif sent_count > burst_count - 1.0:
+ action_count = 1.0
+ elif performed_count > burst_count - 1.0:
+ # Deny, we have exceeded our burst count
allowed = False
else:
+ # We haven't reached our limit yet
allowed = True
- message_count += 1
+ action_count += 1.0
if update:
- self.message_counts[key] = (message_count, time_start, rate_hz)
+ self.actions[key] = (action_count, time_start, rate_hz)
if rate_hz > 0:
- time_allowed = time_start + (message_count - burst_count + 1) / rate_hz
+ # Find out when the count of existing actions expires
+ time_allowed = time_start + (action_count - burst_count + 1) / rate_hz
+
+ # Don't give back a time in the past
if time_allowed < time_now_s:
time_allowed = time_now_s
+
else:
+ # XXX: Why is this -1? This seems to only be used in
+ # self.ratelimit. I guess so that clients get a time in the past and don't
+ # feel afraid to try again immediately
time_allowed = -1
return allowed, time_allowed
- def prune_message_counts(self, time_now_s):
- for key in list(self.message_counts.keys()):
- message_count, time_start, rate_hz = self.message_counts[key]
+ def _prune_message_counts(self, time_now_s: int):
+ """Remove message count entries that have not exceeded their defined
+ rate_hz limit
+
+ Args:
+ time_now_s: The current time
+ """
+ # We create a copy of the key list here as the dictionary is modified during
+ # the loop
+ for key in list(self.actions.keys()):
+ action_count, time_start, rate_hz = self.actions[key]
+
+ # Rate limit = "seconds since we started limiting this action" * rate_hz
+ # If this limit has not been exceeded, wipe our record of this action
time_delta = time_now_s - time_start
- if message_count - time_delta * rate_hz > 0:
- break
+ if action_count - time_delta * rate_hz > 0:
+ continue
else:
- del self.message_counts[key]
+ del self.actions[key]
+
+ def ratelimit(
+ self,
+ key: Any,
+ rate_hz: Optional[float] = None,
+ burst_count: Optional[int] = None,
+ update: bool = True,
+ _time_now_s: Optional[int] = None,
+ ):
+ """Checks if an action can be performed. If not, raises a LimitExceededError
+
+ Args:
+ key: An arbitrary key used to classify an action
+ rate_hz: The long term number of actions that can be performed in a second.
+ Overrides the value set during instantiation if set.
+ burst_count: How many actions that can be performed before being limited.
+ Overrides the value set during instantiation if set.
+ update: Whether to count this check as performing the action
+ _time_now_s: The current time. Optional, defaults to the current time according
+ to self.clock. Only used by tests.
+
+ Raises:
+ LimitExceededError: If an action could not be performed, along with the time in
+ milliseconds until the action can be performed again
+ """
+ time_now_s = _time_now_s if _time_now_s is not None else self.clock.time()
- def ratelimit(self, key, time_now_s, rate_hz, burst_count, update=True):
allowed, time_allowed = self.can_do_action(
- key, time_now_s, rate_hz, burst_count, update
+ key,
+ rate_hz=rate_hz,
+ burst_count=burst_count,
+ update=update,
+ _time_now_s=time_now_s,
)
if not allowed:
diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py
index 95292b7dec..d7baf2bc39 100644
--- a/synapse/api/room_versions.py
+++ b/synapse/api/room_versions.py
@@ -12,6 +12,9 @@
# 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 typing import Dict
+
import attr
@@ -54,6 +57,17 @@ class RoomVersion(object):
state_res = attr.ib() # int; one of the StateResolutionVersions
enforce_key_validity = attr.ib() # bool
+ # bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules
+ special_case_aliases_auth = attr.ib(type=bool)
+ # Strictly enforce canonicaljson, do not allow:
+ # * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
+ # * Floats
+ # * NaN, Infinity, -Infinity
+ strict_canonicaljson = attr.ib(type=bool)
+ # bool: MSC2209: Check 'notifications' key while verifying
+ # m.room.power_levels auth rules.
+ limit_notifications_power_levels = attr.ib(type=bool)
+
class RoomVersions(object):
V1 = RoomVersion(
@@ -62,6 +76,9 @@ class RoomVersions(object):
EventFormatVersions.V1,
StateResolutionVersions.V1,
enforce_key_validity=False,
+ special_case_aliases_auth=True,
+ strict_canonicaljson=False,
+ limit_notifications_power_levels=False,
)
V2 = RoomVersion(
"2",
@@ -69,6 +86,9 @@ class RoomVersions(object):
EventFormatVersions.V1,
StateResolutionVersions.V2,
enforce_key_validity=False,
+ special_case_aliases_auth=True,
+ strict_canonicaljson=False,
+ limit_notifications_power_levels=False,
)
V3 = RoomVersion(
"3",
@@ -76,6 +96,9 @@ class RoomVersions(object):
EventFormatVersions.V2,
StateResolutionVersions.V2,
enforce_key_validity=False,
+ special_case_aliases_auth=True,
+ strict_canonicaljson=False,
+ limit_notifications_power_levels=False,
)
V4 = RoomVersion(
"4",
@@ -83,6 +106,9 @@ class RoomVersions(object):
EventFormatVersions.V3,
StateResolutionVersions.V2,
enforce_key_validity=False,
+ special_case_aliases_auth=True,
+ strict_canonicaljson=False,
+ limit_notifications_power_levels=False,
)
V5 = RoomVersion(
"5",
@@ -90,6 +116,19 @@ class RoomVersions(object):
EventFormatVersions.V3,
StateResolutionVersions.V2,
enforce_key_validity=True,
+ special_case_aliases_auth=True,
+ strict_canonicaljson=False,
+ limit_notifications_power_levels=False,
+ )
+ V6 = RoomVersion(
+ "6",
+ RoomDisposition.STABLE,
+ EventFormatVersions.V3,
+ StateResolutionVersions.V2,
+ enforce_key_validity=True,
+ special_case_aliases_auth=False,
+ strict_canonicaljson=True,
+ limit_notifications_power_levels=True,
)
@@ -101,5 +140,6 @@ KNOWN_ROOM_VERSIONS = {
RoomVersions.V3,
RoomVersions.V4,
RoomVersions.V5,
+ RoomVersions.V6,
)
-} # type: dict[str, RoomVersion]
+} # type: Dict[str, RoomVersion]
diff --git a/synapse/api/urls.py b/synapse/api/urls.py
index ff1f39e86c..f34434bd67 100644
--- a/synapse/api/urls.py
+++ b/synapse/api/urls.py
@@ -29,7 +29,6 @@ FEDERATION_V2_PREFIX = FEDERATION_PREFIX + "/v2"
FEDERATION_UNSTABLE_PREFIX = FEDERATION_PREFIX + "/unstable"
STATIC_PREFIX = "/_matrix/static"
WEB_CLIENT_PREFIX = "/_matrix/client"
-CONTENT_REPO_PREFIX = "/_matrix/content"
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
MEDIA_PREFIX = "/_matrix/media/r0"
LEGACY_MEDIA_PREFIX = "/_matrix/media/v1"
|