diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py
index a4dc9db03e..8033fa2e52 100644
--- a/synapse/config/emailconfig.py
+++ b/synapse/config/emailconfig.py
@@ -52,6 +52,7 @@ DEFAULT_SUBJECTS = {
"invite_from_person_to_space": "[%(app)s] %(person)s has invited you to join the %(space)s space on %(app)s...",
"password_reset": "[%(server_name)s] Password reset",
"email_validation": "[%(server_name)s] Validate your email",
+ "email_already_in_use": "[%(server_name)s] Email already in use",
}
LEGACY_TEMPLATE_DIR_WARNING = """
@@ -76,6 +77,7 @@ class EmailSubjectConfig:
invite_from_person_to_space: str
password_reset: str
email_validation: str
+ email_already_in_use: str
class EmailConfig(Config):
@@ -180,6 +182,12 @@ class EmailConfig(Config):
registration_template_text = email_config.get(
"registration_template_text", "registration.txt"
)
+ already_in_use_template_html = email_config.get(
+ "already_in_use_template_html", "already_in_use.html"
+ )
+ already_in_use_template_text = email_config.get(
+ "already_in_use_template_html", "already_in_use.txt"
+ )
add_threepid_template_html = email_config.get(
"add_threepid_template_html", "add_threepid.html"
)
@@ -215,6 +223,8 @@ class EmailConfig(Config):
self.email_password_reset_template_text,
self.email_registration_template_html,
self.email_registration_template_text,
+ self.email_already_in_use_template_html,
+ self.email_already_in_use_template_text,
self.email_add_threepid_template_html,
self.email_add_threepid_template_text,
self.email_password_reset_template_confirmation_html,
@@ -230,6 +240,8 @@ class EmailConfig(Config):
password_reset_template_text,
registration_template_html,
registration_template_text,
+ already_in_use_template_html,
+ already_in_use_template_text,
add_threepid_template_html,
add_threepid_template_text,
"password_reset_confirmation.html",
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index fcc78d2d81..353ae23f91 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -411,3 +411,14 @@ class ExperimentalConfig(Config):
self.msc4069_profile_inhibit_propagation = experimental.get(
"msc4069_profile_inhibit_propagation", False
)
+
+ # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code
+ self.msc4108_delegation_endpoint: Optional[str] = experimental.get(
+ "msc4108_delegation_endpoint", None
+ )
+
+ if self.msc4108_delegation_endpoint is not None and not self.msc3861.enabled:
+ raise ConfigError(
+ "MSC4108 requires MSC3861 to be enabled",
+ ("experimental", "msc4108_delegation_endpoint"),
+ )
diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py
index 63e00f102e..1ece54ccfc 100644
--- a/synapse/handlers/e2e_keys.py
+++ b/synapse/handlers/e2e_keys.py
@@ -1476,6 +1476,42 @@ class E2eKeysHandler:
else:
return exists, self.clock.time_msec() < ts_replacable_without_uia_before
+ async def has_different_keys(self, user_id: str, body: JsonDict) -> bool:
+ """
+ Check if a key provided in `body` differs from the same key stored in the DB. Returns
+ true on the first difference. If a key exists in `body` but does not exist in the DB,
+ returns True. If `body` has no keys, this always returns False.
+ Note by 'key' we mean Matrix key rather than JSON key.
+
+ The purpose of this function is to detect whether or not we need to apply UIA checks.
+ We must apply UIA checks if any key in the database is being overwritten. If a key is
+ being inserted for the first time, or if the key exactly matches what is in the database,
+ then no UIA check needs to be performed.
+
+ Args:
+ user_id: The user who sent the `body`.
+ body: The JSON request body from POST /keys/device_signing/upload
+ Returns:
+ True if any key in `body` has a different value in the database.
+ """
+ # Ensure that each key provided in the request body exactly matches the one we have stored.
+ # The first time we see the DB having a different key to the matching request key, bail.
+ # Note: we do not care if the DB has a key which the request does not specify, as we only
+ # care about *replacements* or *insertions* (i.e UPSERT)
+ req_body_key_to_db_key = {
+ "master_key": "master",
+ "self_signing_key": "self_signing",
+ "user_signing_key": "user_signing",
+ }
+ for req_body_key, db_key in req_body_key_to_db_key.items():
+ if req_body_key in body:
+ existing_key = await self.store.get_e2e_cross_signing_key(
+ user_id, db_key
+ )
+ if existing_key != body[req_body_key]:
+ return True
+ return False
+
def _check_cross_signing_key(
key: JsonDict, user_id: str, key_type: str, signing_key: Optional[VerifyKey] = None
diff --git a/synapse/http/proxy.py b/synapse/http/proxy.py
index 6cbbd5741b..5b5ded757b 100644
--- a/synapse/http/proxy.py
+++ b/synapse/http/proxy.py
@@ -262,7 +262,8 @@ class _ProxyResponseBody(protocol.Protocol):
self._request.finish()
else:
# Abort the underlying request since our remote request also failed.
- self._request.transport.abortConnection()
+ if self._request.channel:
+ self._request.channel.forceAbortClient()
class ProxySite(Site):
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 632284712c..45b2cbffcd 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -153,9 +153,9 @@ def return_json_error(
# Only respond with an error response if we haven't already started writing,
# otherwise lets just kill the connection
if request.startedWriting:
- if request.transport:
+ if request.channel:
try:
- request.transport.abortConnection()
+ request.channel.forceAbortClient()
except Exception:
# abortConnection throws if the connection is already closed
pass
@@ -909,7 +909,18 @@ def set_cors_headers(request: "SynapseRequest") -> None:
request.setHeader(
b"Access-Control-Allow-Methods", b"GET, HEAD, POST, PUT, DELETE, OPTIONS"
)
- if request.experimental_cors_msc3886:
+ if request.path is not None and request.path.startswith(
+ b"/_matrix/client/unstable/org.matrix.msc4108/rendezvous"
+ ):
+ request.setHeader(
+ b"Access-Control-Allow-Headers",
+ b"Content-Type, If-Match, If-None-Match",
+ )
+ request.setHeader(
+ b"Access-Control-Expose-Headers",
+ b"Synapse-Trace-Id, Server, ETag",
+ )
+ elif request.experimental_cors_msc3886:
request.setHeader(
b"Access-Control-Allow-Headers",
b"X-Requested-With, Content-Type, Authorization, Date, If-Match, If-None-Match",
diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py
index b73d06f1d3..ab12951da8 100644
--- a/synapse/http/servlet.py
+++ b/synapse/http/servlet.py
@@ -19,9 +19,11 @@
#
#
-""" This module contains base REST classes for constructing REST servlets. """
+"""This module contains base REST classes for constructing REST servlets."""
+
import enum
import logging
+import urllib.parse as urlparse
from http import HTTPStatus
from typing import (
TYPE_CHECKING,
@@ -65,17 +67,49 @@ def parse_integer(request: Request, name: str, default: int) -> int: ...
@overload
-def parse_integer(request: Request, name: str, *, required: Literal[True]) -> int: ...
+def parse_integer(
+ request: Request, name: str, *, default: int, negative: bool
+) -> int: ...
+
+
+@overload
+def parse_integer(
+ request: Request, name: str, *, default: int, negative: bool = False
+) -> int: ...
+
+
+@overload
+def parse_integer(
+ request: Request, name: str, *, required: Literal[True], negative: bool = False
+) -> int: ...
@overload
def parse_integer(
- request: Request, name: str, default: Optional[int] = None, required: bool = False
+ request: Request, name: str, *, default: Literal[None], negative: bool = False
+) -> None: ...
+
+
+@overload
+def parse_integer(request: Request, name: str, *, negative: bool) -> Optional[int]: ...
+
+
+@overload
+def parse_integer(
+ request: Request,
+ name: str,
+ default: Optional[int] = None,
+ required: bool = False,
+ negative: bool = False,
) -> Optional[int]: ...
def parse_integer(
- request: Request, name: str, default: Optional[int] = None, required: bool = False
+ request: Request,
+ name: str,
+ default: Optional[int] = None,
+ required: bool = False,
+ negative: bool = False,
) -> Optional[int]:
"""Parse an integer parameter from the request string
@@ -85,16 +119,17 @@ def parse_integer(
default: value to use if the parameter is absent, defaults to None.
required: whether to raise a 400 SynapseError if the parameter is absent,
defaults to False.
-
+ negative: whether to allow negative integers, defaults to True.
Returns:
An int value or the default.
Raises:
- SynapseError: if the parameter is absent and required, or if the
- parameter is present and not an integer.
+ SynapseError: if the parameter is absent and required, if the
+ parameter is present and not an integer, or if the
+ parameter is illegitimate negative.
"""
args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore
- return parse_integer_from_args(args, name, default, required)
+ return parse_integer_from_args(args, name, default, required, negative)
@overload
@@ -120,6 +155,7 @@ def parse_integer_from_args(
name: str,
default: Optional[int] = None,
required: bool = False,
+ negative: bool = False,
) -> Optional[int]: ...
@@ -128,6 +164,7 @@ def parse_integer_from_args(
name: str,
default: Optional[int] = None,
required: bool = False,
+ negative: bool = True,
) -> Optional[int]:
"""Parse an integer parameter from the request string
@@ -137,33 +174,37 @@ def parse_integer_from_args(
default: value to use if the parameter is absent, defaults to None.
required: whether to raise a 400 SynapseError if the parameter is absent,
defaults to False.
+ negative: whether to allow negative integers, defaults to True.
Returns:
An int value or the default.
Raises:
- SynapseError: if the parameter is absent and required, or if the
- parameter is present and not an integer.
+ SynapseError: if the parameter is absent and required, if the
+ parameter is present and not an integer, or if the
+ parameter is illegitimate negative.
"""
name_bytes = name.encode("ascii")
- if name_bytes in args:
- try:
- return int(args[name_bytes][0])
- except Exception:
- message = "Query parameter %r must be an integer" % (name,)
- raise SynapseError(
- HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM
- )
- else:
- if required:
- message = "Missing integer query parameter %r" % (name,)
- raise SynapseError(
- HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM
- )
- else:
+ if name_bytes not in args:
+ if not required:
return default
+ message = f"Missing required integer query parameter {name}"
+ raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM)
+
+ try:
+ integer = int(args[name_bytes][0])
+ except Exception:
+ message = f"Query parameter {name} must be an integer"
+ raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM)
+
+ if not negative and integer < 0:
+ message = f"Query parameter {name} must be a positive integer."
+ raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.INVALID_PARAM)
+
+ return integer
+
@overload
def parse_boolean(request: Request, name: str, default: bool) -> bool: ...
@@ -410,6 +451,87 @@ def parse_string(
)
+def parse_json(
+ request: Request,
+ name: str,
+ default: Optional[dict] = None,
+ required: bool = False,
+ encoding: str = "ascii",
+) -> Optional[JsonDict]:
+ """
+ Parse a JSON parameter from the request query string.
+
+ Args:
+ request: the twisted HTTP request.
+ name: the name of the query parameter.
+ default: value to use if the parameter is absent,
+ defaults to None.
+ required: whether to raise a 400 SynapseError if the
+ parameter is absent, defaults to False.
+ encoding: The encoding to decode the string content with.
+
+ Returns:
+ A JSON value, or `default` if the named query parameter was not found
+ and `required` was False.
+
+ Raises:
+ SynapseError if the parameter is absent and required, or if the
+ parameter is present and not a JSON object.
+ """
+ args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore
+ return parse_json_from_args(
+ args,
+ name,
+ default,
+ required=required,
+ encoding=encoding,
+ )
+
+
+def parse_json_from_args(
+ args: Mapping[bytes, Sequence[bytes]],
+ name: str,
+ default: Optional[dict] = None,
+ required: bool = False,
+ encoding: str = "ascii",
+) -> Optional[JsonDict]:
+ """
+ Parse a JSON parameter from the request query string.
+
+ Args:
+ args: a mapping of request args as bytes to a list of bytes (e.g. request.args).
+ name: the name of the query parameter.
+ default: value to use if the parameter is absent,
+ defaults to None.
+ required: whether to raise a 400 SynapseError if the
+ parameter is absent, defaults to False.
+ encoding: the encoding to decode the string content with.
+
+ A JSON value, or `default` if the named query parameter was not found
+ and `required` was False.
+
+ Raises:
+ SynapseError if the parameter is absent and required, or if the
+ parameter is present and not a JSON object.
+ """
+ name_bytes = name.encode("ascii")
+
+ if name_bytes not in args:
+ if not required:
+ return default
+
+ message = f"Missing required integer query parameter {name}"
+ raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM)
+
+ json_str = parse_string_from_args(args, name, required=True, encoding=encoding)
+
+ try:
+ return json_decoder.decode(urlparse.unquote(json_str))
+ except Exception:
+ message = f"Query parameter {name} must be a valid JSON object"
+ raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.NOT_JSON)
+
+
EnumT = TypeVar("EnumT", bound=enum.Enum)
diff --git a/synapse/http/site.py b/synapse/http/site.py
index 682b28e4c6..a5b5780679 100644
--- a/synapse/http/site.py
+++ b/synapse/http/site.py
@@ -150,7 +150,8 @@ class SynapseRequest(Request):
self.get_method(),
self.get_redacted_uri(),
)
- self.transport.abortConnection()
+ if self.channel:
+ self.channel.forceAbortClient()
return
super().handleContentChunk(data)
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py
index f1ffc8115f..7c15eb7440 100644
--- a/synapse/push/mailer.py
+++ b/synapse/push/mailer.py
@@ -205,6 +205,22 @@ class Mailer:
template_vars,
)
+ emails_sent_counter.labels("already_in_use")
+
+ async def send_already_in_use_mail(self, email_address: str) -> None:
+ """Send an email if the address is already bound to an user account
+
+ Args:
+ email_address: Email address we're sending to the "already in use" mail
+ """
+
+ await self.send_email(
+ email_address,
+ self.email_subjects.email_already_in_use
+ % {"server_name": self.hs.config.server.server_name, "app": self.app_name},
+ {},
+ )
+
emails_sent_counter.labels("add_threepid")
async def send_add_threepid_mail(
diff --git a/synapse/res/templates/already_in_use.html b/synapse/res/templates/already_in_use.html
new file mode 100644
index 0000000000..4c4c3c36a7
--- /dev/null
+++ b/synapse/res/templates/already_in_use.html
@@ -0,0 +1,12 @@
+{% extends "_base.html" %}
+{% block title %}Email already in use{% endblock %}
+
+{% block body %}
+<p>You have asked us to register this email with a new Matrix account, but this email is already registered with an existing account.</p>
+
+<p>Please reset your password if needed.</p>
+
+<p>If this was not you, you can safely disregard this email.</p>
+
+<p>Thank you.</p>
+{% endblock %}
diff --git a/synapse/res/templates/already_in_use.txt b/synapse/res/templates/already_in_use.txt
new file mode 100644
index 0000000000..c60401a940
--- /dev/null
+++ b/synapse/res/templates/already_in_use.txt
@@ -0,0 +1,10 @@
+Hello there,
+
+You have asked us to register this email with a new Matrix account,
+but this email is already registered with an existing account.
+
+Please reset your password if needed.
+
+If this was not you, you can safely disregard this email.
+
+Thank you.
diff --git a/synapse/rest/admin/federation.py b/synapse/rest/admin/federation.py
index 045153e0cb..14ab4644cb 100644
--- a/synapse/rest/admin/federation.py
+++ b/synapse/rest/admin/federation.py
@@ -23,7 +23,7 @@ from http import HTTPStatus
from typing import TYPE_CHECKING, Tuple
from synapse.api.constants import Direction
-from synapse.api.errors import Codes, NotFoundError, SynapseError
+from synapse.api.errors import NotFoundError, SynapseError
from synapse.federation.transport.server import Authenticator
from synapse.http.servlet import RestServlet, parse_enum, parse_integer, parse_string
from synapse.http.site import SynapseRequest
@@ -61,22 +61,8 @@ class ListDestinationsRestServlet(RestServlet):
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self._auth, request)
- start = parse_integer(request, "from", default=0)
- limit = parse_integer(request, "limit", default=100)
-
- if start < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- if limit < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter limit must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
+ start = parse_integer(request, "from", default=0, negative=False)
+ limit = parse_integer(request, "limit", default=100, negative=False)
destination = parse_string(request, "destination")
@@ -195,22 +181,8 @@ class DestinationMembershipRestServlet(RestServlet):
if not await self._store.is_destination_known(destination):
raise NotFoundError("Unknown destination")
- start = parse_integer(request, "from", default=0)
- limit = parse_integer(request, "limit", default=100)
-
- if start < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- if limit < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter limit must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
+ start = parse_integer(request, "from", default=0, negative=False)
+ limit = parse_integer(request, "limit", default=100, negative=False)
direction = parse_enum(request, "dir", Direction, default=Direction.FORWARDS)
diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py
index 27f0808658..a05b7252ec 100644
--- a/synapse/rest/admin/media.py
+++ b/synapse/rest/admin/media.py
@@ -311,29 +311,17 @@ class DeleteMediaByDateSize(RestServlet):
) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)
- before_ts = parse_integer(request, "before_ts", required=True)
- size_gt = parse_integer(request, "size_gt", default=0)
+ before_ts = parse_integer(request, "before_ts", required=True, negative=False)
+ size_gt = parse_integer(request, "size_gt", default=0, negative=False)
keep_profiles = parse_boolean(request, "keep_profiles", default=True)
- if before_ts < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter before_ts must be a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
- elif before_ts < 30000000000: # Dec 1970 in milliseconds, Aug 2920 in seconds
+ if before_ts < 30000000000: # Dec 1970 in milliseconds, Aug 2920 in seconds
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"Query parameter before_ts you provided is from the year 1970. "
+ "Double check that you are providing a timestamp in milliseconds.",
errcode=Codes.INVALID_PARAM,
)
- if size_gt < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter size_gt must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
# This check is useless, we keep it for the legacy endpoint only.
if server_name is not None and self.server_name != server_name:
@@ -389,22 +377,8 @@ class UserMediaRestServlet(RestServlet):
if user is None:
raise NotFoundError("Unknown user")
- start = parse_integer(request, "from", default=0)
- limit = parse_integer(request, "limit", default=100)
-
- if start < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- if limit < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter limit must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
+ start = parse_integer(request, "from", default=0, negative=False)
+ limit = parse_integer(request, "limit", default=100, negative=False)
# If neither `order_by` nor `dir` is set, set the default order
# to newest media is on top for backward compatibility.
@@ -447,22 +421,8 @@ class UserMediaRestServlet(RestServlet):
if user is None:
raise NotFoundError("Unknown user")
- start = parse_integer(request, "from", default=0)
- limit = parse_integer(request, "limit", default=100)
-
- if start < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- if limit < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter limit must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
+ start = parse_integer(request, "from", default=0, negative=False)
+ limit = parse_integer(request, "limit", default=100, negative=False)
# If neither `order_by` nor `dir` is set, set the default order
# to newest media is on top for backward compatibility.
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index 4252f98a6c..0d86a4e15f 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -21,7 +21,6 @@
import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, List, Optional, Tuple, cast
-from urllib import parse as urlparse
import attr
@@ -38,6 +37,7 @@ from synapse.http.servlet import (
assert_params_in_dict,
parse_enum,
parse_integer,
+ parse_json,
parse_json_object_from_request,
parse_string,
)
@@ -51,7 +51,6 @@ from synapse.storage.databases.main.room import RoomSortOrder
from synapse.streams.config import PaginationConfig
from synapse.types import JsonDict, RoomID, ScheduledTask, UserID, create_requester
from synapse.types.state import StateFilter
-from synapse.util import json_decoder
if TYPE_CHECKING:
from synapse.api.auth import Auth
@@ -776,14 +775,8 @@ class RoomEventContextServlet(RestServlet):
limit = parse_integer(request, "limit", default=10)
# picking the API shape for symmetry with /messages
- filter_str = parse_string(request, "filter", encoding="utf-8")
- if filter_str:
- filter_json = urlparse.unquote(filter_str)
- event_filter: Optional[Filter] = Filter(
- self._hs, json_decoder.decode(filter_json)
- )
- else:
- event_filter = None
+ filter_json = parse_json(request, "filter", encoding="utf-8")
+ event_filter = Filter(self._hs, filter_json) if filter_json else None
event_context = await self.room_context_handler.get_event_context(
requester,
@@ -914,21 +907,16 @@ class RoomMessagesRestServlet(RestServlet):
)
# Twisted will have processed the args by now.
assert request.args is not None
+
+ filter_json = parse_json(request, "filter", encoding="utf-8")
+ event_filter = Filter(self._hs, filter_json) if filter_json else None
+
as_client_event = b"raw" not in request.args
- filter_str = parse_string(request, "filter", encoding="utf-8")
- if filter_str:
- filter_json = urlparse.unquote(filter_str)
- event_filter: Optional[Filter] = Filter(
- self._hs, json_decoder.decode(filter_json)
- )
- if (
- event_filter
- and event_filter.filter_json.get("event_format", "client")
- == "federation"
- ):
- as_client_event = False
- else:
- event_filter = None
+ if (
+ event_filter
+ and event_filter.filter_json.get("event_format", "client") == "federation"
+ ):
+ as_client_event = False
msgs = await self._pagination_handler.get_messages(
room_id=room_id,
diff --git a/synapse/rest/admin/statistics.py b/synapse/rest/admin/statistics.py
index 832f20402e..dc27a41dd9 100644
--- a/synapse/rest/admin/statistics.py
+++ b/synapse/rest/admin/statistics.py
@@ -63,38 +63,12 @@ class UserMediaStatisticsRestServlet(RestServlet):
),
)
- start = parse_integer(request, "from", default=0)
- if start < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- limit = parse_integer(request, "limit", default=100)
- if limit < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter limit must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
+ start = parse_integer(request, "from", default=0, negative=False)
+ limit = parse_integer(request, "limit", default=100, negative=False)
+ from_ts = parse_integer(request, "from_ts", default=0, negative=False)
+ until_ts = parse_integer(request, "until_ts", negative=False)
- from_ts = parse_integer(request, "from_ts", default=0)
- if from_ts < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from_ts must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- until_ts = parse_integer(request, "until_ts")
if until_ts is not None:
- if until_ts < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter until_ts must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
if until_ts <= from_ts:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py
index 4e34e46512..5bf12c4979 100644
--- a/synapse/rest/admin/users.py
+++ b/synapse/rest/admin/users.py
@@ -90,22 +90,8 @@ class UsersRestServletV2(RestServlet):
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)
- start = parse_integer(request, "from", default=0)
- limit = parse_integer(request, "limit", default=100)
-
- if start < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter from must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
-
- if limit < 0:
- raise SynapseError(
- HTTPStatus.BAD_REQUEST,
- "Query parameter limit must be a string representing a positive integer.",
- errcode=Codes.INVALID_PARAM,
- )
+ start = parse_integer(request, "from", default=0, negative=False)
+ limit = parse_integer(request, "limit", default=100, negative=False)
user_id = parse_string(request, "user_id")
name = parse_string(request, "name", encoding="utf-8")
diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py
index b6d9ee074a..86c9515854 100644
--- a/synapse/rest/client/keys.py
+++ b/synapse/rest/client/keys.py
@@ -409,7 +409,18 @@ class SigningKeyUploadServlet(RestServlet):
# But first-time setup is fine
elif self.hs.config.experimental.msc3967_enabled:
- # If we already have a master key then cross signing is set up and we require UIA to reset
+ # MSC3967 allows this endpoint to 200 OK for idempotency. Resending exactly the same
+ # keys should just 200 OK without doing a UIA prompt.
+ keys_are_different = await self.e2e_keys_handler.has_different_keys(
+ user_id, body
+ )
+ if not keys_are_different:
+ # FIXME: we do not fallthrough to upload_signing_keys_for_user because confusingly
+ # if we do, we 500 as it looks like it tries to INSERT the same key twice, causing a
+ # unique key constraint violation. This sounds like a bug?
+ return 200, {}
+ # the keys are different, is x-signing set up? If no, then the keys don't exist which is
+ # why they are different. If yes, then we need to UIA to change them.
if is_cross_signing_setup:
await self.auth_handler.validate_user_via_ui_auth(
requester,
@@ -420,7 +431,6 @@ class SigningKeyUploadServlet(RestServlet):
can_skip_ui_auth=False,
)
# Otherwise we don't require UIA since we are setting up cross signing for first time
-
else:
# Previous behaviour is to always require UIA but allow it to be skipped
await self.auth_handler.validate_user_via_ui_auth(
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index 634ebed2be..5dddbc69be 100644
--- a/synapse/rest/client/register.py
+++ b/synapse/rest/client/register.py
@@ -86,12 +86,18 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
self.config = hs.config
if self.hs.config.email.can_verify_email:
- self.mailer = Mailer(
+ self.registration_mailer = Mailer(
hs=self.hs,
app_name=self.config.email.email_app_name,
template_html=self.config.email.email_registration_template_html,
template_text=self.config.email.email_registration_template_text,
)
+ self.already_in_use_mailer = Mailer(
+ hs=self.hs,
+ app_name=self.config.email.email_app_name,
+ template_html=self.config.email.email_already_in_use_template_html,
+ template_text=self.config.email.email_already_in_use_template_text,
+ )
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
if not self.hs.config.email.can_verify_email:
@@ -139,8 +145,10 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
if self.hs.config.server.request_token_inhibit_3pid_errors:
# Make the client think the operation succeeded. See the rationale in the
# comments for request_token_inhibit_3pid_errors.
+ # Still send an email to warn the user that an account already exists.
# Also wait for some random amount of time between 100ms and 1s to make it
# look like we did something.
+ await self.already_in_use_mailer.send_already_in_use_mail(email)
await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
return 200, {"sid": random_string(16)}
@@ -151,7 +159,7 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
email,
client_secret,
send_attempt,
- self.mailer.send_registration_mail,
+ self.registration_mailer.send_registration_mail,
next_link,
)
diff --git a/synapse/rest/client/rendezvous.py b/synapse/rest/client/rendezvous.py
index dee7c37ec5..ed06a29987 100644
--- a/synapse/rest/client/rendezvous.py
+++ b/synapse/rest/client/rendezvous.py
@@ -2,7 +2,7 @@
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright 2022 The Matrix.org Foundation C.I.C.
-# Copyright (C) 2023 New Vector, Ltd
+# Copyright (C) 2023-2024 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
-class RendezvousServlet(RestServlet):
+class MSC3886RendezvousServlet(RestServlet):
"""
This is a placeholder implementation of [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886)
simple client rendezvous capability that is used by the "Sign in with QR" functionality.
@@ -76,6 +76,30 @@ class RendezvousServlet(RestServlet):
# PUT, GET and DELETE are not implemented as they should be fulfilled by the redirect target.
+class MSC4108DelegationRendezvousServlet(RestServlet):
+ PATTERNS = client_patterns(
+ "/org.matrix.msc4108/rendezvous$", releases=[], v1=False, unstable=True
+ )
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+ redirection_target: Optional[str] = (
+ hs.config.experimental.msc4108_delegation_endpoint
+ )
+ assert (
+ redirection_target is not None
+ ), "Servlet is only registered if there is a delegation target"
+ self.endpoint = redirection_target.encode("utf-8")
+
+ async def on_POST(self, request: SynapseRequest) -> None:
+ respond_with_redirect(
+ request, self.endpoint, statusCode=TEMPORARY_REDIRECT, cors=True
+ )
+
+
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.experimental.msc3886_endpoint is not None:
- RendezvousServlet(hs).register(http_server)
+ MSC3886RendezvousServlet(hs).register(http_server)
+
+ if hs.config.experimental.msc4108_delegation_endpoint is not None:
+ MSC4108DelegationRendezvousServlet(hs).register(http_server)
diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py
index 65dedb8b92..e4c7dd1a58 100644
--- a/synapse/rest/client/room.py
+++ b/synapse/rest/client/room.py
@@ -52,6 +52,7 @@ from synapse.http.servlet import (
parse_boolean,
parse_enum,
parse_integer,
+ parse_json,
parse_json_object_from_request,
parse_string,
parse_strings_from_args,
@@ -65,7 +66,6 @@ from synapse.rest.client.transactions import HttpTransactionCache
from synapse.streams.config import PaginationConfig
from synapse.types import JsonDict, Requester, StreamToken, ThirdPartyInstanceID, UserID
from synapse.types.state import StateFilter
-from synapse.util import json_decoder
from synapse.util.cancellation import cancellable
from synapse.util.stringutils import parse_and_validate_server_name, random_string
@@ -499,7 +499,7 @@ class PublicRoomListRestServlet(RestServlet):
if server:
raise e
- limit: Optional[int] = parse_integer(request, "limit", 0)
+ limit: Optional[int] = parse_integer(request, "limit", 0, negative=False)
since_token = parse_string(request, "since")
if limit == 0:
@@ -703,21 +703,16 @@ class RoomMessageListRestServlet(RestServlet):
)
# Twisted will have processed the args by now.
assert request.args is not None
+
+ filter_json = parse_json(request, "filter", encoding="utf-8")
+ event_filter = Filter(self._hs, filter_json) if filter_json else None
+
as_client_event = b"raw" not in request.args
- filter_str = parse_string(request, "filter", encoding="utf-8")
- if filter_str:
- filter_json = urlparse.unquote(filter_str)
- event_filter: Optional[Filter] = Filter(
- self._hs, json_decoder.decode(filter_json)
- )
- if (
- event_filter
- and event_filter.filter_json.get("event_format", "client")
- == "federation"
- ):
- as_client_event = False
- else:
- event_filter = None
+ if (
+ event_filter
+ and event_filter.filter_json.get("event_format", "client") == "federation"
+ ):
+ as_client_event = False
msgs = await self.pagination_handler.get_messages(
room_id=room_id,
@@ -898,14 +893,8 @@ class RoomEventContextServlet(RestServlet):
limit = parse_integer(request, "limit", default=10)
# picking the API shape for symmetry with /messages
- filter_str = parse_string(request, "filter", encoding="utf-8")
- if filter_str:
- filter_json = urlparse.unquote(filter_str)
- event_filter: Optional[Filter] = Filter(
- self._hs, json_decoder.decode(filter_json)
- )
- else:
- event_filter = None
+ filter_json = parse_json(request, "filter", encoding="utf-8")
+ event_filter = Filter(self._hs, filter_json) if filter_json else None
event_context = await self.room_context_handler.get_event_context(
requester, room_id, event_id, limit, event_filter
diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py
index c46d4fe8cf..638d4c45ae 100644
--- a/synapse/rest/client/versions.py
+++ b/synapse/rest/client/versions.py
@@ -140,6 +140,9 @@ class VersionsRestServlet(RestServlet):
"org.matrix.msc4069": self.config.experimental.msc4069_profile_inhibit_propagation,
# Allows clients to handle push for encrypted events.
"org.matrix.msc4028": self.config.experimental.msc4028_push_encrypted_events,
+ # MSC4108: Mechanism to allow OIDC sign in and E2EE set up via QR code
+ "org.matrix.msc4108": self.config.experimental.msc4108_delegation_endpoint
+ is not None,
},
},
)
diff --git a/synapse/rest/media/preview_url_resource.py b/synapse/rest/media/preview_url_resource.py
index 6724986fcc..bfeff2179b 100644
--- a/synapse/rest/media/preview_url_resource.py
+++ b/synapse/rest/media/preview_url_resource.py
@@ -72,9 +72,6 @@ class PreviewUrlResource(RestServlet):
# XXX: if get_user_by_req fails, what should we do in an async render?
requester = await self.auth.get_user_by_req(request)
url = parse_string(request, "url", required=True)
- ts = parse_integer(request, "ts")
- if ts is None:
- ts = self.clock.time_msec()
-
+ ts = parse_integer(request, "ts", default=self.clock.time_msec())
og = await self.url_previewer.preview(url, requester.user, ts)
respond_with_json_bytes(request, 200, og, send_cors=True)
diff --git a/synapse/storage/databases/main/event_push_actions.py b/synapse/storage/databases/main/event_push_actions.py
index 40bf000e9c..bdd0781c48 100644
--- a/synapse/storage/databases/main/event_push_actions.py
+++ b/synapse/storage/databases/main/event_push_actions.py
@@ -385,7 +385,6 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
WITH all_receipts AS (
SELECT room_id, thread_id, MAX(event_stream_ordering) AS max_receipt_stream_ordering
FROM receipts_linearized
- LEFT JOIN events USING (room_id, event_id)
WHERE
{receipt_types_clause}
AND user_id = ?
@@ -621,13 +620,12 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
SELECT notif_count, COALESCE(unread_count, 0), thread_id
FROM event_push_summary
LEFT JOIN (
- SELECT thread_id, MAX(stream_ordering) AS threaded_receipt_stream_ordering
+ SELECT thread_id, MAX(event_stream_ordering) AS threaded_receipt_stream_ordering
FROM receipts_linearized
- LEFT JOIN events USING (room_id, event_id)
WHERE
user_id = ?
AND room_id = ?
- AND stream_ordering > ?
+ AND event_stream_ordering > ?
AND {receipt_types_clause}
GROUP BY thread_id
) AS receipts USING (thread_id)
@@ -659,13 +657,12 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
sql = f"""
SELECT COUNT(*), thread_id FROM event_push_actions
LEFT JOIN (
- SELECT thread_id, MAX(stream_ordering) AS threaded_receipt_stream_ordering
+ SELECT thread_id, MAX(event_stream_ordering) AS threaded_receipt_stream_ordering
FROM receipts_linearized
- LEFT JOIN events USING (room_id, event_id)
WHERE
user_id = ?
AND room_id = ?
- AND stream_ordering > ?
+ AND event_stream_ordering > ?
AND {receipt_types_clause}
GROUP BY thread_id
) AS receipts USING (thread_id)
@@ -738,13 +735,12 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
thread_id
FROM event_push_actions
LEFT JOIN (
- SELECT thread_id, MAX(stream_ordering) AS threaded_receipt_stream_ordering
+ SELECT thread_id, MAX(event_stream_ordering) AS threaded_receipt_stream_ordering
FROM receipts_linearized
- LEFT JOIN events USING (room_id, event_id)
WHERE
user_id = ?
AND room_id = ?
- AND stream_ordering > ?
+ AND event_stream_ordering > ?
AND {receipt_types_clause}
GROUP BY thread_id
) AS receipts USING (thread_id)
@@ -910,9 +906,8 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
# given this function generally gets called with only one room and
# thread ID.
sql = f"""
- SELECT room_id, thread_id, MAX(stream_ordering)
+ SELECT room_id, thread_id, MAX(event_stream_ordering)
FROM receipts_linearized
- INNER JOIN events USING (room_id, event_id)
WHERE {receipt_types_clause}
AND {thread_ids_clause}
AND {room_ids_clause}
@@ -1442,9 +1437,8 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
)
sql = """
- SELECT r.stream_id, r.room_id, r.user_id, r.thread_id, e.stream_ordering
+ SELECT r.stream_id, r.room_id, r.user_id, r.thread_id, r.event_stream_ordering
FROM receipts_linearized AS r
- INNER JOIN events AS e USING (event_id)
WHERE ? < r.stream_id AND r.stream_id <= ? AND user_id LIKE ?
ORDER BY r.stream_id ASC
LIMIT ?
diff --git a/synapse/storage/databases/main/receipts.py b/synapse/storage/databases/main/receipts.py
index d513c42530..13387a3839 100644
--- a/synapse/storage/databases/main/receipts.py
+++ b/synapse/storage/databases/main/receipts.py
@@ -178,14 +178,13 @@ class ReceiptsWorkerStore(SQLBaseStore):
)
sql = f"""
- SELECT event_id, stream_ordering
+ SELECT event_id, event_stream_ordering
FROM receipts_linearized
- INNER JOIN events USING (room_id, event_id)
WHERE {clause}
AND user_id = ?
AND room_id = ?
AND thread_id IS NULL
- ORDER BY stream_ordering DESC
+ ORDER BY event_stream_ordering DESC
LIMIT 1
"""
@@ -735,10 +734,13 @@ class ReceiptsWorkerStore(SQLBaseStore):
thread_clause = "r.thread_id = ?"
thread_args = (thread_id,)
+ # If the receipt doesn't have a stream ordering it is because we
+ # don't have the associated event, and so must be a remote receipt.
+ # Hence it's safe to just allow new receipts to clobber it.
sql = f"""
- SELECT stream_ordering, event_id FROM events
- INNER JOIN receipts_linearized AS r USING (event_id, room_id)
- WHERE r.room_id = ? AND r.receipt_type = ? AND r.user_id = ? AND {thread_clause}
+ SELECT r.event_stream_ordering, r.event_id FROM receipts_linearized AS r
+ WHERE r.room_id = ? AND r.receipt_type = ? AND r.user_id = ?
+ AND r.event_stream_ordering IS NOT NULL AND {thread_clause}
"""
txn.execute(
sql,
|