diff --git a/synapse/__init__.py b/synapse/__init__.py
index 6bb5a8b24d..315fa96551 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -27,4 +27,4 @@ try:
except ImportError:
pass
-__version__ = "0.99.3"
+__version__ = "0.99.3.2"
diff --git a/synapse/api/auth.py b/synapse/api/auth.py
index 960e66dbdc..0c6c93a87b 100644
--- a/synapse/api/auth.py
+++ b/synapse/api/auth.py
@@ -556,7 +556,7 @@ class Auth(object):
""" Check if the given user is a local server admin.
Args:
- user (str): mxid of user to check
+ user (UserID): user to check
Returns:
bool: True if the user is an admin
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index 0860b75905..8547a63535 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -20,6 +20,9 @@
# the "depth" field on events is limited to 2**63 - 1
MAX_DEPTH = 2**63 - 1
+# the maximum length for a room alias is 255 characters
+MAX_ALIAS_LENGTH = 255
+
class Membership(object):
diff --git a/synapse/app/_base.py b/synapse/app/_base.py
index d4c6c4c8e2..08199a5e8d 100644
--- a/synapse/app/_base.py
+++ b/synapse/app/_base.py
@@ -22,13 +22,14 @@ import traceback
import psutil
from daemonize import Daemonize
-from twisted.internet import error, reactor
+from twisted.internet import defer, error, reactor
from twisted.protocols.tls import TLSMemoryBIOFactory
import synapse
from synapse.app import check_bind_error
from synapse.crypto import context_factory
from synapse.util import PreserveLoggingContext
+from synapse.util.async_helpers import Linearizer
from synapse.util.rlimit import change_resource_limit
from synapse.util.versionstring import get_version_string
@@ -99,6 +100,8 @@ def start_reactor(
logger (logging.Logger): logger instance to pass to Daemonize
"""
+ install_dns_limiter(reactor)
+
def run():
# make sure that we run the reactor with the sentinel log context,
# otherwise other PreserveLoggingContext instances will get confused
@@ -312,3 +315,81 @@ def setup_sentry(hs):
name = hs.config.worker_name if hs.config.worker_name else "master"
scope.set_tag("worker_app", app)
scope.set_tag("worker_name", name)
+
+
+def install_dns_limiter(reactor, max_dns_requests_in_flight=100):
+ """Replaces the resolver with one that limits the number of in flight DNS
+ requests.
+
+ This is to workaround https://twistedmatrix.com/trac/ticket/9620, where we
+ can run out of file descriptors and infinite loop if we attempt to do too
+ many DNS queries at once
+ """
+ new_resolver = _LimitedHostnameResolver(
+ reactor.nameResolver, max_dns_requests_in_flight,
+ )
+
+ reactor.installNameResolver(new_resolver)
+
+
+class _LimitedHostnameResolver(object):
+ """Wraps a IHostnameResolver, limiting the number of in-flight DNS lookups.
+ """
+
+ def __init__(self, resolver, max_dns_requests_in_flight):
+ self._resolver = resolver
+ self._limiter = Linearizer(
+ name="dns_client_limiter", max_count=max_dns_requests_in_flight,
+ )
+
+ def resolveHostName(self, resolutionReceiver, hostName, portNumber=0,
+ addressTypes=None, transportSemantics='TCP'):
+ # Note this is happening deep within the reactor, so we don't need to
+ # worry about log contexts.
+
+ # We need this function to return `resolutionReceiver` so we do all the
+ # actual logic involving deferreds in a separate function.
+ self._resolve(
+ resolutionReceiver, hostName, portNumber,
+ addressTypes, transportSemantics,
+ )
+
+ return resolutionReceiver
+
+ @defer.inlineCallbacks
+ def _resolve(self, resolutionReceiver, hostName, portNumber=0,
+ addressTypes=None, transportSemantics='TCP'):
+
+ with (yield self._limiter.queue(())):
+ # resolveHostName doesn't return a Deferred, so we need to hook into
+ # the receiver interface to get told when resolution has finished.
+
+ deferred = defer.Deferred()
+ receiver = _DeferredResolutionReceiver(resolutionReceiver, deferred)
+
+ self._resolver.resolveHostName(
+ receiver, hostName, portNumber,
+ addressTypes, transportSemantics,
+ )
+
+ yield deferred
+
+
+class _DeferredResolutionReceiver(object):
+ """Wraps a IResolutionReceiver and simply resolves the given deferred when
+ resolution is complete
+ """
+
+ def __init__(self, receiver, deferred):
+ self._receiver = receiver
+ self._deferred = deferred
+
+ def resolutionBegan(self, resolutionInProgress):
+ self._receiver.resolutionBegan(resolutionInProgress)
+
+ def addressResolved(self, address):
+ self._receiver.addressResolved(address)
+
+ def resolutionComplete(self):
+ self._deferred.callback(())
+ self._receiver.resolutionComplete()
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 79be977ea6..1045d28949 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -62,6 +62,7 @@ from synapse.python_dependencies import check_requirements
from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
from synapse.replication.tcp.resource import ReplicationStreamProtocolFactory
from synapse.rest import ClientRestResource
+from synapse.rest.admin import AdminRestResource
from synapse.rest.key.v2 import KeyApiV2Resource
from synapse.rest.media.v0.content_repository import ContentRepoResource
from synapse.rest.well_known import WellKnownResource
@@ -180,6 +181,7 @@ class SynapseHomeServer(HomeServer):
"/_matrix/client/v2_alpha": client_resource,
"/_matrix/client/versions": client_resource,
"/.well-known/matrix/client": WellKnownResource(self),
+ "/_synapse/admin": AdminRestResource(self),
})
if self.get_config().saml2_enabled:
diff --git a/synapse/config/repository.py b/synapse/config/repository.py
index 207cdf76ef..db7055ac5e 100644
--- a/synapse/config/repository.py
+++ b/synapse/config/repository.py
@@ -192,17 +192,21 @@ class ContentRepositoryConfig(Config):
except ImportError:
raise ConfigError(MISSING_NETADDR)
- if "url_preview_ip_range_blacklist" in config:
- self.url_preview_ip_range_blacklist = IPSet(
- config["url_preview_ip_range_blacklist"]
- )
- else:
+ if "url_preview_ip_range_blacklist" not in config:
raise ConfigError(
"For security, you must specify an explicit target IP address "
"blacklist in url_preview_ip_range_blacklist for url previewing "
"to work"
)
+ self.url_preview_ip_range_blacklist = IPSet(
+ config["url_preview_ip_range_blacklist"]
+ )
+
+ # we always blacklist '0.0.0.0' and '::', which are supposed to be
+ # unroutable addresses.
+ self.url_preview_ip_range_blacklist.update(['0.0.0.0', '::'])
+
self.url_preview_ip_range_whitelist = IPSet(
config.get("url_preview_ip_range_whitelist", ())
)
@@ -266,11 +270,12 @@ class ContentRepositoryConfig(Config):
#thumbnail_sizes:
%(formatted_thumbnail_sizes)s
- # Is the preview URL API enabled? If enabled, you *must* specify
- # an explicit url_preview_ip_range_blacklist of IPs that the spider is
- # denied from accessing.
+ # Is the preview URL API enabled?
+ #
+ # 'false' by default: uncomment the following to enable it (and specify a
+ # url_preview_ip_range_blacklist blacklist).
#
- #url_preview_enabled: false
+ #url_preview_enabled: true
# List of IP address CIDR ranges that the URL preview spider is denied
# from accessing. There are no defaults: you must explicitly
@@ -280,6 +285,12 @@ class ContentRepositoryConfig(Config):
# synapse to issue arbitrary GET requests to your internal services,
# causing serious security issues.
#
+ # (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
+ # listed here, since they correspond to unroutable addresses.)
+ #
+ # This must be specified if url_preview_enabled is set. It is recommended that
+ # you uncomment the following list as a starting point.
+ #
#url_preview_ip_range_blacklist:
# - '127.0.0.0/8'
# - '10.0.0.0/8'
@@ -290,7 +301,7 @@ class ContentRepositoryConfig(Config):
# - '::1/128'
# - 'fe80::/64'
# - 'fc00::/7'
- #
+
# List of IP address CIDR ranges that the URL preview spider is allowed
# to access even if they are specified in url_preview_ip_range_blacklist.
# This is useful for specifying exceptions to wide-ranging blacklisted
diff --git a/synapse/config/server.py b/synapse/config/server.py
index cdf1e4d286..8dce75c56a 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -72,6 +72,19 @@ class ServerConfig(Config):
# master, potentially causing inconsistency.
self.enable_media_repo = config.get("enable_media_repo", True)
+ # Whether to require authentication to retrieve profile data (avatars,
+ # display names) of other users through the client API.
+ self.require_auth_for_profile_requests = config.get(
+ "require_auth_for_profile_requests", False,
+ )
+
+ # If set to 'True', requires authentication to access the server's
+ # public rooms directory through the client API, and forbids any other
+ # homeserver to fetch it via federation.
+ self.restrict_public_rooms_to_local_users = config.get(
+ "restrict_public_rooms_to_local_users", False,
+ )
+
# whether to enable search. If disabled, new entries will not be inserted
# into the search tables and they will not be indexed. Users will receive
# errors when attempting to search for messages.
@@ -134,6 +147,12 @@ class ServerConfig(Config):
# sending out any replication updates.
self.replication_torture_level = config.get("replication_torture_level")
+ # Whether to require a user to be in the room to add an alias to it.
+ # Defaults to True.
+ self.require_membership_for_aliases = config.get(
+ "require_membership_for_aliases", True,
+ )
+
self.listeners = []
for listener in config.get("listeners", []):
if not isinstance(listener.get("port", None), int):
@@ -321,6 +340,20 @@ class ServerConfig(Config):
#
#use_presence: false
+ # Whether to require authentication to retrieve profile data (avatars,
+ # display names) of other users through the client API. Defaults to
+ # 'false'. Note that profile data is also available via the federation
+ # API, so this setting is of limited value if federation is enabled on
+ # the server.
+ #
+ #require_auth_for_profile_requests: true
+
+ # If set to 'true', requires authentication to access the server's
+ # public rooms directory through the client API, and forbids any other
+ # homeserver to fetch it via federation. Defaults to 'false'.
+ #
+ #restrict_public_rooms_to_local_users: true
+
# The GC threshold parameters to pass to `gc.set_threshold`, if defined
#
#gc_thresholds: [700, 10, 10]
@@ -388,8 +421,8 @@ class ServerConfig(Config):
#
# Valid resource names are:
#
- # client: the client-server API (/_matrix/client). Also implies 'media' and
- # 'static'.
+ # client: the client-server API (/_matrix/client), and the synapse admin
+ # API (/_synapse/admin). Also implies 'media' and 'static'.
#
# consent: user consent forms (/_matrix/consent). See
# docs/consent_tracking.md.
@@ -490,6 +523,11 @@ class ServerConfig(Config):
# Used by phonehome stats to group together related servers.
#server_context: context
+
+ # Whether to require a user to be in the room to add an alias to it.
+ # Defaults to 'true'.
+ #
+ #require_membership_for_aliases: false
""" % locals()
def read_arguments(self, args):
diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py
index 368b5f6ae4..fa09c132a0 100644
--- a/synapse/events/snapshot.py
+++ b/synapse/events/snapshot.py
@@ -187,7 +187,9 @@ class EventContext(object):
Returns:
Deferred[dict[(str, str), str]|None]: Returns None if state_group
- is None, which happens when the associated event is an outlier.
+ is None, which happens when the associated event is an outlier.
+ Maps a (type, state_key) to the event ID of the state event matching
+ this tuple.
"""
if not self._fetching_state_deferred:
@@ -205,7 +207,9 @@ class EventContext(object):
Returns:
Deferred[dict[(str, str), str]|None]: Returns None if state_group
- is None, which happens when the associated event is an outlier.
+ is None, which happens when the associated event is an outlier.
+ Maps a (type, state_key) to the event ID of the state event matching
+ this tuple.
"""
if not self._fetching_state_deferred:
diff --git a/synapse/events/validator.py b/synapse/events/validator.py
index 514273c792..711af512b2 100644
--- a/synapse/events/validator.py
+++ b/synapse/events/validator.py
@@ -15,8 +15,8 @@
from six import string_types
-from synapse.api.constants import EventTypes, Membership
-from synapse.api.errors import SynapseError
+from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
+from synapse.api.errors import Codes, SynapseError
from synapse.api.room_versions import EventFormatVersions
from synapse.types import EventID, RoomID, UserID
@@ -56,6 +56,17 @@ class EventValidator(object):
if not isinstance(getattr(event, s), string_types):
raise SynapseError(400, "'%s' not a string type" % (s,))
+ if event.type == EventTypes.Aliases:
+ if "aliases" in event.content:
+ for alias in event.content["aliases"]:
+ if len(alias) > MAX_ALIAS_LENGTH:
+ raise SynapseError(
+ 400,
+ ("Can't create aliases longer than"
+ " %d characters" % (MAX_ALIAS_LENGTH,)),
+ Codes.INVALID_PARAM,
+ )
+
def validate_builder(self, event):
"""Validates that the builder/event has roughly the right format. Only
checks values that we expect a proto event to have, rather than all the
diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py
index 452599e1a1..9030eb18c5 100644
--- a/synapse/federation/transport/server.py
+++ b/synapse/federation/transport/server.py
@@ -716,8 +716,17 @@ class PublicRoomList(BaseFederationServlet):
PATH = "/publicRooms"
+ def __init__(self, handler, authenticator, ratelimiter, server_name, deny_access):
+ super(PublicRoomList, self).__init__(
+ handler, authenticator, ratelimiter, server_name,
+ )
+ self.deny_access = deny_access
+
@defer.inlineCallbacks
def on_GET(self, origin, content, query):
+ if self.deny_access:
+ raise FederationDeniedError(origin)
+
limit = parse_integer_from_args(query, "limit", 0)
since_token = parse_string_from_args(query, "since", None)
include_all_networks = parse_boolean_from_args(
@@ -1417,6 +1426,7 @@ def register_servlets(hs, resource, authenticator, ratelimiter, servlet_groups=N
authenticator=authenticator,
ratelimiter=ratelimiter,
server_name=hs.hostname,
+ deny_access=hs.config.restrict_public_rooms_to_local_users,
).register(resource)
if "group_server" in servlet_groups:
diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py
index 27bd06df5d..a12f9508d8 100644
--- a/synapse/handlers/directory.py
+++ b/synapse/handlers/directory.py
@@ -19,7 +19,7 @@ import string
from twisted.internet import defer
-from synapse.api.constants import EventTypes
+from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes
from synapse.api.errors import (
AuthError,
CodeMessageException,
@@ -43,8 +43,10 @@ class DirectoryHandler(BaseHandler):
self.state = hs.get_state_handler()
self.appservice_handler = hs.get_application_service_handler()
self.event_creation_handler = hs.get_event_creation_handler()
+ self.store = hs.get_datastore()
self.config = hs.config
self.enable_room_list_search = hs.config.enable_room_list_search
+ self.require_membership = hs.config.require_membership_for_aliases
self.federation = hs.get_federation_client()
hs.get_federation_registry().register_query_handler(
@@ -83,7 +85,7 @@ class DirectoryHandler(BaseHandler):
@defer.inlineCallbacks
def create_association(self, requester, room_alias, room_id, servers=None,
- send_event=True):
+ send_event=True, check_membership=True):
"""Attempt to create a new alias
Args:
@@ -93,6 +95,8 @@ class DirectoryHandler(BaseHandler):
servers (list[str]|None): List of servers that others servers
should try and join via
send_event (bool): Whether to send an updated m.room.aliases event
+ check_membership (bool): Whether to check if the user is in the room
+ before the alias can be set (if the server's config requires it).
Returns:
Deferred
@@ -100,6 +104,13 @@ class DirectoryHandler(BaseHandler):
user_id = requester.user.to_string()
+ if len(room_alias.to_string()) > MAX_ALIAS_LENGTH:
+ raise SynapseError(
+ 400,
+ "Can't create aliases longer than %s characters" % MAX_ALIAS_LENGTH,
+ Codes.INVALID_PARAM,
+ )
+
service = requester.app_service
if service:
if not service.is_interested_in_alias(room_alias.to_string()):
@@ -108,6 +119,14 @@ class DirectoryHandler(BaseHandler):
" this kind of alias.", errcode=Codes.EXCLUSIVE
)
else:
+ if self.require_membership and check_membership:
+ rooms_for_user = yield self.store.get_rooms_for_user(user_id)
+ if room_id not in rooms_for_user:
+ raise AuthError(
+ 403,
+ "You must be in the room to create an alias for it",
+ )
+
if not self.spam_checker.user_may_create_room_alias(user_id, room_alias):
raise AuthError(
403, "This user is not permitted to create this alias",
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 224d34ef3a..e5afeadf68 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -228,6 +228,7 @@ class EventCreationHandler(object):
self.ratelimiter = hs.get_ratelimiter()
self.notifier = hs.get_notifier()
self.config = hs.config
+ self.require_membership_for_aliases = hs.config.require_membership_for_aliases
self.send_event_to_master = ReplicationSendEventRestServlet.make_client(hs)
@@ -336,6 +337,35 @@ class EventCreationHandler(object):
prev_events_and_hashes=prev_events_and_hashes,
)
+ # In an ideal world we wouldn't need the second part of this condition. However,
+ # this behaviour isn't spec'd yet, meaning we should be able to deactivate this
+ # behaviour. Another reason is that this code is also evaluated each time a new
+ # m.room.aliases event is created, which includes hitting a /directory route.
+ # Therefore not including this condition here would render the similar one in
+ # synapse.handlers.directory pointless.
+ if builder.type == EventTypes.Aliases and self.require_membership_for_aliases:
+ # Ideally we'd do the membership check in event_auth.check(), which
+ # describes a spec'd algorithm for authenticating events received over
+ # federation as well as those created locally. As of room v3, aliases events
+ # can be created by users that are not in the room, therefore we have to
+ # tolerate them in event_auth.check().
+ prev_state_ids = yield context.get_prev_state_ids(self.store)
+ prev_event_id = prev_state_ids.get((EventTypes.Member, event.sender))
+ prev_event = yield self.store.get_event(prev_event_id, allow_none=True)
+ if not prev_event or prev_event.membership != Membership.JOIN:
+ logger.warning(
+ ("Attempt to send `m.room.aliases` in room %s by user %s but"
+ " membership is %s"),
+ event.room_id,
+ event.sender,
+ prev_event.membership if prev_event else None,
+ )
+
+ raise AuthError(
+ 403,
+ "You must be in the room to create an alias for it",
+ )
+
self.validator.validate_new(event)
defer.returnValue((event, context))
diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py
index a65c98ff5c..91fc718ff8 100644
--- a/synapse/handlers/profile.py
+++ b/synapse/handlers/profile.py
@@ -53,6 +53,7 @@ class BaseProfileHandler(BaseHandler):
@defer.inlineCallbacks
def get_profile(self, user_id):
target_user = UserID.from_string(user_id)
+
if self.hs.is_mine(target_user):
try:
displayname = yield self.store.get_profile_displayname(
@@ -283,6 +284,48 @@ class BaseProfileHandler(BaseHandler):
room_id, str(e)
)
+ @defer.inlineCallbacks
+ def check_profile_query_allowed(self, target_user, requester=None):
+ """Checks whether a profile query is allowed. If the
+ 'require_auth_for_profile_requests' config flag is set to True and a
+ 'requester' is provided, the query is only allowed if the two users
+ share a room.
+
+ Args:
+ target_user (UserID): The owner of the queried profile.
+ requester (None|UserID): The user querying for the profile.
+
+ Raises:
+ SynapseError(403): The two users share no room, or ne user couldn't
+ be found to be in any room the server is in, and therefore the query
+ is denied.
+ """
+ # Implementation of MSC1301: don't allow looking up profiles if the
+ # requester isn't in the same room as the target. We expect requester to
+ # be None when this function is called outside of a profile query, e.g.
+ # when building a membership event. In this case, we must allow the
+ # lookup.
+ if not self.hs.config.require_auth_for_profile_requests or not requester:
+ return
+
+ try:
+ requester_rooms = yield self.store.get_rooms_for_user(
+ requester.to_string()
+ )
+ target_user_rooms = yield self.store.get_rooms_for_user(
+ target_user.to_string(),
+ )
+
+ # Check if the room lists have no elements in common.
+ if requester_rooms.isdisjoint(target_user_rooms):
+ raise SynapseError(403, "Profile isn't available", Codes.FORBIDDEN)
+ except StoreError as e:
+ if e.code == 404:
+ # This likely means that one of the users doesn't exist,
+ # so we act as if we couldn't find the profile.
+ raise SynapseError(403, "Profile isn't available", Codes.FORBIDDEN)
+ raise
+
class MasterProfileHandler(BaseProfileHandler):
PROFILE_UPDATE_MS = 60 * 1000
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 17628e2684..e37ae96899 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -402,7 +402,7 @@ class RoomCreationHandler(BaseHandler):
yield directory_handler.create_association(
requester, RoomAlias.from_string(alias),
new_room_id, servers=(self.hs.hostname, ),
- send_event=False,
+ send_event=False, check_membership=False,
)
logger.info("Moved alias %s to new room", alias)
except SynapseError as e:
@@ -538,6 +538,7 @@ class RoomCreationHandler(BaseHandler):
room_alias=room_alias,
servers=[self.hs.hostname],
send_event=False,
+ check_membership=False,
)
preset_config = config.get(
diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py
index 024d6db27a..3e86b9c690 100644
--- a/synapse/handlers/room_member.py
+++ b/synapse/handlers/room_member.py
@@ -33,6 +33,8 @@ from synapse.types import RoomID, UserID
from synapse.util.async_helpers import Linearizer
from synapse.util.distributor import user_joined_room, user_left_room
+from ._base import BaseHandler
+
logger = logging.getLogger(__name__)
id_server_scheme = "https://"
@@ -72,6 +74,11 @@ class RoomMemberHandler(object):
self._server_notices_mxid = self.config.server_notices_mxid
self._enable_lookup = hs.config.enable_3pid_lookup
+ # This is only used to get at ratelimit function, and
+ # maybe_kick_guest_users. It's fine there are multiple of these as
+ # it doesn't store state.
+ self.base_handler = BaseHandler(hs)
+
@abc.abstractmethod
def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
"""Try and join a room that this server is not in
@@ -703,6 +710,10 @@ class RoomMemberHandler(object):
Codes.FORBIDDEN,
)
+ # We need to rate limit *before* we send out any 3PID invites, so we
+ # can't just rely on the standard ratelimiting of events.
+ yield self.base_handler.ratelimit(requester)
+
invitee = yield self._lookup_3pid(
id_server, medium, address
)
diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py
index 779f36dbed..2708f5e820 100644
--- a/synapse/python_dependencies.py
+++ b/synapse/python_dependencies.py
@@ -69,6 +69,14 @@ REQUIREMENTS = [
"attrs>=17.4.0",
"netaddr>=0.7.18",
+
+ # requests is a transitive dep of treq, and urlib3 is a transitive dep
+ # of requests, as well as of sentry-sdk.
+ #
+ # As of requests 2.21, requests does not yet support urllib3 1.25.
+ # (If we do not pin it here, pip will give us the latest urllib3
+ # due to the dep via sentry-sdk.)
+ "urllib3<1.25",
]
CONDITIONAL_REQUIREMENTS = {
diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py
index a66885d349..3a24d31d1b 100644
--- a/synapse/rest/__init__.py
+++ b/synapse/rest/__init__.py
@@ -13,11 +13,10 @@
# 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 synapse.rest.admin
from synapse.http.server import JsonResource
from synapse.rest.client import versions
from synapse.rest.client.v1 import (
- admin,
directory,
events,
initial_sync,
@@ -58,8 +57,14 @@ from synapse.rest.client.v2_alpha import (
class ClientRestResource(JsonResource):
- """A resource for version 1 of the matrix client API."""
+ """Matrix Client API REST resource.
+ This gets mounted at various points under /_matrix/client, including:
+ * /_matrix/client/r0
+ * /_matrix/client/api/v1
+ * /_matrix/client/unstable
+ * etc
+ """
def __init__(self, hs):
JsonResource.__init__(self, hs, canonical_json=False)
self.register_servlets(self, hs)
@@ -82,7 +87,6 @@ class ClientRestResource(JsonResource):
presence.register_servlets(hs, client_resource)
directory.register_servlets(hs, client_resource)
voip.register_servlets(hs, client_resource)
- admin.register_servlets(hs, client_resource)
pusher.register_servlets(hs, client_resource)
push_rule.register_servlets(hs, client_resource)
logout.register_servlets(hs, client_resource)
@@ -111,3 +115,8 @@ class ClientRestResource(JsonResource):
room_upgrade_rest_servlet.register_servlets(hs, client_resource)
capabilities.register_servlets(hs, client_resource)
account_validity.register_servlets(hs, client_resource)
+
+ # moving to /_synapse/admin
+ synapse.rest.admin.register_servlets_for_client_rest_resource(
+ hs, client_resource
+ )
diff --git a/synapse/rest/client/v1/admin.py b/synapse/rest/admin/__init__.py
index 0a1e233b23..744d85594f 100644
--- a/synapse/rest/client/v1/admin.py
+++ b/synapse/rest/admin/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
-# Copyright 2018 New Vector Ltd
+# Copyright 2018-2019 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ import hashlib
import hmac
import logging
import platform
+import re
from six import text_type
from six.moves import http_client
@@ -27,39 +28,56 @@ from twisted.internet import defer
import synapse
from synapse.api.constants import Membership, UserTypes
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
+from synapse.http.server import JsonResource
from synapse.http.servlet import (
+ RestServlet,
assert_params_in_dict,
parse_integer,
parse_json_object_from_request,
parse_string,
)
+from synapse.rest.admin._base import assert_requester_is_admin, assert_user_is_admin
+from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
from synapse.types import UserID, create_requester
from synapse.util.versionstring import get_version_string
-from .base import ClientV1RestServlet, client_path_patterns
-
logger = logging.getLogger(__name__)
-class UsersRestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/admin/users/(?P<user_id>[^/]*)")
+def historical_admin_path_patterns(path_regex):
+ """Returns the list of patterns for an admin endpoint, including historical ones
+
+ This is a backwards-compatibility hack. Previously, the Admin API was exposed at
+ various paths under /_matrix/client. This function returns a list of patterns
+ matching those paths (as well as the new one), so that existing scripts which rely
+ on the endpoints being available there are not broken.
+
+ Note that this should only be used for existing endpoints: new ones should just
+ register for the /_synapse/admin path.
+ """
+ return list(
+ re.compile(prefix + path_regex)
+ for prefix in (
+ "^/_synapse/admin/v1",
+ "^/_matrix/client/api/v1/admin",
+ "^/_matrix/client/unstable/admin",
+ "^/_matrix/client/r0/admin"
+ )
+ )
+
+
+class UsersRestServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns("/users/(?P<user_id>[^/]*)")
def __init__(self, hs):
- super(UsersRestServlet, self).__init__(hs)
+ self.hs = hs
+ self.auth = hs.get_auth()
self.handlers = hs.get_handlers()
@defer.inlineCallbacks
def on_GET(self, request, user_id):
target_user = UserID.from_string(user_id)
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
-
- # To allow all users to get the users list
- # if not is_admin and target_user != auth_user:
- # raise AuthError(403, "You are not a server admin")
+ yield assert_requester_is_admin(self.auth, request)
if not self.hs.is_mine(target_user):
raise SynapseError(400, "Can only users a local user")
@@ -69,37 +87,30 @@ class UsersRestServlet(ClientV1RestServlet):
defer.returnValue((200, ret))
-class VersionServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/admin/server_version")
-
- @defer.inlineCallbacks
- def on_GET(self, request):
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+class VersionServlet(RestServlet):
+ PATTERNS = (re.compile("^/_synapse/admin/v1/server_version$"), )
- ret = {
+ def __init__(self, hs):
+ self.res = {
'server_version': get_version_string(synapse),
'python_version': platform.python_version(),
}
- defer.returnValue((200, ret))
+ def on_GET(self, request):
+ return 200, self.res
-class UserRegisterServlet(ClientV1RestServlet):
+class UserRegisterServlet(RestServlet):
"""
Attributes:
NONCE_TIMEOUT (int): Seconds until a generated nonce won't be accepted
nonces (dict[str, int]): The nonces that we will accept. A dict of
nonce to the time it was generated, in int seconds.
"""
- PATTERNS = client_path_patterns("/admin/register")
+ PATTERNS = historical_admin_path_patterns("/register")
NONCE_TIMEOUT = 60
def __init__(self, hs):
- super(UserRegisterServlet, self).__init__(hs)
self.handlers = hs.get_handlers()
self.reactor = hs.get_reactor()
self.nonces = {}
@@ -226,11 +237,12 @@ class UserRegisterServlet(ClientV1RestServlet):
defer.returnValue((200, result))
-class WhoisRestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/admin/whois/(?P<user_id>[^/]*)")
+class WhoisRestServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns("/whois/(?P<user_id>[^/]*)")
def __init__(self, hs):
- super(WhoisRestServlet, self).__init__(hs)
+ self.hs = hs
+ self.auth = hs.get_auth()
self.handlers = hs.get_handlers()
@defer.inlineCallbacks
@@ -238,10 +250,9 @@ class WhoisRestServlet(ClientV1RestServlet):
target_user = UserID.from_string(user_id)
requester = yield self.auth.get_user_by_req(request)
auth_user = requester.user
- is_admin = yield self.auth.is_server_admin(requester.user)
- if not is_admin and target_user != auth_user:
- raise AuthError(403, "You are not a server admin")
+ if target_user != auth_user:
+ yield assert_user_is_admin(self.auth, auth_user)
if not self.hs.is_mine(target_user):
raise SynapseError(400, "Can only whois a local user")
@@ -251,20 +262,16 @@ class WhoisRestServlet(ClientV1RestServlet):
defer.returnValue((200, ret))
-class PurgeMediaCacheRestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/admin/purge_media_cache")
+class PurgeMediaCacheRestServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns("/purge_media_cache")
def __init__(self, hs):
self.media_repository = hs.get_media_repository()
- super(PurgeMediaCacheRestServlet, self).__init__(hs)
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request):
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_requester_is_admin(self.auth, request)
before_ts = parse_integer(request, "before_ts", required=True)
logger.info("before_ts: %r", before_ts)
@@ -274,9 +281,9 @@ class PurgeMediaCacheRestServlet(ClientV1RestServlet):
defer.returnValue((200, ret))
-class PurgeHistoryRestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns(
- "/admin/purge_history/(?P<room_id>[^/]*)(/(?P<event_id>[^/]+))?"
+class PurgeHistoryRestServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns(
+ "/purge_history/(?P<room_id>[^/]*)(/(?P<event_id>[^/]+))?"
)
def __init__(self, hs):
@@ -285,17 +292,13 @@ class PurgeHistoryRestServlet(ClientV1RestServlet):
Args:
hs (synapse.server.HomeServer)
"""
- super(PurgeHistoryRestServlet, self).__init__(hs)
self.pagination_handler = hs.get_pagination_handler()
self.store = hs.get_datastore()
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request, room_id, event_id):
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_requester_is_admin(self.auth, request)
body = parse_json_object_from_request(request, allow_empty_body=True)
@@ -371,9 +374,9 @@ class PurgeHistoryRestServlet(ClientV1RestServlet):
}))
-class PurgeHistoryStatusRestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns(
- "/admin/purge_history_status/(?P<purge_id>[^/]+)"
+class PurgeHistoryStatusRestServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns(
+ "/purge_history_status/(?P<purge_id>[^/]+)"
)
def __init__(self, hs):
@@ -382,16 +385,12 @@ class PurgeHistoryStatusRestServlet(ClientV1RestServlet):
Args:
hs (synapse.server.HomeServer)
"""
- super(PurgeHistoryStatusRestServlet, self).__init__(hs)
self.pagination_handler = hs.get_pagination_handler()
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_GET(self, request, purge_id):
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_requester_is_admin(self.auth, request)
purge_status = self.pagination_handler.get_purge_status(purge_id)
if purge_status is None:
@@ -400,15 +399,16 @@ class PurgeHistoryStatusRestServlet(ClientV1RestServlet):
defer.returnValue((200, purge_status.asdict()))
-class DeactivateAccountRestServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/admin/deactivate/(?P<target_user_id>[^/]*)")
+class DeactivateAccountRestServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns("/deactivate/(?P<target_user_id>[^/]*)")
def __init__(self, hs):
- super(DeactivateAccountRestServlet, self).__init__(hs)
self._deactivate_account_handler = hs.get_deactivate_account_handler()
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request, target_user_id):
+ yield assert_requester_is_admin(self.auth, request)
body = parse_json_object_from_request(request, allow_empty_body=True)
erase = body.get("erase", False)
if not isinstance(erase, bool):
@@ -419,11 +419,6 @@ class DeactivateAccountRestServlet(ClientV1RestServlet):
)
UserID.from_string(target_user_id)
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
result = yield self._deactivate_account_handler.deactivate_account(
target_user_id, erase,
@@ -438,13 +433,13 @@ class DeactivateAccountRestServlet(ClientV1RestServlet):
}))
-class ShutdownRoomRestServlet(ClientV1RestServlet):
+class ShutdownRoomRestServlet(RestServlet):
"""Shuts down a room by removing all local users from the room and blocking
all future invites and joins to the room. Any local aliases will be repointed
to a new room created by `new_room_user_id` and kicked users will be auto
joined to the new room.
"""
- PATTERNS = client_path_patterns("/admin/shutdown_room/(?P<room_id>[^/]+)")
+ PATTERNS = historical_admin_path_patterns("/shutdown_room/(?P<room_id>[^/]+)")
DEFAULT_MESSAGE = (
"Sharing illegal content on this server is not permitted and rooms in"
@@ -452,19 +447,18 @@ class ShutdownRoomRestServlet(ClientV1RestServlet):
)
def __init__(self, hs):
- super(ShutdownRoomRestServlet, self).__init__(hs)
+ self.hs = hs
self.store = hs.get_datastore()
self.state = hs.get_state_handler()
self._room_creation_handler = hs.get_room_creation_handler()
self.event_creation_handler = hs.get_event_creation_handler()
self.room_member_handler = hs.get_room_member_handler()
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request, room_id):
requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_user_is_admin(self.auth, requester.user)
content = parse_json_object_from_request(request)
assert_params_in_dict(content, ["new_room_user_id"])
@@ -564,22 +558,20 @@ class ShutdownRoomRestServlet(ClientV1RestServlet):
}))
-class QuarantineMediaInRoom(ClientV1RestServlet):
+class QuarantineMediaInRoom(RestServlet):
"""Quarantines all media in a room so that no one can download it via
this server.
"""
- PATTERNS = client_path_patterns("/admin/quarantine_media/(?P<room_id>[^/]+)")
+ PATTERNS = historical_admin_path_patterns("/quarantine_media/(?P<room_id>[^/]+)")
def __init__(self, hs):
- super(QuarantineMediaInRoom, self).__init__(hs)
self.store = hs.get_datastore()
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request, room_id):
requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_user_is_admin(self.auth, requester.user)
num_quarantined = yield self.store.quarantine_media_ids_in_room(
room_id, requester.user.to_string(),
@@ -588,13 +580,12 @@ class QuarantineMediaInRoom(ClientV1RestServlet):
defer.returnValue((200, {"num_quarantined": num_quarantined}))
-class ListMediaInRoom(ClientV1RestServlet):
+class ListMediaInRoom(RestServlet):
"""Lists all of the media in a given room.
"""
- PATTERNS = client_path_patterns("/admin/room/(?P<room_id>[^/]+)/media")
+ PATTERNS = historical_admin_path_patterns("/room/(?P<room_id>[^/]+)/media")
def __init__(self, hs):
- super(ListMediaInRoom, self).__init__(hs)
self.store = hs.get_datastore()
@defer.inlineCallbacks
@@ -609,11 +600,11 @@ class ListMediaInRoom(ClientV1RestServlet):
defer.returnValue((200, {"local": local_mxcs, "remote": remote_mxcs}))
-class ResetPasswordRestServlet(ClientV1RestServlet):
+class ResetPasswordRestServlet(RestServlet):
"""Post request to allow an administrator reset password for a user.
This needs user to have administrator access in Synapse.
Example:
- http://localhost:8008/_matrix/client/api/v1/admin/reset_password/
+ http://localhost:8008/_synapse/admin/v1/reset_password/
@user:to_reset_password?access_token=admin_access_token
JsonBodyToSend:
{
@@ -622,11 +613,10 @@ class ResetPasswordRestServlet(ClientV1RestServlet):
Returns:
200 OK with empty object if success otherwise an error.
"""
- PATTERNS = client_path_patterns("/admin/reset_password/(?P<target_user_id>[^/]*)")
+ PATTERNS = historical_admin_path_patterns("/reset_password/(?P<target_user_id>[^/]*)")
def __init__(self, hs):
self.store = hs.get_datastore()
- super(ResetPasswordRestServlet, self).__init__(hs)
self.hs = hs
self.auth = hs.get_auth()
self._set_password_handler = hs.get_set_password_handler()
@@ -636,12 +626,10 @@ class ResetPasswordRestServlet(ClientV1RestServlet):
"""Post request to allow an administrator reset password for a user.
This needs user to have administrator access in Synapse.
"""
- UserID.from_string(target_user_id)
requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
+ yield assert_user_is_admin(self.auth, requester.user)
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ UserID.from_string(target_user_id)
params = parse_json_object_from_request(request)
assert_params_in_dict(params, ["new_password"])
@@ -653,20 +641,19 @@ class ResetPasswordRestServlet(ClientV1RestServlet):
defer.returnValue((200, {}))
-class GetUsersPaginatedRestServlet(ClientV1RestServlet):
+class GetUsersPaginatedRestServlet(RestServlet):
"""Get request to get specific number of users from Synapse.
This needs user to have administrator access in Synapse.
Example:
- http://localhost:8008/_matrix/client/api/v1/admin/users_paginate/
+ http://localhost:8008/_synapse/admin/v1/users_paginate/
@admin:user?access_token=admin_access_token&start=0&limit=10
Returns:
200 OK with json object {list[dict[str, Any]], count} or empty object.
"""
- PATTERNS = client_path_patterns("/admin/users_paginate/(?P<target_user_id>[^/]*)")
+ PATTERNS = historical_admin_path_patterns("/users_paginate/(?P<target_user_id>[^/]*)")
def __init__(self, hs):
self.store = hs.get_datastore()
- super(GetUsersPaginatedRestServlet, self).__init__(hs)
self.hs = hs
self.auth = hs.get_auth()
self.handlers = hs.get_handlers()
@@ -676,16 +663,9 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
"""Get request to get specific number of users from Synapse.
This needs user to have administrator access in Synapse.
"""
- target_user = UserID.from_string(target_user_id)
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_requester_is_admin(self.auth, request)
- # To allow all users to get the users list
- # if not is_admin and target_user != auth_user:
- # raise AuthError(403, "You are not a server admin")
+ target_user = UserID.from_string(target_user_id)
if not self.hs.is_mine(target_user):
raise SynapseError(400, "Can only users a local user")
@@ -706,7 +686,7 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
"""Post request to get specific number of users from Synapse..
This needs user to have administrator access in Synapse.
Example:
- http://localhost:8008/_matrix/client/api/v1/admin/users_paginate/
+ http://localhost:8008/_synapse/admin/v1/users_paginate/
@admin:user?access_token=admin_access_token
JsonBodyToSend:
{
@@ -716,12 +696,8 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
Returns:
200 OK with json object {list[dict[str, Any]], count} or empty object.
"""
+ yield assert_requester_is_admin(self.auth, request)
UserID.from_string(target_user_id)
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
order = "name" # order by name in user table
params = parse_json_object_from_request(request)
@@ -736,21 +712,20 @@ class GetUsersPaginatedRestServlet(ClientV1RestServlet):
defer.returnValue((200, ret))
-class SearchUsersRestServlet(ClientV1RestServlet):
+class SearchUsersRestServlet(RestServlet):
"""Get request to search user table for specific users according to
search term.
This needs user to have administrator access in Synapse.
Example:
- http://localhost:8008/_matrix/client/api/v1/admin/search_users/
+ http://localhost:8008/_synapse/admin/v1/search_users/
@admin:user?access_token=admin_access_token&term=alice
Returns:
200 OK with json object {list[dict[str, Any]], count} or empty object.
"""
- PATTERNS = client_path_patterns("/admin/search_users/(?P<target_user_id>[^/]*)")
+ PATTERNS = historical_admin_path_patterns("/search_users/(?P<target_user_id>[^/]*)")
def __init__(self, hs):
self.store = hs.get_datastore()
- super(SearchUsersRestServlet, self).__init__(hs)
self.hs = hs
self.auth = hs.get_auth()
self.handlers = hs.get_handlers()
@@ -761,12 +736,9 @@ class SearchUsersRestServlet(ClientV1RestServlet):
search term.
This needs user to have a administrator access in Synapse.
"""
- target_user = UserID.from_string(target_user_id)
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
+ yield assert_requester_is_admin(self.auth, request)
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ target_user = UserID.from_string(target_user_id)
# To allow all users to get the users list
# if not is_admin and target_user != auth_user:
@@ -784,23 +756,20 @@ class SearchUsersRestServlet(ClientV1RestServlet):
defer.returnValue((200, ret))
-class DeleteGroupAdminRestServlet(ClientV1RestServlet):
+class DeleteGroupAdminRestServlet(RestServlet):
"""Allows deleting of local groups
"""
- PATTERNS = client_path_patterns("/admin/delete_group/(?P<group_id>[^/]*)")
+ PATTERNS = historical_admin_path_patterns("/delete_group/(?P<group_id>[^/]*)")
def __init__(self, hs):
- super(DeleteGroupAdminRestServlet, self).__init__(hs)
self.group_server = hs.get_groups_server_handler()
self.is_mine_id = hs.is_mine_id
+ self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request, group_id):
requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_user_is_admin(self.auth, requester.user)
if not self.is_mine_id(group_id):
raise SynapseError(400, "Can only delete local groups")
@@ -809,27 +778,21 @@ class DeleteGroupAdminRestServlet(ClientV1RestServlet):
defer.returnValue((200, {}))
-class AccountValidityRenewServlet(ClientV1RestServlet):
- PATTERNS = client_path_patterns("/admin/account_validity/validity$")
+class AccountValidityRenewServlet(RestServlet):
+ PATTERNS = historical_admin_path_patterns("/account_validity/validity$")
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
- super(AccountValidityRenewServlet, self).__init__(hs)
-
self.hs = hs
self.account_activity_handler = hs.get_account_validity_handler()
self.auth = hs.get_auth()
@defer.inlineCallbacks
def on_POST(self, request):
- requester = yield self.auth.get_user_by_req(request)
- is_admin = yield self.auth.is_server_admin(requester.user)
-
- if not is_admin:
- raise AuthError(403, "You are not a server admin")
+ yield assert_requester_is_admin(self.auth, request)
body = parse_json_object_from_request(request)
@@ -846,8 +809,27 @@ class AccountValidityRenewServlet(ClientV1RestServlet):
}
defer.returnValue((200, res))
+########################################################################################
+#
+# please don't add more servlets here: this file is already long and unwieldy. Put
+# them in separate files within the 'admin' package.
+#
+########################################################################################
+
+
+class AdminRestResource(JsonResource):
+ """The REST resource which gets mounted at /_synapse/admin"""
+
+ def __init__(self, hs):
+ JsonResource.__init__(self, hs, canonical_json=False)
+
+ register_servlets_for_client_rest_resource(hs, self)
+ SendServerNoticeServlet(hs).register(self)
+ VersionServlet(hs).register(self)
+
-def register_servlets(hs, http_server):
+def register_servlets_for_client_rest_resource(hs, http_server):
+ """Register only the servlets which need to be exposed on /_matrix/client/xxx"""
WhoisRestServlet(hs).register(http_server)
PurgeMediaCacheRestServlet(hs).register(http_server)
PurgeHistoryStatusRestServlet(hs).register(http_server)
@@ -861,6 +843,7 @@ def register_servlets(hs, http_server):
QuarantineMediaInRoom(hs).register(http_server)
ListMediaInRoom(hs).register(http_server)
UserRegisterServlet(hs).register(http_server)
- VersionServlet(hs).register(http_server)
DeleteGroupAdminRestServlet(hs).register(http_server)
AccountValidityRenewServlet(hs).register(http_server)
+ # don't add more things here: new servlets should only be exposed on
+ # /_synapse/admin so should not go here. Instead register them in AdminRestResource.
diff --git a/synapse/rest/admin/_base.py b/synapse/rest/admin/_base.py
new file mode 100644
index 0000000000..881d67b89c
--- /dev/null
+++ b/synapse/rest/admin/_base.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 New Vector Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from twisted.internet import defer
+
+from synapse.api.errors import AuthError
+
+
+@defer.inlineCallbacks
+def assert_requester_is_admin(auth, request):
+ """Verify that the requester is an admin user
+
+ WARNING: MAKE SURE YOU YIELD ON THE RESULT!
+
+ Args:
+ auth (synapse.api.auth.Auth):
+ request (twisted.web.server.Request): incoming request
+
+ Returns:
+ Deferred
+
+ Raises:
+ AuthError if the requester is not an admin
+ """
+ requester = yield auth.get_user_by_req(request)
+ yield assert_user_is_admin(auth, requester.user)
+
+
+@defer.inlineCallbacks
+def assert_user_is_admin(auth, user_id):
+ """Verify that the given user is an admin user
+
+ WARNING: MAKE SURE YOU YIELD ON THE RESULT!
+
+ Args:
+ auth (synapse.api.auth.Auth):
+ user_id (UserID):
+
+ Returns:
+ Deferred
+
+ Raises:
+ AuthError if the user is not an admin
+ """
+
+ is_admin = yield auth.is_server_admin(user_id)
+ if not is_admin:
+ raise AuthError(403, "You are not a server admin")
diff --git a/synapse/rest/admin/server_notice_servlet.py b/synapse/rest/admin/server_notice_servlet.py
new file mode 100644
index 0000000000..ae5aca9dac
--- /dev/null
+++ b/synapse/rest/admin/server_notice_servlet.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 New Vector 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 re
+
+from twisted.internet import defer
+
+from synapse.api.constants import EventTypes
+from synapse.api.errors import SynapseError
+from synapse.http.servlet import (
+ RestServlet,
+ assert_params_in_dict,
+ parse_json_object_from_request,
+)
+from synapse.rest.admin import assert_requester_is_admin
+from synapse.rest.client.transactions import HttpTransactionCache
+from synapse.types import UserID
+
+
+class SendServerNoticeServlet(RestServlet):
+ """Servlet which will send a server notice to a given user
+
+ POST /_synapse/admin/v1/send_server_notice
+ {
+ "user_id": "@target_user:server_name",
+ "content": {
+ "msgtype": "m.text",
+ "body": "This is my message"
+ }
+ }
+
+ returns:
+
+ {
+ "event_id": "$1895723857jgskldgujpious"
+ }
+ """
+ def __init__(self, hs):
+ """
+ Args:
+ hs (synapse.server.HomeServer): server
+ """
+ self.hs = hs
+ self.auth = hs.get_auth()
+ self.txns = HttpTransactionCache(hs)
+ self.snm = hs.get_server_notices_manager()
+
+ def register(self, json_resource):
+ PATTERN = "^/_synapse/admin/v1/send_server_notice"
+ json_resource.register_paths(
+ "POST",
+ (re.compile(PATTERN + "$"), ),
+ self.on_POST,
+ )
+ json_resource.register_paths(
+ "PUT",
+ (re.compile(PATTERN + "/(?P<txn_id>[^/]*)$",), ),
+ self.on_PUT,
+ )
+
+ @defer.inlineCallbacks
+ def on_POST(self, request, txn_id=None):
+ yield assert_requester_is_admin(self.auth, request)
+ body = parse_json_object_from_request(request)
+ assert_params_in_dict(body, ("user_id", "content"))
+ event_type = body.get("type", EventTypes.Message)
+ state_key = body.get("state_key")
+
+ if not self.snm.is_enabled():
+ raise SynapseError(400, "Server notices are not enabled on this server")
+
+ user_id = body["user_id"]
+ UserID.from_string(user_id)
+ if not self.hs.is_mine_id(user_id):
+ raise SynapseError(400, "Server notices can only be sent to local users")
+
+ event = yield self.snm.send_notice(
+ user_id=body["user_id"],
+ type=event_type,
+ state_key=state_key,
+ event_content=body["content"],
+ )
+
+ defer.returnValue((200, {"event_id": event.event_id}))
+
+ def on_PUT(self, request, txn_id):
+ return self.txns.fetch_or_execute_request(
+ request, self.on_POST, request, txn_id,
+ )
diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py
index a23edd8fe5..eac1966c5e 100644
--- a/synapse/rest/client/v1/profile.py
+++ b/synapse/rest/client/v1/profile.py
@@ -31,11 +31,17 @@ class ProfileDisplaynameRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks
def on_GET(self, request, user_id):
+ requester_user = None
+
+ if self.hs.config.require_auth_for_profile_requests:
+ requester = yield self.auth.get_user_by_req(request)
+ requester_user = requester.user
+
user = UserID.from_string(user_id)
- displayname = yield self.profile_handler.get_displayname(
- user,
- )
+ yield self.profile_handler.check_profile_query_allowed(user, requester_user)
+
+ displayname = yield self.profile_handler.get_displayname(user)
ret = {}
if displayname is not None:
@@ -74,11 +80,17 @@ class ProfileAvatarURLRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks
def on_GET(self, request, user_id):
+ requester_user = None
+
+ if self.hs.config.require_auth_for_profile_requests:
+ requester = yield self.auth.get_user_by_req(request)
+ requester_user = requester.user
+
user = UserID.from_string(user_id)
- avatar_url = yield self.profile_handler.get_avatar_url(
- user,
- )
+ yield self.profile_handler.check_profile_query_allowed(user, requester_user)
+
+ avatar_url = yield self.profile_handler.get_avatar_url(user)
ret = {}
if avatar_url is not None:
@@ -116,14 +128,18 @@ class ProfileRestServlet(ClientV1RestServlet):
@defer.inlineCallbacks
def on_GET(self, request, user_id):
+ requester_user = None
+
+ if self.hs.config.require_auth_for_profile_requests:
+ requester = yield self.auth.get_user_by_req(request)
+ requester_user = requester.user
+
user = UserID.from_string(user_id)
- displayname = yield self.profile_handler.get_displayname(
- user,
- )
- avatar_url = yield self.profile_handler.get_avatar_url(
- user,
- )
+ yield self.profile_handler.check_profile_query_allowed(user, requester_user)
+
+ displayname = yield self.profile_handler.get_displayname(user)
+ avatar_url = yield self.profile_handler.get_avatar_url(user)
ret = {}
if displayname is not None:
diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py
index 48da4d557f..fab04965cb 100644
--- a/synapse/rest/client/v1/room.py
+++ b/synapse/rest/client/v1/room.py
@@ -301,6 +301,12 @@ class PublicRoomListRestServlet(ClientV1RestServlet):
try:
yield self.auth.get_user_by_req(request, allow_guest=True)
except AuthError as e:
+ # Option to allow servers to require auth when accessing
+ # /publicRooms via CS API. This is especially helpful in private
+ # federations.
+ if self.hs.config.restrict_public_rooms_to_local_users:
+ raise
+
# We allow people to not be authed if they're just looking at our
# room list, but require auth when we proxy the request.
# In both cases we call the auth function, as that has the side
diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py
index fdcb375f95..69dffd8244 100644
--- a/synapse/util/stringutils.py
+++ b/synapse/util/stringutils.py
@@ -24,14 +24,19 @@ _string_with_symbols = (
string.digits + string.ascii_letters + ".,;:^&*-_+=#~@"
)
+# random_string and random_string_with_symbols are used for a range of things,
+# some cryptographically important, some less so. We use SystemRandom to make sure
+# we get cryptographically-secure randoms.
+rand = random.SystemRandom()
+
def random_string(length):
- return ''.join(random.choice(string.ascii_letters) for _ in range(length))
+ return ''.join(rand.choice(string.ascii_letters) for _ in range(length))
def random_string_with_symbols(length):
return ''.join(
- random.choice(_string_with_symbols) for _ in range(length)
+ rand.choice(_string_with_symbols) for _ in range(length)
)
|