From a1adba13702c28eca485db8426bc6bc8e2406dc3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 27 Jan 2022 14:11:34 +0000 Subject: [PATCH 1/5] Configurable limits on avatars Only allow files which file size and content types match configured limits to be set as avatar. --- changelog.d/11844.feature | 1 + docs/sample_config.yaml | 16 +++++++ synapse/config/server.py | 29 ++++++++++++ synapse/handlers/profile.py | 79 +++++++++++++++++++++++++++++++++ synapse/handlers/room_member.py | 5 +++ 5 files changed, 130 insertions(+) create mode 100644 changelog.d/11844.feature diff --git a/changelog.d/11844.feature b/changelog.d/11844.feature new file mode 100644 index 000000000000..fcf6affdb53d --- /dev/null +++ b/changelog.d/11844.feature @@ -0,0 +1 @@ +Allow configuring a maximum file size as well as a list of allowed content types for avatars. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index abf28e449089..7ed3585ace71 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -471,6 +471,22 @@ limit_remote_rooms: # #allow_per_room_profiles: false +# The largest allowed size for a user avatar. If not defined, no +# restriction will be imposed. +# +# Note that user avatar changes will not work if this is set without +# using Synapse's media repository. +# +#max_avatar_size: 10M + +# Allow mimetypes for a user avatar. If not defined, no restriction will +# be imposed. +# +# Note that user avatar changes will not work if this is set without +# using Synapse's media repository. +# +#allowed_avatar_mimetypes: ["image/png", "image/jpeg", "image/gif"] + # How long to keep redacted events in unredacted form in the database. After # this period redacted events get replaced with their redacted form in the DB. # diff --git a/synapse/config/server.py b/synapse/config/server.py index f200d0c1f1cf..26f1177e64c9 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -489,6 +489,19 @@ def read_config(self, config, **kwargs): # events with profile information that differ from the target's global profile. self.allow_per_room_profiles = config.get("allow_per_room_profiles", True) + # The maximum size an avatar can have, in bytes. + self.max_avatar_size = config.get("max_avatar_size") + if self.max_avatar_size is not None: + self.max_avatar_size = self.parse_size(self.max_avatar_size) + + # The MIME types allowed for an avatar. + self.allowed_avatar_mimetypes = config.get("allowed_avatar_mimetypes") + if self.allowed_avatar_mimetypes and not isinstance( + self.allowed_avatar_mimetypes, + list, + ): + raise ConfigError("allowed_avatar_mimetypes must be a list") + self.listeners = [parse_listener_def(x) for x in config.get("listeners", [])] # no_tls is not really supported any more, but let's grandfather it in @@ -1168,6 +1181,22 @@ def generate_config_section( # #allow_per_room_profiles: false + # The largest allowed size for a user avatar. If not defined, no + # restriction will be imposed. + # + # Note that user avatar changes will not work if this is set without + # using Synapse's media repository. + # + #max_avatar_size: 10M + + # Allow mimetypes for a user avatar. If not defined, no restriction will + # be imposed. + # + # Note that user avatar changes will not work if this is set without + # using Synapse's media repository. + # + #allowed_avatar_mimetypes: ["image/png", "image/jpeg", "image/gif"] + # How long to keep redacted events in unredacted form in the database. After # this period redacted events get replaced with their redacted form in the DB. # diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 6b5a6ded8b29..6b48de20268b 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -31,6 +31,8 @@ create_requester, get_domain_from_id, ) +from synapse.util.caches.descriptors import cached +from synapse.util.stringutils import parse_and_validate_mxc_uri if TYPE_CHECKING: from synapse.server import HomeServer @@ -64,6 +66,11 @@ def __init__(self, hs: "HomeServer"): self.user_directory_handler = hs.get_user_directory_handler() self.request_ratelimiter = hs.get_request_ratelimiter() + self.max_avatar_size = hs.config.server.max_avatar_size + self.allowed_avatar_mimetypes = hs.config.server.allowed_avatar_mimetypes + + self.server_name = hs.config.server.server_name + if hs.config.worker.run_background_tasks: self.clock.looping_call( self._update_remote_profile_cache, self.PROFILE_UPDATE_MS @@ -286,6 +293,8 @@ async def set_avatar_url( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) ) + await self.check_avatar_size_and_content_type(new_avatar_url) + avatar_url_to_set: Optional[str] = new_avatar_url if new_avatar_url == "": avatar_url_to_set = None @@ -307,6 +316,76 @@ async def set_avatar_url( await self._update_join_states(requester, target_user) + async def check_avatar_size_and_content_type(self, mxc: str) -> None: + """Check that the size and content type of the avatar at the given MXC URI are + within the configured limits. + + Args: + mxc: The MXC URI at which the avatar can be found. + + Raises: + SynapseError with an M_FORBIDDEN error code if the avatar doesn't fit within + the limits allowed by the configuration. + """ + if not await self._check_avatar_size_and_content_type(mxc): + raise SynapseError(403, "This avatar is not allowed", Codes.FORBIDDEN) + + @cached() + async def _check_avatar_size_and_content_type(self, mxc: str) -> bool: + """Check that the size and content type of the avatar at the given MXC URI are + within the configured limits. + + Args: + mxc: The MXC URI at which the avatar can be found. + + Returns: + A boolean indicating whether the file can be allowed to be set as an avatar. + """ + if not self.max_avatar_size and not self.allowed_avatar_mimetypes: + return True + + server_name, _, media_id = parse_and_validate_mxc_uri(mxc) + + if server_name == self.server_name: + media_info = await self.store.get_local_media(media_id) + else: + media_info = await self.store.get_cached_remote_media(server_name, media_id) + + if media_info is None: + # Both configuration options need to access the file's metadata, and + # retrieving remote avatars just for this becomes a bit of a faff, especially + # if e.g. the file is too big. It's also generally safe to assume most files + # used as avatar are uploaded locally, or if the upload didn't happen as part + # of a PUT request on /avatar_url that the file was at least previewed by the + # user locally (and therefore downloaded to the remote media cache). + logger.warning("Forbidding avatar change to %s: avatar not on server", mxc) + return False + + if self.max_avatar_size: + # Ensure avatar does not exceed max allowed avatar size + if media_info["media_length"] > self.max_avatar_size: + logger.warning( + "Forbidding avatar change to %s: avatar must be less than %s bytes", + mxc, + self.max_avatar_size, + ) + return False + + if self.allowed_avatar_mimetypes: + # Ensure the avatar's file type is allowed + if ( + self.allowed_avatar_mimetypes + and media_info["media_type"] not in self.allowed_avatar_mimetypes + ): + logger.warning( + "Forbidding avatar change to %s: mimetype %s not allowed", + mxc, + media_info["media_type"], + ) + return False + + return True + async def on_profile_query(self, args: JsonDict) -> JsonDict: """Handles federation profile query requests.""" diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 6aa910dd10f9..3bbd8ffd9a18 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -590,6 +590,11 @@ async def update_membership_locked( errcode=Codes.BAD_JSON, ) + if "avatar_url" in content: + await self.profile_handler.check_avatar_size_and_content_type( + content["avatar_url"], + ) + # The event content should *not* include the authorising user as # it won't be properly signed. Strip it out since it might come # back from a client updating a display name / avatar. From 5ec76317685a93fccc6e2d84a8e6989b7824c763 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 27 Jan 2022 16:02:09 +0000 Subject: [PATCH 2/5] Add tests --- synapse/handlers/profile.py | 8 +- synapse/handlers/room_member.py | 4 +- tests/handlers/test_profile.py | 106 +++++++++++++++++- tests/rest/client/test_profile.py | 172 ++++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 6b48de20268b..33f8678e5074 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -293,7 +293,7 @@ async def set_avatar_url( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) ) - await self.check_avatar_size_and_content_type(new_avatar_url) + await self.check_avatar_size_and_mime_type(new_avatar_url) avatar_url_to_set: Optional[str] = new_avatar_url if new_avatar_url == "": @@ -316,7 +316,7 @@ async def set_avatar_url( await self._update_join_states(requester, target_user) - async def check_avatar_size_and_content_type(self, mxc: str) -> None: + async def check_avatar_size_and_mime_type(self, mxc: str) -> None: """Check that the size and content type of the avatar at the given MXC URI are within the configured limits. @@ -327,11 +327,11 @@ async def check_avatar_size_and_content_type(self, mxc: str) -> None: SynapseError with an M_FORBIDDEN error code if the avatar doesn't fit within the limits allowed by the configuration. """ - if not await self._check_avatar_size_and_content_type(mxc): + if not await self._check_avatar_size_and_mime_type(mxc): raise SynapseError(403, "This avatar is not allowed", Codes.FORBIDDEN) @cached() - async def _check_avatar_size_and_content_type(self, mxc: str) -> bool: + async def _check_avatar_size_and_mime_type(self, mxc: str) -> bool: """Check that the size and content type of the avatar at the given MXC URI are within the configured limits. diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 3bbd8ffd9a18..4e979dd8e2d2 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -591,8 +591,8 @@ async def update_membership_locked( ) if "avatar_url" in content: - await self.profile_handler.check_avatar_size_and_content_type( - content["avatar_url"], + await self.profile_handler.check_avatar_size_and_mime_type( + content["avatar_url"] ) # The event content should *not* include the authorising user as diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index c153018fd81b..b7d6187a2177 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -11,12 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from typing import Any, Dict from unittest.mock import Mock import synapse.types from synapse.api.errors import AuthError, SynapseError from synapse.rest import admin +from synapse.server import HomeServer from synapse.types import UserID from tests import unittest @@ -46,7 +47,7 @@ def register_query_handler(query_type, handler): ) return hs - def prepare(self, reactor, clock, hs): + def prepare(self, reactor, clock, hs: HomeServer): self.store = hs.get_datastore() self.frank = UserID.from_string("@1234abcd:test") @@ -248,3 +249,104 @@ def test_set_my_avatar_if_disabled(self): ), SynapseError, ) + + def test_avatar_constraints_no_config(self): + """Tests that the method to check an avatar against configured constraints skips + all of its check if no constraint is configured. + """ + # The first check that's done by this method is whether the file exists; if we + # don't get an error on a non-existing file then it means all of the checks were + # successfully skipped. + allowed = self.get_success( + self.handler._check_avatar_size_and_mime_type("mxc://test/unknown_file") + ) + self.assertTrue(allowed) + + @unittest.override_config( + { + "max_avatar_size": 50, + } + ) + def test_avatar_constraints_missing(self): + """Tests that an avatar isn't allowed if the file at the given MXC URI couldn't + be found. + """ + allowed = self.get_success( + self.handler._check_avatar_size_and_mime_type("mxc://test/unknown_file") + ) + self.assertFalse(allowed) + + @unittest.override_config( + { + "max_avatar_size": 50, + } + ) + def test_avatar_constraints_file_size(self): + """Tests that a file that's above the allowed file size is forbidden but one + that's below it is allowed. + """ + self._setup_local_files( + { + "small": {"size": 40}, + "big": {"size": 60}, + } + ) + + allowed = self.get_success( + self.handler._check_avatar_size_and_mime_type("mxc://test/small") + ) + self.assertTrue(allowed) + + allowed = self.get_success( + self.handler._check_avatar_size_and_mime_type("mxc://test/big") + ) + self.assertFalse(allowed) + + @unittest.override_config( + { + "allowed_avatar_mimetypes": ["image/png"], + } + ) + def test_avatar_constraint_mime_type(self): + """Tests that a file with an unauthorised MIME type is forbidden but one with + an authorised content type is allowed. + """ + self._setup_local_files( + { + "good": {"mimetype": "image/png"}, + "bad": {"mimetype": "application/octet-stream"}, + } + ) + + allowed = self.get_success( + self.handler._check_avatar_size_and_mime_type("mxc://test/good") + ) + self.assertTrue(allowed) + + allowed = self.get_success( + self.handler._check_avatar_size_and_mime_type("mxc://test/bad") + ) + self.assertFalse(allowed) + + def _setup_local_files(self, names_and_props: Dict[str, Dict[str, Any]]): + """Stores metadata about files in the database. + + Args: + names_and_props: A dictionary with one entry per file, with the key being the + file's name, and the value being a dictionary of properties. Supported + properties are "mimetype" (for the file's type) and "size" (for the + file's size). + """ + store = self.hs.get_datastore() + + for name, props in names_and_props.items(): + self.get_success( + store.store_local_media( + media_id=name, + media_type=props.get("mimetype", "image/png"), + time_now_ms=self.clock.time_msec(), + upload_name=None, + media_length=props.get("size", 50), + user_id=UserID.from_string("@rin:test"), + ) + ) diff --git a/tests/rest/client/test_profile.py b/tests/rest/client/test_profile.py index 2860579c2e54..140e62a46a29 100644 --- a/tests/rest/client/test_profile.py +++ b/tests/rest/client/test_profile.py @@ -13,8 +13,12 @@ # limitations under the License. """Tests REST events for /profile paths.""" +from typing import Any, Dict + +from synapse.api.errors import Codes from synapse.rest import admin from synapse.rest.client import login, profile, room +from synapse.types import UserID from tests import unittest @@ -25,6 +29,7 @@ class ProfileTestCase(unittest.HomeserverTestCase): admin.register_servlets_for_client_rest_resource, login.register_servlets, profile.register_servlets, + room.register_servlets, ] def make_homeserver(self, reactor, clock): @@ -150,6 +155,173 @@ def _get_avatar_url(self, name=None): self.assertEqual(channel.code, 200, channel.result) return channel.json_body.get("avatar_url") + @unittest.override_config( + { + "max_avatar_size": 50, + } + ) + def test_avatar_size_limit_global(self): + """Tests that the maximum size limit for avatars is enforced when updating a + global profile. + """ + self._setup_local_files( + { + "small": {"size": 40}, + "big": {"size": 60}, + } + ) + + channel = self.make_request( + "PUT", + "/profile/%s/avatar_url" % (self.owner,), + content={"avatar_url": "mxc://test/big"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 403, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body + ) + + channel = self.make_request( + "PUT", + "/profile/%s/avatar_url" % (self.owner,), + content={"avatar_url": "mxc://test/small"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + @unittest.override_config( + { + "max_avatar_size": 50, + } + ) + def test_avatar_size_limit_per_room(self): + """Tests that the maximum size limit for avatars is enforced when updating a + per-room profile. + """ + self._setup_local_files( + { + "small": {"size": 40}, + "big": {"size": 60}, + } + ) + + room_id = self.helper.create_room_as(tok=self.owner_tok) + + channel = self.make_request( + "PUT", + "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + content={"membership": "join", "avatar_url": "mxc://test/big"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 403, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body + ) + + channel = self.make_request( + "PUT", + "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + content={"membership": "join", "avatar_url": "mxc://test/small"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + @unittest.override_config( + { + "allowed_avatar_mimetypes": ["image/png"], + } + ) + def test_avatar_allowed_mime_type_global(self): + """Tests that the MIME type whitelist for avatars is enforced when updating a + global profile. + """ + self._setup_local_files( + { + "good": {"mimetype": "image/png"}, + "bad": {"mimetype": "application/octet-stream"}, + } + ) + + channel = self.make_request( + "PUT", + "/profile/%s/avatar_url" % (self.owner,), + content={"avatar_url": "mxc://test/bad"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 403, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body + ) + + channel = self.make_request( + "PUT", + "/profile/%s/avatar_url" % (self.owner,), + content={"avatar_url": "mxc://test/good"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + @unittest.override_config( + { + "allowed_avatar_mimetypes": ["image/png"], + } + ) + def test_avatar_allowed_mime_type_per_room(self): + """Tests that the MIME type whitelist for avatars is enforced when updating a + per-room profile. + """ + self._setup_local_files( + { + "good": {"mimetype": "image/png"}, + "bad": {"mimetype": "application/octet-stream"}, + } + ) + + room_id = self.helper.create_room_as(tok=self.owner_tok) + + channel = self.make_request( + "PUT", + "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + content={"membership": "join", "avatar_url": "mxc://test/bad"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 403, channel.result) + self.assertEqual( + channel.json_body["errcode"], Codes.FORBIDDEN, channel.json_body + ) + + channel = self.make_request( + "PUT", + "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + content={"membership": "join", "avatar_url": "mxc://test/good"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + + def _setup_local_files(self, names_and_props: Dict[str, Dict[str, Any]]): + """Stores metadata about files in the database. + + Args: + names_and_props: A dictionary with one entry per file, with the key being the + file's name, and the value being a dictionary of properties. Supported + properties are "mimetype" (for the file's type) and "size" (for the + file's size). + """ + store = self.hs.get_datastore() + + for name, props in names_and_props.items(): + self.get_success( + store.store_local_media( + media_id=name, + media_type=props.get("mimetype", "image/png"), + time_now_ms=self.clock.time_msec(), + upload_name=None, + media_length=props.get("size", 50), + user_id=UserID.from_string("@rin:test"), + ) + ) + class ProfilesRestrictedTestCase(unittest.HomeserverTestCase): From 6189f9ff16abe1f089a3622f94a0b22039e1ae8a Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 27 Jan 2022 16:07:41 +0000 Subject: [PATCH 3/5] Fix changelog number --- changelog.d/{11844.feature => 11846.feature} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{11844.feature => 11846.feature} (100%) diff --git a/changelog.d/11844.feature b/changelog.d/11846.feature similarity index 100% rename from changelog.d/11844.feature rename to changelog.d/11846.feature From 73f80627d112bd4806b7b030c44d111e7a78bad2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 28 Jan 2022 14:00:12 +0000 Subject: [PATCH 4/5] Incorporate review --- synapse/config/server.py | 6 ++-- synapse/handlers/profile.py | 24 ++++---------- synapse/handlers/room_member.py | 7 ++-- tests/handlers/test_profile.py | 54 ++++++++++++------------------- tests/rest/client/test_profile.py | 40 +++++++---------------- 5 files changed, 45 insertions(+), 86 deletions(-) diff --git a/synapse/config/server.py b/synapse/config/server.py index 26f1177e64c9..a460cf25b42f 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -1181,16 +1181,14 @@ def generate_config_section( # #allow_per_room_profiles: false - # The largest allowed size for a user avatar. If not defined, no - # restriction will be imposed. + # The largest allowed file size for a user avatar. Defaults to no restriction. # # Note that user avatar changes will not work if this is set without # using Synapse's media repository. # #max_avatar_size: 10M - # Allow mimetypes for a user avatar. If not defined, no restriction will - # be imposed. + # The MIME types allowed for user avatars. Defaults to no restriction. # # Note that user avatar changes will not work if this is set without # using Synapse's media repository. diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 33f8678e5074..36e3ad2ba971 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -293,7 +293,8 @@ async def set_avatar_url( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) ) - await self.check_avatar_size_and_mime_type(new_avatar_url) + if not await self.check_avatar_size_and_mime_type(new_avatar_url): + raise SynapseError(403, "This avatar is not allowed", Codes.FORBIDDEN) avatar_url_to_set: Optional[str] = new_avatar_url if new_avatar_url == "": @@ -316,22 +317,8 @@ async def set_avatar_url( await self._update_join_states(requester, target_user) - async def check_avatar_size_and_mime_type(self, mxc: str) -> None: - """Check that the size and content type of the avatar at the given MXC URI are - within the configured limits. - - Args: - mxc: The MXC URI at which the avatar can be found. - - Raises: - SynapseError with an M_FORBIDDEN error code if the avatar doesn't fit within - the limits allowed by the configuration. - """ - if not await self._check_avatar_size_and_mime_type(mxc): - raise SynapseError(403, "This avatar is not allowed", Codes.FORBIDDEN) - @cached() - async def _check_avatar_size_and_mime_type(self, mxc: str) -> bool: + async def check_avatar_size_and_mime_type(self, mxc: str) -> bool: """Check that the size and content type of the avatar at the given MXC URI are within the configured limits. @@ -365,9 +352,10 @@ async def _check_avatar_size_and_mime_type(self, mxc: str) -> bool: # Ensure avatar does not exceed max allowed avatar size if media_info["media_length"] > self.max_avatar_size: logger.warning( - "Forbidding avatar change to %s: avatar must be less than %s bytes", + "Forbidding avatar change to %s: %d bytes is above the allowed size " + "limit", mxc, - self.max_avatar_size, + media_info["media_length"], ) return False diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 4e979dd8e2d2..3dd5e1b6e4dc 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -591,9 +591,10 @@ async def update_membership_locked( ) if "avatar_url" in content: - await self.profile_handler.check_avatar_size_and_mime_type( - content["avatar_url"] - ) + if not await self.profile_handler.check_avatar_size_and_mime_type( + content["avatar_url"], + ): + raise SynapseError(403, "This avatar is not allowed", Codes.FORBIDDEN) # The event content should *not* include the authorising user as # it won't be properly signed. Strip it out since it might come diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index b7d6187a2177..60235e569987 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -257,30 +257,22 @@ def test_avatar_constraints_no_config(self): # The first check that's done by this method is whether the file exists; if we # don't get an error on a non-existing file then it means all of the checks were # successfully skipped. - allowed = self.get_success( - self.handler._check_avatar_size_and_mime_type("mxc://test/unknown_file") + res = self.get_success( + self.handler.check_avatar_size_and_mime_type("mxc://test/unknown_file") ) - self.assertTrue(allowed) + self.assertTrue(res) - @unittest.override_config( - { - "max_avatar_size": 50, - } - ) + @unittest.override_config({"max_avatar_size": 50}) def test_avatar_constraints_missing(self): """Tests that an avatar isn't allowed if the file at the given MXC URI couldn't be found. """ - allowed = self.get_success( - self.handler._check_avatar_size_and_mime_type("mxc://test/unknown_file") + res = self.get_success( + self.handler.check_avatar_size_and_mime_type("mxc://test/unknown_file") ) - self.assertFalse(allowed) + self.assertFalse(res) - @unittest.override_config( - { - "max_avatar_size": 50, - } - ) + @unittest.override_config({"max_avatar_size": 50}) def test_avatar_constraints_file_size(self): """Tests that a file that's above the allowed file size is forbidden but one that's below it is allowed. @@ -292,21 +284,17 @@ def test_avatar_constraints_file_size(self): } ) - allowed = self.get_success( - self.handler._check_avatar_size_and_mime_type("mxc://test/small") + res = self.get_success( + self.handler.check_avatar_size_and_mime_type("mxc://test/small") ) - self.assertTrue(allowed) + self.assertTrue(res) - allowed = self.get_success( - self.handler._check_avatar_size_and_mime_type("mxc://test/big") + res = self.get_success( + self.handler.check_avatar_size_and_mime_type("mxc://test/big") ) - self.assertFalse(allowed) + self.assertFalse(res) - @unittest.override_config( - { - "allowed_avatar_mimetypes": ["image/png"], - } - ) + @unittest.override_config({"allowed_avatar_mimetypes": ["image/png"]}) def test_avatar_constraint_mime_type(self): """Tests that a file with an unauthorised MIME type is forbidden but one with an authorised content type is allowed. @@ -318,15 +306,15 @@ def test_avatar_constraint_mime_type(self): } ) - allowed = self.get_success( - self.handler._check_avatar_size_and_mime_type("mxc://test/good") + res = self.get_success( + self.handler.check_avatar_size_and_mime_type("mxc://test/good") ) - self.assertTrue(allowed) + self.assertTrue(res) - allowed = self.get_success( - self.handler._check_avatar_size_and_mime_type("mxc://test/bad") + res = self.get_success( + self.handler.check_avatar_size_and_mime_type("mxc://test/bad") ) - self.assertFalse(allowed) + self.assertFalse(res) def _setup_local_files(self, names_and_props: Dict[str, Dict[str, Any]]): """Stores metadata about files in the database. diff --git a/tests/rest/client/test_profile.py b/tests/rest/client/test_profile.py index 140e62a46a29..ead883ded886 100644 --- a/tests/rest/client/test_profile.py +++ b/tests/rest/client/test_profile.py @@ -155,11 +155,7 @@ def _get_avatar_url(self, name=None): self.assertEqual(channel.code, 200, channel.result) return channel.json_body.get("avatar_url") - @unittest.override_config( - { - "max_avatar_size": 50, - } - ) + @unittest.override_config({"max_avatar_size": 50}) def test_avatar_size_limit_global(self): """Tests that the maximum size limit for avatars is enforced when updating a global profile. @@ -173,7 +169,7 @@ def test_avatar_size_limit_global(self): channel = self.make_request( "PUT", - "/profile/%s/avatar_url" % (self.owner,), + f"/profile/{self.owner}/avatar_url", content={"avatar_url": "mxc://test/big"}, access_token=self.owner_tok, ) @@ -184,17 +180,13 @@ def test_avatar_size_limit_global(self): channel = self.make_request( "PUT", - "/profile/%s/avatar_url" % (self.owner,), + f"/profile/{self.owner}/avatar_url", content={"avatar_url": "mxc://test/small"}, access_token=self.owner_tok, ) self.assertEqual(channel.code, 200, channel.result) - @unittest.override_config( - { - "max_avatar_size": 50, - } - ) + @unittest.override_config({"max_avatar_size": 50}) def test_avatar_size_limit_per_room(self): """Tests that the maximum size limit for avatars is enforced when updating a per-room profile. @@ -210,7 +202,7 @@ def test_avatar_size_limit_per_room(self): channel = self.make_request( "PUT", - "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + f"/rooms/{room_id}/state/m.room.member/{self.owner}", content={"membership": "join", "avatar_url": "mxc://test/big"}, access_token=self.owner_tok, ) @@ -221,17 +213,13 @@ def test_avatar_size_limit_per_room(self): channel = self.make_request( "PUT", - "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + f"/rooms/{room_id}/state/m.room.member/{self.owner}", content={"membership": "join", "avatar_url": "mxc://test/small"}, access_token=self.owner_tok, ) self.assertEqual(channel.code, 200, channel.result) - @unittest.override_config( - { - "allowed_avatar_mimetypes": ["image/png"], - } - ) + @unittest.override_config({"allowed_avatar_mimetypes": ["image/png"]}) def test_avatar_allowed_mime_type_global(self): """Tests that the MIME type whitelist for avatars is enforced when updating a global profile. @@ -245,7 +233,7 @@ def test_avatar_allowed_mime_type_global(self): channel = self.make_request( "PUT", - "/profile/%s/avatar_url" % (self.owner,), + f"/profile/{self.owner}/avatar_url", content={"avatar_url": "mxc://test/bad"}, access_token=self.owner_tok, ) @@ -256,17 +244,13 @@ def test_avatar_allowed_mime_type_global(self): channel = self.make_request( "PUT", - "/profile/%s/avatar_url" % (self.owner,), + f"/profile/{self.owner}/avatar_url", content={"avatar_url": "mxc://test/good"}, access_token=self.owner_tok, ) self.assertEqual(channel.code, 200, channel.result) - @unittest.override_config( - { - "allowed_avatar_mimetypes": ["image/png"], - } - ) + @unittest.override_config({"allowed_avatar_mimetypes": ["image/png"]}) def test_avatar_allowed_mime_type_per_room(self): """Tests that the MIME type whitelist for avatars is enforced when updating a per-room profile. @@ -282,7 +266,7 @@ def test_avatar_allowed_mime_type_per_room(self): channel = self.make_request( "PUT", - "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + f"/rooms/{room_id}/state/m.room.member/{self.owner}", content={"membership": "join", "avatar_url": "mxc://test/bad"}, access_token=self.owner_tok, ) @@ -293,7 +277,7 @@ def test_avatar_allowed_mime_type_per_room(self): channel = self.make_request( "PUT", - "/rooms/%s/state/m.room.member/%s" % (room_id, self.owner), + f"/rooms/{room_id}/state/m.room.member/{self.owner}", content={"membership": "join", "avatar_url": "mxc://test/good"}, access_token=self.owner_tok, ) From 1aa9bc1f960cc981af0830f827848e4c918b2775 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 28 Jan 2022 14:00:38 +0000 Subject: [PATCH 5/5] Regenerate sample config --- docs/sample_config.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 7ed3585ace71..689b207fc0c0 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -471,16 +471,14 @@ limit_remote_rooms: # #allow_per_room_profiles: false -# The largest allowed size for a user avatar. If not defined, no -# restriction will be imposed. +# The largest allowed file size for a user avatar. Defaults to no restriction. # # Note that user avatar changes will not work if this is set without # using Synapse's media repository. # #max_avatar_size: 10M -# Allow mimetypes for a user avatar. If not defined, no restriction will -# be imposed. +# The MIME types allowed for user avatars. Defaults to no restriction. # # Note that user avatar changes will not work if this is set without # using Synapse's media repository.