Merge remote-tracking branch 'origin/develop' into shhs
This commit is contained in:
20
CHANGES.md
20
CHANGES.md
@@ -1,3 +1,23 @@
|
||||
Synapse 0.99.3.2 (2019-05-03)
|
||||
=============================
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Ensure that we have `urllib3` <1.25, to resolve incompatibility with `requests`. ([\#5135](https://github.com/matrix-org/synapse/issues/5135))
|
||||
|
||||
|
||||
Synapse 0.99.3.1 (2019-05-03)
|
||||
=============================
|
||||
|
||||
Security update
|
||||
---------------
|
||||
|
||||
This release includes two security fixes:
|
||||
|
||||
- Switch to using a cryptographically-secure random number generator for token strings, ensuring they cannot be predicted by an attacker. Thanks to @opnsec for identifying and responsibly disclosing this issue! ([\#5133](https://github.com/matrix-org/synapse/issues/5133))
|
||||
- Blacklist 0.0.0.0 and :: by default for URL previews. Thanks to @opnsec for identifying and responsibly disclosing this issue too! ([\#5134](https://github.com/matrix-org/synapse/issues/5134))
|
||||
|
||||
Synapse 0.99.3 (2019-04-01)
|
||||
===========================
|
||||
|
||||
|
||||
1
changelog.d/5037.bugfix
Normal file
1
changelog.d/5037.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Workaround bug in twisted where attempting too many concurrent DNS requests could cause it to hang due to running out of file descriptors.
|
||||
1
changelog.d/5083.feature
Normal file
1
changelog.d/5083.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add an configuration option to require authentication on /publicRooms and /profile endpoints.
|
||||
1
changelog.d/5104.bugfix
Normal file
1
changelog.d/5104.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix the ratelimting on third party invites.
|
||||
1
changelog.d/5119.feature
Normal file
1
changelog.d/5119.feature
Normal file
@@ -0,0 +1 @@
|
||||
Move admin APIs to `/_synapse/admin/v1`. (The old paths are retained for backwards-compatibility, for now).
|
||||
1
changelog.d/5120.misc
Normal file
1
changelog.d/5120.misc
Normal file
@@ -0,0 +1 @@
|
||||
Factor out an "assert_requester_is_admin" function.
|
||||
1
changelog.d/5121.feature
Normal file
1
changelog.d/5121.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement an admin API for sending server notices. Many thanks to @krombel who provided a foundation for this work.
|
||||
1
changelog.d/5122.misc
Normal file
1
changelog.d/5122.misc
Normal file
@@ -0,0 +1 @@
|
||||
Remove the requirement to authenticate for /admin/server_version.
|
||||
1
changelog.d/5124.bugfix
Normal file
1
changelog.d/5124.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Add some missing limitations to room alias creation.
|
||||
1
changelog.d/5128.bugfix
Normal file
1
changelog.d/5128.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Add some missing limitations to room alias creation.
|
||||
1
changelog.d/5142.feature
Normal file
1
changelog.d/5142.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement an admin API for sending server notices. Many thanks to @krombel who provided a foundation for this work.
|
||||
1
changelog.d/5154.bugfix
Normal file
1
changelog.d/5154.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Fix bogus imports in unit tests.
|
||||
12
debian/changelog
vendored
12
debian/changelog
vendored
@@ -1,3 +1,15 @@
|
||||
matrix-synapse-py3 (0.99.3.2) stable; urgency=medium
|
||||
|
||||
* New synapse release 0.99.3.2.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Fri, 03 May 2019 18:56:20 +0100
|
||||
|
||||
matrix-synapse-py3 (0.99.3.1) stable; urgency=medium
|
||||
|
||||
* New synapse release 0.99.3.1.
|
||||
|
||||
-- Synapse Packaging team <packages@matrix.org> Fri, 03 May 2019 16:02:43 +0100
|
||||
|
||||
matrix-synapse-py3 (0.99.3) stable; urgency=medium
|
||||
|
||||
[ Richard van der Hoff ]
|
||||
|
||||
@@ -57,7 +57,8 @@ RUN apt-get update -qq -o Acquire::Languages=none \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-venv \
|
||||
sqlite3
|
||||
sqlite3 \
|
||||
libpq-dev
|
||||
|
||||
COPY --from=builder /dh-virtualenv_1.1-1_all.deb /
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ This API extends the validity of an account by as much time as configured in the
|
||||
|
||||
The API is::
|
||||
|
||||
POST /_matrix/client/unstable/admin/account_validity/validity
|
||||
POST /_synapse/admin/v1/account_validity/validity
|
||||
|
||||
with the following body:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ being deleted.
|
||||
The API is:
|
||||
|
||||
```
|
||||
POST /_matrix/client/r0/admin/delete_group/<group_id>
|
||||
POST /_synapse/admin/v1/delete_group/<group_id>
|
||||
```
|
||||
|
||||
including an `access_token` of a server admin.
|
||||
|
||||
@@ -4,7 +4,7 @@ This API gets a list of known media in a room.
|
||||
|
||||
The API is:
|
||||
```
|
||||
GET /_matrix/client/r0/admin/room/<room_id>/media
|
||||
GET /_synapse/admin/v1/room/<room_id>/media
|
||||
```
|
||||
including an `access_token` of a server admin.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ paginate further back in the room from the point being purged from.
|
||||
|
||||
The API is:
|
||||
|
||||
``POST /_matrix/client/r0/admin/purge_history/<room_id>[/<event_id>]``
|
||||
``POST /_synapse/admin/v1/purge_history/<room_id>[/<event_id>]``
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
@@ -49,7 +49,7 @@ Purge status query
|
||||
|
||||
It is possible to poll for updates on recent purges with a second API;
|
||||
|
||||
``GET /_matrix/client/r0/admin/purge_history_status/<purge_id>``
|
||||
``GET /_synapse/admin/v1/purge_history_status/<purge_id>``
|
||||
|
||||
(again, with a suitable ``access_token``). This API returns a JSON body like
|
||||
the following:
|
||||
|
||||
@@ -6,7 +6,7 @@ media.
|
||||
|
||||
The API is::
|
||||
|
||||
POST /_matrix/client/r0/admin/purge_media_cache?before_ts=<unix_timestamp_in_ms>&access_token=<access_token>
|
||||
POST /_synapse/admin/v1/purge_media_cache?before_ts=<unix_timestamp_in_ms>&access_token=<access_token>
|
||||
|
||||
{}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ is not enabled.
|
||||
|
||||
To fetch the nonce, you need to request one from the API::
|
||||
|
||||
> GET /_matrix/client/r0/admin/register
|
||||
> GET /_synapse/admin/v1/register
|
||||
|
||||
< {"nonce": "thisisanonce"}
|
||||
|
||||
@@ -22,7 +22,7 @@ body containing the nonce, username, password, whether they are an admin
|
||||
|
||||
As an example::
|
||||
|
||||
> POST /_matrix/client/r0/admin/register
|
||||
> POST /_synapse/admin/v1/register
|
||||
> {
|
||||
"nonce": "thisisanonce",
|
||||
"username": "pepper_roni",
|
||||
|
||||
48
docs/admin_api/server_notices.md
Normal file
48
docs/admin_api/server_notices.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Server Notices
|
||||
|
||||
The API to send notices is as follows:
|
||||
|
||||
```
|
||||
POST /_synapse/admin/v1/send_server_notice
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```
|
||||
PUT /_synapse/admin/v1/send_server_notice/{txnId}
|
||||
```
|
||||
|
||||
You will need to authenticate with an access token for an admin user.
|
||||
|
||||
When using the `PUT` form, retransmissions with the same transaction ID will be
|
||||
ignored in the same way as with `PUT
|
||||
/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`.
|
||||
|
||||
The request body should look something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "@target_user:server_name",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "This is my message"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can optionally include the following additional parameters:
|
||||
|
||||
* `type`: the type of event. Defaults to `m.room.message`.
|
||||
* `state_key`: Setting this will result in a state event being sent.
|
||||
|
||||
|
||||
Once the notice has been sent, the API will return the following response:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "<event_id>"
|
||||
}
|
||||
```
|
||||
|
||||
Note that server notices must be enabled in `homeserver.yaml` before this API
|
||||
can be used. See [server_notices.md](../server_notices.md) for more information.
|
||||
@@ -5,7 +5,7 @@ This API returns information about a specific user account.
|
||||
|
||||
The api is::
|
||||
|
||||
GET /_matrix/client/r0/admin/whois/<user_id>
|
||||
GET /_synapse/admin/v1/whois/<user_id>
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
@@ -50,7 +50,7 @@ references to it).
|
||||
|
||||
The api is::
|
||||
|
||||
POST /_matrix/client/r0/admin/deactivate/<user_id>
|
||||
POST /_synapse/admin/v1/deactivate/<user_id>
|
||||
|
||||
with a body of:
|
||||
|
||||
@@ -73,7 +73,7 @@ Changes the password of another user.
|
||||
|
||||
The api is::
|
||||
|
||||
POST /_matrix/client/r0/admin/reset_password/<user_id>
|
||||
POST /_synapse/admin/v1/reset_password/<user_id>
|
||||
|
||||
with a body of:
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ contains Synapse version information).
|
||||
|
||||
The api is::
|
||||
|
||||
GET /_matrix/client/r0/admin/server_version
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
GET /_synapse/admin/v1/server_version
|
||||
|
||||
It returns a JSON body like the following:
|
||||
|
||||
|
||||
@@ -69,6 +69,20 @@ pid_file: DATADIR/homeserver.pid
|
||||
#
|
||||
#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]
|
||||
@@ -136,8 +150,8 @@ pid_file: DATADIR/homeserver.pid
|
||||
#
|
||||
# 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.
|
||||
@@ -239,6 +253,11 @@ listeners:
|
||||
# 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
|
||||
|
||||
|
||||
## TLS ##
|
||||
|
||||
@@ -543,11 +562,12 @@ uploads_path: "DATADIR/uploads"
|
||||
# height: 600
|
||||
# method: scale
|
||||
|
||||
# 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?
|
||||
#
|
||||
#url_preview_enabled: false
|
||||
# 'false' by default: uncomment the following to enable it (and specify a
|
||||
# url_preview_ip_range_blacklist blacklist).
|
||||
#
|
||||
#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
|
||||
@@ -557,6 +577,12 @@ uploads_path: "DATADIR/uploads"
|
||||
# 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'
|
||||
@@ -567,7 +593,7 @@ uploads_path: "DATADIR/uploads"
|
||||
# - '::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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Server Notices
|
||||
==============
|
||||
# Server Notices
|
||||
|
||||
'Server Notices' are a new feature introduced in Synapse 0.30. They provide a
|
||||
channel whereby server administrators can send messages to users on the server.
|
||||
@@ -11,8 +10,7 @@ they may also find a use for features such as "Message of the day".
|
||||
This is a feature specific to Synapse, but it uses standard Matrix
|
||||
communication mechanisms, so should work with any Matrix client.
|
||||
|
||||
User experience
|
||||
---------------
|
||||
## User experience
|
||||
|
||||
When the user is first sent a server notice, they will get an invitation to a
|
||||
room (typically called 'Server Notices', though this is configurable in
|
||||
@@ -29,8 +27,7 @@ levels.
|
||||
Having joined the room, the user can leave the room if they want. Subsequent
|
||||
server notices will then cause a new room to be created.
|
||||
|
||||
Synapse configuration
|
||||
---------------------
|
||||
## Synapse configuration
|
||||
|
||||
Server notices come from a specific user id on the server. Server
|
||||
administrators are free to choose the user id - something like `server` is
|
||||
@@ -58,17 +55,7 @@ room which will be created.
|
||||
`system_mxid_display_name` and `system_mxid_avatar_url` can be used to set the
|
||||
displayname and avatar of the Server Notices user.
|
||||
|
||||
Sending notices
|
||||
---------------
|
||||
## Sending notices
|
||||
|
||||
As of the current version of synapse, there is no convenient interface for
|
||||
sending notices (other than the automated ones sent as part of consent
|
||||
tracking).
|
||||
|
||||
In the meantime, it is possible to test this feature using the manhole. Having
|
||||
gone into the manhole as described in [manhole.md](manhole.md), a notice can be
|
||||
sent with something like:
|
||||
|
||||
```
|
||||
>>> hs.get_server_notices_manager().send_notice('@user:server.com', {'msgtype':'m.text', 'body':'foo'})
|
||||
```
|
||||
To send server notices to users you can use the
|
||||
[admin_api](admin_api/server_notices.md).
|
||||
|
||||
@@ -24,6 +24,7 @@ DISTS = (
|
||||
"ubuntu:xenial",
|
||||
"ubuntu:bionic",
|
||||
"ubuntu:cosmic",
|
||||
"ubuntu:disco",
|
||||
)
|
||||
|
||||
DESC = '''\
|
||||
|
||||
@@ -27,4 +27,4 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__version__ = "0.99.3"
|
||||
__version__ = "0.99.3.2"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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?
|
||||
#
|
||||
#url_preview_enabled: false
|
||||
# 'false' by default: uncomment the following to enable it (and specify a
|
||||
# url_preview_ip_range_blacklist blacklist).
|
||||
#
|
||||
#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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
class VersionServlet(RestServlet):
|
||||
PATTERNS = (re.compile("^/_synapse/admin/v1/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")
|
||||
|
||||
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.
|
||||
"""
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
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")
|
||||
|
||||
# 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")
|
||||
|
||||
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.
|
||||
#
|
||||
########################################################################################
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
|
||||
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_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.
|
||||
59
synapse/rest/admin/_base.py
Normal file
59
synapse/rest/admin/_base.py
Normal file
@@ -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")
|
||||
100
synapse/rest/admin/server_notice_servlet.py
Normal file
100
synapse/rest/admin/server_notice_servlet.py
Normal file
@@ -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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
# limitations under the License.
|
||||
from mock import Mock
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import UserTypes
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.rest.client.v2_alpha import user_directory
|
||||
from synapse.storage.roommember import ProfileInfo
|
||||
|
||||
@@ -29,7 +30,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
login.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
@@ -327,7 +328,7 @@ class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
|
||||
user_directory.register_servlets,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
|
||||
@@ -19,7 +19,8 @@ import pkg_resources
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
|
||||
from tests.unittest import HomeserverTestCase
|
||||
|
||||
@@ -33,7 +34,7 @@ class EmailPusherTests(HomeserverTestCase):
|
||||
|
||||
skip = "No Jinja installed" if not load_jinja2_templates else None
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
@@ -17,7 +17,8 @@ from mock import Mock
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.util.logcontext import make_deferred_yieldable
|
||||
|
||||
from tests.unittest import HomeserverTestCase
|
||||
@@ -32,7 +33,7 @@ class HTTPPusherTests(HomeserverTestCase):
|
||||
|
||||
skip = "No Jinja installed" if not load_jinja2_templates else None
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
14
tests/rest/admin/__init__.py
Normal file
14
tests/rest/admin/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- 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.
|
||||
@@ -19,28 +19,26 @@ import json
|
||||
|
||||
from mock import Mock
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import UserTypes
|
||||
from synapse.rest.client.v1 import admin, events, login, room
|
||||
from synapse.http.server import JsonResource
|
||||
from synapse.rest.admin import VersionServlet
|
||||
from synapse.rest.client.v1 import events, login, room
|
||||
from synapse.rest.client.v2_alpha import groups
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class VersionTestCase(unittest.HomeserverTestCase):
|
||||
url = '/_synapse/admin/v1/server_version'
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
url = '/_matrix/client/r0/admin/server_version'
|
||||
def create_test_json_resource(self):
|
||||
resource = JsonResource(self.hs)
|
||||
VersionServlet(self.hs).register(resource)
|
||||
return resource
|
||||
|
||||
def test_version_string(self):
|
||||
self.register_user("admin", "pass", admin=True)
|
||||
self.admin_token = self.login("admin", "pass")
|
||||
|
||||
request, channel = self.make_request("GET", self.url,
|
||||
access_token=self.admin_token)
|
||||
request, channel = self.make_request("GET", self.url, shorthand=False)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(200, int(channel.result["code"]),
|
||||
@@ -48,21 +46,10 @@ class VersionTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual({'server_version', 'python_version'},
|
||||
set(channel.json_body.keys()))
|
||||
|
||||
def test_inaccessible_to_non_admins(self):
|
||||
self.register_user("unprivileged-user", "pass", admin=False)
|
||||
user_token = self.login("unprivileged-user", "pass")
|
||||
|
||||
request, channel = self.make_request("GET", self.url,
|
||||
access_token=user_token)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(403, int(channel.result['code']),
|
||||
msg=channel.result['body'])
|
||||
|
||||
|
||||
class UserRegisterTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [admin.register_servlets]
|
||||
servlets = [synapse.rest.admin.register_servlets_for_client_rest_resource]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
|
||||
@@ -358,7 +345,7 @@ class UserRegisterTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
events.register_servlets,
|
||||
room.register_servlets,
|
||||
@@ -495,7 +482,7 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
class DeleteGroupTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
groups.register_servlets,
|
||||
]
|
||||
@@ -15,8 +15,9 @@
|
||||
|
||||
import os
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.urls import ConsentURIBuilder
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.rest.consent import consent_resource
|
||||
|
||||
from tests import unittest
|
||||
@@ -31,7 +32,7 @@ except Exception:
|
||||
class ConsentResourceTestCase(unittest.HomeserverTestCase):
|
||||
skip = "No Jinja installed" if not load_jinja2_templates else None
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
import json
|
||||
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@@ -23,7 +24,7 @@ from tests import unittest
|
||||
class IdentityTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
169
tests/rest/client/v1/test_directory.py
Normal file
169
tests/rest/client/v1/test_directory.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# -*- 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 json
|
||||
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client.v1 import directory, login, room
|
||||
from synapse.types import RoomAlias
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class DirectoryTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets_for_client_rest_resource,
|
||||
directory.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
config = self.default_config()
|
||||
config.require_membership_for_aliases = True
|
||||
|
||||
self.hs = self.setup_test_homeserver(config=config)
|
||||
|
||||
return self.hs
|
||||
|
||||
def prepare(self, reactor, clock, homeserver):
|
||||
self.room_owner = self.register_user("room_owner", "test")
|
||||
self.room_owner_tok = self.login("room_owner", "test")
|
||||
|
||||
self.room_id = self.helper.create_room_as(
|
||||
self.room_owner, tok=self.room_owner_tok,
|
||||
)
|
||||
|
||||
self.user = self.register_user("user", "test")
|
||||
self.user_tok = self.login("user", "test")
|
||||
|
||||
def test_state_event_not_in_room(self):
|
||||
self.ensure_user_left_room()
|
||||
self.set_alias_via_state_event(403)
|
||||
|
||||
def test_directory_endpoint_not_in_room(self):
|
||||
self.ensure_user_left_room()
|
||||
self.set_alias_via_directory(403)
|
||||
|
||||
def test_state_event_in_room_too_long(self):
|
||||
self.ensure_user_joined_room()
|
||||
self.set_alias_via_state_event(400, alias_length=256)
|
||||
|
||||
def test_directory_in_room_too_long(self):
|
||||
self.ensure_user_joined_room()
|
||||
self.set_alias_via_directory(400, alias_length=256)
|
||||
|
||||
def test_state_event_in_room(self):
|
||||
self.ensure_user_joined_room()
|
||||
self.set_alias_via_state_event(200)
|
||||
|
||||
def test_directory_in_room(self):
|
||||
self.ensure_user_joined_room()
|
||||
self.set_alias_via_directory(200)
|
||||
|
||||
def test_room_creation_too_long(self):
|
||||
url = "/_matrix/client/r0/createRoom"
|
||||
|
||||
# We use deliberately a localpart under the length threshold so
|
||||
# that we can make sure that the check is done on the whole alias.
|
||||
data = {
|
||||
"room_alias_name": random_string(256 - len(self.hs.hostname)),
|
||||
}
|
||||
request_data = json.dumps(data)
|
||||
request, channel = self.make_request(
|
||||
"POST", url, request_data, access_token=self.user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
|
||||
def test_room_creation(self):
|
||||
url = "/_matrix/client/r0/createRoom"
|
||||
|
||||
# Check with an alias of allowed length. There should already be
|
||||
# a test that ensures it works in test_register.py, but let's be
|
||||
# as cautious as possible here.
|
||||
data = {
|
||||
"room_alias_name": random_string(5),
|
||||
}
|
||||
request_data = json.dumps(data)
|
||||
request, channel = self.make_request(
|
||||
"POST", url, request_data, access_token=self.user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
|
||||
def set_alias_via_state_event(self, expected_code, alias_length=5):
|
||||
url = ("/_matrix/client/r0/rooms/%s/state/m.room.aliases/%s"
|
||||
% (self.room_id, self.hs.hostname))
|
||||
|
||||
data = {
|
||||
"aliases": [
|
||||
self.random_alias(alias_length),
|
||||
],
|
||||
}
|
||||
request_data = json.dumps(data)
|
||||
|
||||
request, channel = self.make_request(
|
||||
"PUT", url, request_data, access_token=self.user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
def set_alias_via_directory(self, expected_code, alias_length=5):
|
||||
url = "/_matrix/client/r0/directory/room/%s" % self.random_alias(alias_length)
|
||||
data = {
|
||||
"room_id": self.room_id,
|
||||
}
|
||||
request_data = json.dumps(data)
|
||||
|
||||
request, channel = self.make_request(
|
||||
"PUT", url, request_data, access_token=self.user_tok,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
def random_alias(self, length):
|
||||
return RoomAlias(
|
||||
random_string(length),
|
||||
self.hs.hostname,
|
||||
).to_string()
|
||||
|
||||
def ensure_user_left_room(self):
|
||||
self.ensure_membership("leave")
|
||||
|
||||
def ensure_user_joined_room(self):
|
||||
self.ensure_membership("join")
|
||||
|
||||
def ensure_membership(self, membership):
|
||||
try:
|
||||
if membership == "leave":
|
||||
self.helper.leave(
|
||||
room=self.room_id,
|
||||
user=self.user,
|
||||
tok=self.user_tok,
|
||||
)
|
||||
if membership == "join":
|
||||
self.helper.join(
|
||||
room=self.room_id,
|
||||
user=self.user,
|
||||
tok=self.user_tok,
|
||||
)
|
||||
except AssertionError:
|
||||
# We don't care whether the leave request didn't return a 200 (e.g.
|
||||
# if the user isn't already in the room), because we only want to
|
||||
# make sure the user isn't in the room.
|
||||
pass
|
||||
@@ -17,7 +17,8 @@
|
||||
|
||||
from mock import Mock, NonCallableMock
|
||||
|
||||
from synapse.rest.client.v1 import admin, events, login, room
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import events, login, room
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@@ -28,7 +29,7 @@ class EventStreamPermissionsTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
events.register_servlets,
|
||||
room.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
from synapse.rest.client.v1 import admin, login
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@@ -10,7 +11,7 @@ LOGIN_URL = b"/_matrix/client/r0/login"
|
||||
class LoginRestServletTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ from twisted.internet import defer
|
||||
|
||||
import synapse.types
|
||||
from synapse.api.errors import AuthError, SynapseError
|
||||
from synapse.rest.client.v1 import profile
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client.v1 import login, profile, room
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@@ -42,6 +43,7 @@ class ProfileTestCase(unittest.TestCase):
|
||||
"set_displayname",
|
||||
"get_avatar_url",
|
||||
"set_avatar_url",
|
||||
"check_profile_query_allowed",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -155,3 +157,92 @@ class ProfileTestCase(unittest.TestCase):
|
||||
self.assertEquals(mocked_set.call_args[0][0].localpart, "1234ABCD")
|
||||
self.assertEquals(mocked_set.call_args[0][1].user.localpart, "1234ABCD")
|
||||
self.assertEquals(mocked_set.call_args[0][2], "http://my.server/pic.gif")
|
||||
|
||||
|
||||
class ProfilesRestrictedTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
profile.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
|
||||
config = self.default_config()
|
||||
config.require_auth_for_profile_requests = True
|
||||
self.hs = self.setup_test_homeserver(config=config)
|
||||
|
||||
return self.hs
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
# User owning the requested profile.
|
||||
self.owner = self.register_user("owner", "pass")
|
||||
self.owner_tok = self.login("owner", "pass")
|
||||
self.profile_url = "/profile/%s" % (self.owner)
|
||||
|
||||
# User requesting the profile.
|
||||
self.requester = self.register_user("requester", "pass")
|
||||
self.requester_tok = self.login("requester", "pass")
|
||||
|
||||
self.room_id = self.helper.create_room_as(self.owner, tok=self.owner_tok)
|
||||
|
||||
def test_no_auth(self):
|
||||
self.try_fetch_profile(401)
|
||||
|
||||
def test_not_in_shared_room(self):
|
||||
self.ensure_requester_left_room()
|
||||
|
||||
self.try_fetch_profile(403, access_token=self.requester_tok)
|
||||
|
||||
def test_in_shared_room(self):
|
||||
self.ensure_requester_left_room()
|
||||
|
||||
self.helper.join(
|
||||
room=self.room_id,
|
||||
user=self.requester,
|
||||
tok=self.requester_tok,
|
||||
)
|
||||
|
||||
self.try_fetch_profile(200, self.requester_tok)
|
||||
|
||||
def try_fetch_profile(self, expected_code, access_token=None):
|
||||
self.request_profile(
|
||||
expected_code,
|
||||
access_token=access_token
|
||||
)
|
||||
|
||||
self.request_profile(
|
||||
expected_code,
|
||||
url_suffix="/displayname",
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
self.request_profile(
|
||||
expected_code,
|
||||
url_suffix="/avatar_url",
|
||||
access_token=access_token,
|
||||
)
|
||||
|
||||
def request_profile(self, expected_code, url_suffix="", access_token=None):
|
||||
request, channel = self.make_request(
|
||||
"GET",
|
||||
self.profile_url + url_suffix,
|
||||
access_token=access_token,
|
||||
)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, expected_code, channel.result)
|
||||
|
||||
def ensure_requester_left_room(self):
|
||||
try:
|
||||
self.helper.leave(
|
||||
room=self.room_id,
|
||||
user=self.requester,
|
||||
tok=self.requester_tok,
|
||||
)
|
||||
except AssertionError:
|
||||
# We don't care whether the leave request didn't return a 200 (e.g.
|
||||
# if the user isn't already in the room), because we only want to
|
||||
# make sure the user isn't in the room.
|
||||
pass
|
||||
|
||||
@@ -22,8 +22,9 @@ from six.moves.urllib import parse as urlparse
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
from synapse.rest.client.v1 import login, room
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@@ -803,7 +804,7 @@ class RoomMessageListTestCase(RoomBase):
|
||||
|
||||
class RoomSearchTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
@@ -903,3 +904,35 @@ class RoomSearchTestCase(unittest.HomeserverTestCase):
|
||||
self.assertEqual(
|
||||
context["profile_info"][self.other_user_id]["displayname"], "otheruser"
|
||||
)
|
||||
|
||||
|
||||
class PublicRoomsRestrictedTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
|
||||
self.url = b"/_matrix/client/r0/publicRooms"
|
||||
|
||||
config = self.default_config()
|
||||
config.restrict_public_rooms_to_local_users = True
|
||||
self.hs = self.setup_test_homeserver(config=config)
|
||||
|
||||
return self.hs
|
||||
|
||||
def test_restricted_no_auth(self):
|
||||
request, channel = self.make_request("GET", self.url)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 401, channel.result)
|
||||
|
||||
def test_restricted_auth(self):
|
||||
self.register_user("user", "pass")
|
||||
tok = self.login("user", "pass")
|
||||
|
||||
request, channel = self.make_request("GET", self.url, access_token=tok)
|
||||
self.render(request)
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
from twisted.internet.defer import succeed
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.rest.client.v1 import admin
|
||||
from synapse.rest.client.v2_alpha import auth, register
|
||||
|
||||
from tests import unittest
|
||||
@@ -27,7 +27,7 @@ class FallbackAuthTests(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
auth.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
register.register_servlets,
|
||||
]
|
||||
hijack_auth = False
|
||||
|
||||
@@ -12,9 +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.
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.room_versions import DEFAULT_ROOM_VERSION, KNOWN_ROOM_VERSIONS
|
||||
from synapse.rest.client.v1 import admin, login
|
||||
from synapse.rest.client.v1 import login
|
||||
from synapse.rest.client.v2_alpha import capabilities
|
||||
|
||||
from tests import unittest
|
||||
@@ -23,7 +23,7 @@ from tests import unittest
|
||||
class CapabilitiesTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
capabilities.register_servlets,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
@@ -4,10 +4,11 @@ import os
|
||||
|
||||
import pkg_resources
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.rest.client.v1 import admin, login
|
||||
from synapse.rest.client.v1 import login
|
||||
from synapse.rest.client.v2_alpha import account_validity, register, sync
|
||||
|
||||
from tests import unittest
|
||||
@@ -198,7 +199,7 @@ class AccountValidityTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
register.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
sync.register_servlets,
|
||||
account_validity.register_servlets,
|
||||
@@ -307,7 +308,7 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
|
||||
skip = "No Jinja installed" if not load_jinja2_templates else None
|
||||
servlets = [
|
||||
register.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
sync.register_servlets,
|
||||
account_validity.register_servlets,
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
|
||||
from mock import Mock
|
||||
|
||||
from synapse.rest.client.v1 import admin, login, room
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.rest.client.v2_alpha import sync
|
||||
|
||||
from tests import unittest
|
||||
@@ -72,7 +73,7 @@ class FilterTestCase(unittest.HomeserverTestCase):
|
||||
class SyncTypingTests(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
sync.register_servlets,
|
||||
|
||||
@@ -12,8 +12,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 synapse.rest.client.v1 import admin, login, room
|
||||
import synapse.rest.admin
|
||||
from synapse.rest.client.v1 import login, room
|
||||
from synapse.rest.client.v2_alpha import sync
|
||||
|
||||
from tests import unittest
|
||||
@@ -23,7 +23,7 @@ class ConsentNoticesTests(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
sync.register_servlets,
|
||||
admin.register_servlets,
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
@@ -18,8 +18,9 @@ from mock import Mock
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import synapse.rest.admin
|
||||
from synapse.http.site import XForwardedForRequest
|
||||
from synapse.rest.client.v1 import admin, login
|
||||
from synapse.rest.client.v1 import login
|
||||
|
||||
from tests import unittest
|
||||
|
||||
@@ -205,7 +206,10 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
class ClientIpAuthTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
servlets = [admin.register_servlets, login.register_servlets]
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
hs = self.setup_test_homeserver()
|
||||
|
||||
@@ -181,10 +181,7 @@ class HomeserverTestCase(TestCase):
|
||||
raise Exception("A homeserver wasn't returned, but %r" % (self.hs,))
|
||||
|
||||
# Register the resources
|
||||
self.resource = JsonResource(self.hs)
|
||||
|
||||
for servlet in self.servlets:
|
||||
servlet(self.hs, self.resource)
|
||||
self.resource = self.create_test_json_resource()
|
||||
|
||||
from tests.rest.client.v1.utils import RestHelper
|
||||
|
||||
@@ -230,6 +227,23 @@ class HomeserverTestCase(TestCase):
|
||||
hs = self.setup_test_homeserver()
|
||||
return hs
|
||||
|
||||
def create_test_json_resource(self):
|
||||
"""
|
||||
Create a test JsonResource, with the relevant servlets registerd to it
|
||||
|
||||
The default implementation calls each function in `servlets` to do the
|
||||
registration.
|
||||
|
||||
Returns:
|
||||
JsonResource:
|
||||
"""
|
||||
resource = JsonResource(self.hs)
|
||||
|
||||
for servlet in self.servlets:
|
||||
servlet(self.hs, resource)
|
||||
|
||||
return resource
|
||||
|
||||
def default_config(self, name="test"):
|
||||
"""
|
||||
Get a default HomeServer config object.
|
||||
|
||||
Reference in New Issue
Block a user