diff --git a/changelog.d/18562.feature b/changelog.d/18562.feature new file mode 100644 index 00000000000..c25ad4718f3 --- /dev/null +++ b/changelog.d/18562.feature @@ -0,0 +1 @@ +Add `msc4133_key_allowlist` experimental option to configure a list of custom profile keys that users may set. \ No newline at end of file diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 2dc75a778e0..8e976c94836 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -21,7 +21,7 @@ import enum from functools import cache -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, List, Optional import attr import attr.validators @@ -552,6 +552,18 @@ def read_config( # MSC4133: Custom profile fields self.msc4133_enabled: bool = experimental.get("msc4133_enabled", False) + self.msc4133_key_allowlist: Optional[List[str]] = experimental.get( + "msc4133_key_allowlist" + ) + if self.msc4133_key_allowlist is not None: + if not isinstance(self.msc4133_key_allowlist, list) or not all( + isinstance(k, str) for k in self.msc4133_key_allowlist + ): + raise ConfigError( + "experimental_features.msc4133_key_allowlist must be a list of strings", + ("experimental", "msc4133_key_allowlist"), + ) + # MSC4210: Remove legacy mentions self.msc4210_enabled: bool = experimental.get("msc4210_enabled", False) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index cdc388b4ab1..7e52cf77ea7 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -481,6 +481,14 @@ async def set_profile_field( if not by_admin and target_user != requester.user: raise AuthError(403, "Cannot set another user's profile") + allowlist = self.hs.config.experimental.msc4133_key_allowlist + if allowlist is not None and field_name not in allowlist: + raise SynapseError( + 403, + "Changing this profile field is disabled on this server", + Codes.FORBIDDEN, + ) + await self.store.set_profile_field(target_user, field_name, new_value) # Custom fields do not propagate into the user directory *or* rooms. diff --git a/tests/rest/client/test_profile.py b/tests/rest/client/test_profile.py index 708402b7929..a4a8b3c9abe 100644 --- a/tests/rest/client/test_profile.py +++ b/tests/rest/client/test_profile.py @@ -776,6 +776,34 @@ def test_set_custom_field_other(self) -> None: self.assertEqual(channel.code, 403, channel.result) self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN) + @unittest.override_config( + { + "experimental_features": { + "msc4133_enabled": True, + "msc4133_key_allowlist": ["allowed_field"], + } + } + ) + def test_set_custom_field_not_allowlisted(self) -> None: + """Setting a field not in the allowlist should be rejected.""" + channel = self.make_request( + "PUT", + f"/_matrix/client/unstable/uk.tcpip.msc4133/profile/{self.owner}/blocked", + content={"blocked": "test"}, + access_token=self.owner_tok, + ) + self.assertEqual(channel.code, 403, channel.result) + self.assertEqual(channel.json_body["errcode"], Codes.FORBIDDEN) + + # Allowed field should succeed. + channel = self.make_request( + "PUT", + f"/_matrix/client/unstable/uk.tcpip.msc4133/profile/{self.owner}/allowed_field", + content={"allowed_field": "ok"}, + 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]]) -> None: """Stores metadata about files in the database.