Implement MSC3848: Introduce errcodes for specific event sending failures (#13343)
Implements MSC3848pull/13439/head
parent
39be5bc550
commit
502f075e96
|
@ -0,0 +1 @@
|
|||
Add new unstable error codes `ORG.MATRIX.MSC3848.ALREADY_JOINED`, `ORG.MATRIX.MSC3848.NOT_JOINED`, and `ORG.MATRIX.MSC3848.INSUFFICIENT_POWER` described in MSC3848.
|
|
@ -26,6 +26,7 @@ from synapse.api.errors import (
|
|||
Codes,
|
||||
InvalidClientTokenError,
|
||||
MissingClientTokenError,
|
||||
UnstableSpecAuthError,
|
||||
)
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.http import get_request_user_agent
|
||||
|
@ -106,8 +107,11 @@ class Auth:
|
|||
forgot = await self.store.did_forget(user_id, room_id)
|
||||
if not forgot:
|
||||
return membership, member_event_id
|
||||
|
||||
raise AuthError(403, "User %s not in room %s" % (user_id, room_id))
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s" % (user_id, room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
async def get_user_by_req(
|
||||
self,
|
||||
|
@ -600,8 +604,9 @@ class Auth:
|
|||
== HistoryVisibility.WORLD_READABLE
|
||||
):
|
||||
return Membership.JOIN, None
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s, and room previews are disabled"
|
||||
% (user_id, room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
|
|
@ -26,6 +26,7 @@ from twisted.web import http
|
|||
from synapse.util import json_decoder
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.types import JsonDict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -80,6 +81,12 @@ class Codes(str, Enum):
|
|||
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
|
||||
USER_DEACTIVATED = "M_USER_DEACTIVATED"
|
||||
|
||||
# Part of MSC3848
|
||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/3848
|
||||
ALREADY_JOINED = "ORG.MATRIX.MSC3848.ALREADY_JOINED"
|
||||
NOT_JOINED = "ORG.MATRIX.MSC3848.NOT_JOINED"
|
||||
INSUFFICIENT_POWER = "ORG.MATRIX.MSC3848.INSUFFICIENT_POWER"
|
||||
|
||||
# The account has been suspended on the server.
|
||||
# By opposition to `USER_DEACTIVATED`, this is a reversible measure
|
||||
# that can possibly be appealed and reverted.
|
||||
|
@ -167,7 +174,7 @@ class SynapseError(CodeMessageException):
|
|||
else:
|
||||
self._additional_fields = dict(additional_fields)
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, **self._additional_fields)
|
||||
|
||||
|
||||
|
@ -213,7 +220,7 @@ class ConsentNotGivenError(SynapseError):
|
|||
)
|
||||
self._consent_uri = consent_uri
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, consent_uri=self._consent_uri)
|
||||
|
||||
|
||||
|
@ -307,6 +314,37 @@ class AuthError(SynapseError):
|
|||
super().__init__(code, msg, errcode, additional_fields)
|
||||
|
||||
|
||||
class UnstableSpecAuthError(AuthError):
|
||||
"""An error raised when a new error code is being proposed to replace a previous one.
|
||||
This error will return a "org.matrix.unstable.errcode" property with the new error code,
|
||||
with the previous error code still being defined in the "errcode" property.
|
||||
|
||||
This error will include `org.matrix.msc3848.unstable.errcode` in the C-S error body.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: int,
|
||||
msg: str,
|
||||
errcode: str,
|
||||
previous_errcode: str = Codes.FORBIDDEN,
|
||||
additional_fields: Optional[dict] = None,
|
||||
):
|
||||
self.previous_errcode = previous_errcode
|
||||
super().__init__(code, msg, errcode, additional_fields)
|
||||
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
fields = {}
|
||||
if config is not None and config.experimental.msc3848_enabled:
|
||||
fields["org.matrix.msc3848.unstable.errcode"] = self.errcode
|
||||
return cs_error(
|
||||
self.msg,
|
||||
self.previous_errcode,
|
||||
**fields,
|
||||
**self._additional_fields,
|
||||
)
|
||||
|
||||
|
||||
class InvalidClientCredentialsError(SynapseError):
|
||||
"""An error raised when there was a problem with the authorisation credentials
|
||||
in a client request.
|
||||
|
@ -338,8 +376,8 @@ class InvalidClientTokenError(InvalidClientCredentialsError):
|
|||
super().__init__(msg=msg, errcode="M_UNKNOWN_TOKEN")
|
||||
self._soft_logout = soft_logout
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
d = super().error_dict()
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
d = super().error_dict(config)
|
||||
d["soft_logout"] = self._soft_logout
|
||||
return d
|
||||
|
||||
|
@ -362,7 +400,7 @@ class ResourceLimitError(SynapseError):
|
|||
self.limit_type = limit_type
|
||||
super().__init__(code, msg, errcode=errcode)
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(
|
||||
self.msg,
|
||||
self.errcode,
|
||||
|
@ -397,7 +435,7 @@ class InvalidCaptchaError(SynapseError):
|
|||
super().__init__(code, msg, errcode)
|
||||
self.error_url = error_url
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, error_url=self.error_url)
|
||||
|
||||
|
||||
|
@ -414,7 +452,7 @@ class LimitExceededError(SynapseError):
|
|||
super().__init__(code, msg, errcode)
|
||||
self.retry_after_ms = retry_after_ms
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, retry_after_ms=self.retry_after_ms)
|
||||
|
||||
|
||||
|
@ -429,7 +467,7 @@ class RoomKeysVersionError(SynapseError):
|
|||
super().__init__(403, "Wrong room_keys version", Codes.WRONG_ROOM_KEYS_VERSION)
|
||||
self.current_version = current_version
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, current_version=self.current_version)
|
||||
|
||||
|
||||
|
@ -469,7 +507,7 @@ class IncompatibleRoomVersionError(SynapseError):
|
|||
|
||||
self._room_version = room_version
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
return cs_error(self.msg, self.errcode, room_version=self._room_version)
|
||||
|
||||
|
||||
|
@ -515,7 +553,7 @@ class UnredactedContentDeletedError(SynapseError):
|
|||
)
|
||||
self.content_keep_ms = content_keep_ms
|
||||
|
||||
def error_dict(self) -> "JsonDict":
|
||||
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
|
||||
extra = {}
|
||||
if self.content_keep_ms is not None:
|
||||
extra = {"fi.mau.msc2815.content_keep_ms": self.content_keep_ms}
|
||||
|
|
|
@ -90,3 +90,6 @@ class ExperimentalConfig(Config):
|
|||
|
||||
# MSC3827: Filtering of /publicRooms by room type
|
||||
self.msc3827_enabled: bool = experimental.get("msc3827_enabled", False)
|
||||
|
||||
# MSC3848: Introduce errcodes for specific event sending failures
|
||||
self.msc3848_enabled: bool = experimental.get("msc3848_enabled", False)
|
||||
|
|
|
@ -30,7 +30,13 @@ from synapse.api.constants import (
|
|||
JoinRules,
|
||||
Membership,
|
||||
)
|
||||
from synapse.api.errors import AuthError, EventSizeError, SynapseError
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
EventSizeError,
|
||||
SynapseError,
|
||||
UnstableSpecAuthError,
|
||||
)
|
||||
from synapse.api.room_versions import (
|
||||
KNOWN_ROOM_VERSIONS,
|
||||
EventFormatVersions,
|
||||
|
@ -291,7 +297,11 @@ def check_state_dependent_auth_rules(
|
|||
invite_level = get_named_level(auth_dict, "invite", 0)
|
||||
|
||||
if user_level < invite_level:
|
||||
raise AuthError(403, "You don't have permission to invite users")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to invite users",
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
else:
|
||||
logger.debug("Allowing! %s", event)
|
||||
return
|
||||
|
@ -474,7 +484,11 @@ def _is_membership_change_allowed(
|
|||
return
|
||||
|
||||
if not caller_in_room: # caller isn't joined
|
||||
raise AuthError(403, "%s not in room %s." % (event.user_id, event.room_id))
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"%s not in room %s." % (event.user_id, event.room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
if Membership.INVITE == membership:
|
||||
# TODO (erikj): We should probably handle this more intelligently
|
||||
|
@ -484,10 +498,18 @@ def _is_membership_change_allowed(
|
|||
if target_banned:
|
||||
raise AuthError(403, "%s is banned from the room" % (target_user_id,))
|
||||
elif target_in_room: # the target is already in the room.
|
||||
raise AuthError(403, "%s is already in the room." % target_user_id)
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"%s is already in the room." % target_user_id,
|
||||
errcode=Codes.ALREADY_JOINED,
|
||||
)
|
||||
else:
|
||||
if user_level < invite_level:
|
||||
raise AuthError(403, "You don't have permission to invite users")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to invite users",
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif Membership.JOIN == membership:
|
||||
# Joins are valid iff caller == target and:
|
||||
# * They are not banned.
|
||||
|
@ -549,15 +571,27 @@ def _is_membership_change_allowed(
|
|||
elif Membership.LEAVE == membership:
|
||||
# TODO (erikj): Implement kicks.
|
||||
if target_banned and user_level < ban_level:
|
||||
raise AuthError(403, "You cannot unban user %s." % (target_user_id,))
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You cannot unban user %s." % (target_user_id,),
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif target_user_id != event.user_id:
|
||||
kick_level = get_named_level(auth_events, "kick", 50)
|
||||
|
||||
if user_level < kick_level or user_level <= target_level:
|
||||
raise AuthError(403, "You cannot kick user %s." % target_user_id)
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You cannot kick user %s." % target_user_id,
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif Membership.BAN == membership:
|
||||
if user_level < ban_level or user_level <= target_level:
|
||||
raise AuthError(403, "You don't have permission to ban")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to ban",
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
elif room_version.msc2403_knocking and Membership.KNOCK == membership:
|
||||
if join_rule != JoinRules.KNOCK and (
|
||||
not room_version.msc3787_knock_restricted_join_rule
|
||||
|
@ -567,7 +601,11 @@ def _is_membership_change_allowed(
|
|||
elif target_user_id != event.user_id:
|
||||
raise AuthError(403, "You cannot knock for other users")
|
||||
elif target_in_room:
|
||||
raise AuthError(403, "You cannot knock on a room you are already in")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You cannot knock on a room you are already in",
|
||||
errcode=Codes.ALREADY_JOINED,
|
||||
)
|
||||
elif caller_invited:
|
||||
raise AuthError(403, "You are already invited to this room")
|
||||
elif target_banned:
|
||||
|
@ -638,10 +676,11 @@ def _can_send_event(event: "EventBase", auth_events: StateMap["EventBase"]) -> b
|
|||
user_level = get_user_power_level(event.user_id, auth_events)
|
||||
|
||||
if user_level < send_level:
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"You don't have permission to post that to the room. "
|
||||
+ "user_level (%d) < send_level (%d)" % (user_level, send_level),
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
|
||||
# Check state_key
|
||||
|
@ -716,9 +755,10 @@ def check_historical(
|
|||
historical_level = get_named_level(auth_events, "historical", 100)
|
||||
|
||||
if user_level < historical_level:
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
'You don\'t have permission to send send historical related events ("insertion", "batch", and "marker")',
|
||||
errcode=Codes.INSUFFICIENT_POWER,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -469,7 +469,7 @@ class FederationServer(FederationBase):
|
|||
)
|
||||
for pdu in pdus_by_room[room_id]:
|
||||
event_id = pdu.event_id
|
||||
pdu_results[event_id] = e.error_dict()
|
||||
pdu_results[event_id] = e.error_dict(self.hs.config)
|
||||
return
|
||||
|
||||
for pdu in pdus_by_room[room_id]:
|
||||
|
|
|
@ -565,7 +565,7 @@ class AuthHandler:
|
|||
except LoginError as e:
|
||||
# this step failed. Merge the error dict into the response
|
||||
# so that the client can have another go.
|
||||
errordict = e.error_dict()
|
||||
errordict = e.error_dict(self.hs.config)
|
||||
|
||||
creds = await self.store.get_completed_ui_auth_stages(session.session_id)
|
||||
for f in flows:
|
||||
|
|
|
@ -41,6 +41,7 @@ from synapse.api.errors import (
|
|||
NotFoundError,
|
||||
ShadowBanError,
|
||||
SynapseError,
|
||||
UnstableSpecAuthError,
|
||||
UnsupportedRoomVersionError,
|
||||
)
|
||||
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
|
||||
|
@ -149,7 +150,11 @@ class MessageHandler:
|
|||
"Attempted to retrieve data from a room for a user that has never been in it. "
|
||||
"This should not have happened."
|
||||
)
|
||||
raise SynapseError(403, "User not in room", errcode=Codes.FORBIDDEN)
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User not in room",
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
@ -334,7 +339,11 @@ class MessageHandler:
|
|||
break
|
||||
else:
|
||||
# Loop fell through, AS has no interested users in room
|
||||
raise AuthError(403, "Appservice not in room")
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"Appservice not in room",
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
return {
|
||||
user_id: {
|
||||
|
|
|
@ -28,11 +28,11 @@ from synapse.api.constants import (
|
|||
RoomTypes,
|
||||
)
|
||||
from synapse.api.errors import (
|
||||
AuthError,
|
||||
Codes,
|
||||
NotFoundError,
|
||||
StoreError,
|
||||
SynapseError,
|
||||
UnstableSpecAuthError,
|
||||
UnsupportedRoomVersionError,
|
||||
)
|
||||
from synapse.api.ratelimiting import Ratelimiter
|
||||
|
@ -175,10 +175,11 @@ class RoomSummaryHandler:
|
|||
|
||||
# First of all, check that the room is accessible.
|
||||
if not await self._is_local_room_accessible(requested_room_id, requester):
|
||||
raise AuthError(
|
||||
raise UnstableSpecAuthError(
|
||||
403,
|
||||
"User %s not in room %s, and room previews are disabled"
|
||||
% (requester, requested_room_id),
|
||||
errcode=Codes.NOT_JOINED,
|
||||
)
|
||||
|
||||
# If this is continuing a previous session, pull the persisted data.
|
||||
|
|
|
@ -58,6 +58,7 @@ from synapse.api.errors import (
|
|||
SynapseError,
|
||||
UnrecognizedRequestError,
|
||||
)
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.logging.context import defer_to_thread, preserve_fn, run_in_background
|
||||
from synapse.logging.opentracing import active_span, start_active_span, trace_servlet
|
||||
|
@ -155,15 +156,16 @@ def is_method_cancellable(method: Callable[..., Any]) -> bool:
|
|||
return getattr(method, "cancellable", False)
|
||||
|
||||
|
||||
def return_json_error(f: failure.Failure, request: SynapseRequest) -> None:
|
||||
def return_json_error(
|
||||
f: failure.Failure, request: SynapseRequest, config: Optional[HomeServerConfig]
|
||||
) -> None:
|
||||
"""Sends a JSON error response to clients."""
|
||||
|
||||
if f.check(SynapseError):
|
||||
# mypy doesn't understand that f.check asserts the type.
|
||||
exc: SynapseError = f.value # type: ignore
|
||||
error_code = exc.code
|
||||
error_dict = exc.error_dict()
|
||||
|
||||
error_dict = exc.error_dict(config)
|
||||
logger.info("%s SynapseError: %s - %s", request, error_code, exc.msg)
|
||||
elif f.check(CancelledError):
|
||||
error_code = HTTP_STATUS_REQUEST_CANCELLED
|
||||
|
@ -450,7 +452,7 @@ class DirectServeJsonResource(_AsyncResource):
|
|||
request: SynapseRequest,
|
||||
) -> None:
|
||||
"""Implements _AsyncResource._send_error_response"""
|
||||
return_json_error(f, request)
|
||||
return_json_error(f, request, None)
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
|
@ -575,6 +577,14 @@ class JsonResource(DirectServeJsonResource):
|
|||
|
||||
return callback_return
|
||||
|
||||
def _send_error_response(
|
||||
self,
|
||||
f: failure.Failure,
|
||||
request: SynapseRequest,
|
||||
) -> None:
|
||||
"""Implements _AsyncResource._send_error_response"""
|
||||
return_json_error(f, request, self.hs.config)
|
||||
|
||||
|
||||
class DirectServeHtmlResource(_AsyncResource):
|
||||
"""A resource that will call `self._async_on_<METHOD>` on new requests,
|
||||
|
|
|
@ -20,6 +20,7 @@ from twisted.test.proto_helpers import MemoryReactor
|
|||
from synapse.api.constants import EventTypes, LoginType, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.api.room_versions import RoomVersion
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.third_party_rules import load_legacy_third_party_event_rules
|
||||
from synapse.rest import admin
|
||||
|
@ -185,12 +186,12 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase):
|
|||
"""
|
||||
|
||||
class NastyHackException(SynapseError):
|
||||
def error_dict(self) -> JsonDict:
|
||||
def error_dict(self, config: Optional[HomeServerConfig]) -> JsonDict:
|
||||
"""
|
||||
This overrides SynapseError's `error_dict` to nastily inject
|
||||
JSON into the error response.
|
||||
"""
|
||||
result = super().error_dict()
|
||||
result = super().error_dict(config)
|
||||
result["nasty"] = "very"
|
||||
return result
|
||||
|
||||
|
|
Loading…
Reference in New Issue