spamcheck.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. # Copyright 2017 New Vector Ltd
  2. # Copyright 2019 The Matrix.org Foundation C.I.C.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import inspect
  16. import logging
  17. from typing import (
  18. TYPE_CHECKING,
  19. Any,
  20. Awaitable,
  21. Callable,
  22. Collection,
  23. List,
  24. Optional,
  25. Tuple,
  26. Union,
  27. )
  28. # `Literal` appears with Python 3.8.
  29. from typing_extensions import Literal
  30. import synapse
  31. from synapse.api.errors import Codes
  32. from synapse.logging.opentracing import trace
  33. from synapse.rest.media.v1._base import FileInfo
  34. from synapse.rest.media.v1.media_storage import ReadableFileWrapper
  35. from synapse.spam_checker_api import RegistrationBehaviour
  36. from synapse.types import JsonDict, RoomAlias, UserProfile
  37. from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
  38. from synapse.util.metrics import Measure
  39. if TYPE_CHECKING:
  40. import synapse.events
  41. import synapse.server
  42. logger = logging.getLogger(__name__)
  43. CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[
  44. ["synapse.events.EventBase"],
  45. Awaitable[
  46. Union[
  47. str,
  48. Codes,
  49. # Highly experimental, not officially part of the spamchecker API, may
  50. # disappear without warning depending on the results of ongoing
  51. # experiments.
  52. # Use this to return additional information as part of an error.
  53. Tuple[Codes, JsonDict],
  54. # Deprecated
  55. bool,
  56. ]
  57. ],
  58. ]
  59. SHOULD_DROP_FEDERATED_EVENT_CALLBACK = Callable[
  60. ["synapse.events.EventBase"],
  61. Awaitable[Union[bool, str]],
  62. ]
  63. USER_MAY_JOIN_ROOM_CALLBACK = Callable[
  64. [str, str, bool],
  65. Awaitable[
  66. Union[
  67. Literal["NOT_SPAM"],
  68. Codes,
  69. # Highly experimental, not officially part of the spamchecker API, may
  70. # disappear without warning depending on the results of ongoing
  71. # experiments.
  72. # Use this to return additional information as part of an error.
  73. Tuple[Codes, JsonDict],
  74. # Deprecated
  75. bool,
  76. ]
  77. ],
  78. ]
  79. USER_MAY_INVITE_CALLBACK = Callable[
  80. [str, str, str],
  81. Awaitable[
  82. Union[
  83. Literal["NOT_SPAM"],
  84. Codes,
  85. # Highly experimental, not officially part of the spamchecker API, may
  86. # disappear without warning depending on the results of ongoing
  87. # experiments.
  88. # Use this to return additional information as part of an error.
  89. Tuple[Codes, JsonDict],
  90. # Deprecated
  91. bool,
  92. ]
  93. ],
  94. ]
  95. USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[
  96. [str, str, str, str],
  97. Awaitable[
  98. Union[
  99. Literal["NOT_SPAM"],
  100. Codes,
  101. # Highly experimental, not officially part of the spamchecker API, may
  102. # disappear without warning depending on the results of ongoing
  103. # experiments.
  104. # Use this to return additional information as part of an error.
  105. Tuple[Codes, JsonDict],
  106. # Deprecated
  107. bool,
  108. ]
  109. ],
  110. ]
  111. USER_MAY_CREATE_ROOM_CALLBACK = Callable[
  112. [str],
  113. Awaitable[
  114. Union[
  115. Literal["NOT_SPAM"],
  116. Codes,
  117. # Highly experimental, not officially part of the spamchecker API, may
  118. # disappear without warning depending on the results of ongoing
  119. # experiments.
  120. # Use this to return additional information as part of an error.
  121. Tuple[Codes, JsonDict],
  122. # Deprecated
  123. bool,
  124. ]
  125. ],
  126. ]
  127. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[
  128. [str, RoomAlias],
  129. Awaitable[
  130. Union[
  131. Literal["NOT_SPAM"],
  132. Codes,
  133. # Highly experimental, not officially part of the spamchecker API, may
  134. # disappear without warning depending on the results of ongoing
  135. # experiments.
  136. # Use this to return additional information as part of an error.
  137. Tuple[Codes, JsonDict],
  138. # Deprecated
  139. bool,
  140. ]
  141. ],
  142. ]
  143. USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[
  144. [str, str],
  145. Awaitable[
  146. Union[
  147. Literal["NOT_SPAM"],
  148. Codes,
  149. # Highly experimental, not officially part of the spamchecker API, may
  150. # disappear without warning depending on the results of ongoing
  151. # experiments.
  152. # Use this to return additional information as part of an error.
  153. Tuple[Codes, JsonDict],
  154. # Deprecated
  155. bool,
  156. ]
  157. ],
  158. ]
  159. CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]]
  160. LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
  161. [
  162. Optional[dict],
  163. Optional[str],
  164. Collection[Tuple[str, str]],
  165. ],
  166. Awaitable[RegistrationBehaviour],
  167. ]
  168. CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
  169. [
  170. Optional[dict],
  171. Optional[str],
  172. Collection[Tuple[str, str]],
  173. Optional[str],
  174. ],
  175. Awaitable[RegistrationBehaviour],
  176. ]
  177. CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[
  178. [ReadableFileWrapper, FileInfo],
  179. Awaitable[
  180. Union[
  181. Literal["NOT_SPAM"],
  182. Codes,
  183. # Highly experimental, not officially part of the spamchecker API, may
  184. # disappear without warning depending on the results of ongoing
  185. # experiments.
  186. # Use this to return additional information as part of an error.
  187. Tuple[Codes, JsonDict],
  188. # Deprecated
  189. bool,
  190. ]
  191. ],
  192. ]
  193. def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
  194. """Wrapper that loads spam checkers configured using the old configuration, and
  195. registers the spam checker hooks they implement.
  196. """
  197. spam_checkers: List[Any] = []
  198. api = hs.get_module_api()
  199. for module, config in hs.config.spamchecker.spam_checkers:
  200. # Older spam checkers don't accept the `api` argument, so we
  201. # try and detect support.
  202. spam_args = inspect.getfullargspec(module)
  203. if "api" in spam_args.args:
  204. spam_checkers.append(module(config=config, api=api))
  205. else:
  206. spam_checkers.append(module(config=config))
  207. # The known spam checker hooks. If a spam checker module implements a method
  208. # which name appears in this set, we'll want to register it.
  209. spam_checker_methods = {
  210. "check_event_for_spam",
  211. "user_may_invite",
  212. "user_may_create_room",
  213. "user_may_create_room_alias",
  214. "user_may_publish_room",
  215. "check_username_for_spam",
  216. "check_registration_for_spam",
  217. "check_media_file_for_spam",
  218. }
  219. for spam_checker in spam_checkers:
  220. # Methods on legacy spam checkers might not be async, so we wrap them around a
  221. # wrapper that will call maybe_awaitable on the result.
  222. def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]:
  223. # f might be None if the callback isn't implemented by the module. In this
  224. # case we don't want to register a callback at all so we return None.
  225. if f is None:
  226. return None
  227. wrapped_func = f
  228. if f.__name__ == "check_registration_for_spam":
  229. checker_args = inspect.signature(f)
  230. if len(checker_args.parameters) == 3:
  231. # Backwards compatibility; some modules might implement a hook that
  232. # doesn't expect a 4th argument. In this case, wrap it in a function
  233. # that gives it only 3 arguments and drops the auth_provider_id on
  234. # the floor.
  235. def wrapper(
  236. email_threepid: Optional[dict],
  237. username: Optional[str],
  238. request_info: Collection[Tuple[str, str]],
  239. auth_provider_id: Optional[str],
  240. ) -> Union[Awaitable[RegistrationBehaviour], RegistrationBehaviour]:
  241. # Assertion required because mypy can't prove we won't
  242. # change `f` back to `None`. See
  243. # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
  244. assert f is not None
  245. return f(
  246. email_threepid,
  247. username,
  248. request_info,
  249. )
  250. wrapped_func = wrapper
  251. elif len(checker_args.parameters) != 4:
  252. raise RuntimeError(
  253. "Bad signature for callback check_registration_for_spam",
  254. )
  255. def run(*args: Any, **kwargs: Any) -> Awaitable:
  256. # Assertion required because mypy can't prove we won't change `f`
  257. # back to `None`. See
  258. # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
  259. assert wrapped_func is not None
  260. return maybe_awaitable(wrapped_func(*args, **kwargs))
  261. return run
  262. # Register the hooks through the module API.
  263. hooks = {
  264. hook: async_wrapper(getattr(spam_checker, hook, None))
  265. for hook in spam_checker_methods
  266. }
  267. api.register_spam_checker_callbacks(**hooks)
  268. class SpamChecker:
  269. NOT_SPAM: Literal["NOT_SPAM"] = "NOT_SPAM"
  270. def __init__(self, hs: "synapse.server.HomeServer") -> None:
  271. self.hs = hs
  272. self.clock = hs.get_clock()
  273. self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
  274. self._should_drop_federated_event_callbacks: List[
  275. SHOULD_DROP_FEDERATED_EVENT_CALLBACK
  276. ] = []
  277. self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
  278. self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
  279. self._user_may_send_3pid_invite_callbacks: List[
  280. USER_MAY_SEND_3PID_INVITE_CALLBACK
  281. ] = []
  282. self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
  283. self._user_may_create_room_alias_callbacks: List[
  284. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
  285. ] = []
  286. self._user_may_publish_room_callbacks: List[USER_MAY_PUBLISH_ROOM_CALLBACK] = []
  287. self._check_username_for_spam_callbacks: List[
  288. CHECK_USERNAME_FOR_SPAM_CALLBACK
  289. ] = []
  290. self._check_registration_for_spam_callbacks: List[
  291. CHECK_REGISTRATION_FOR_SPAM_CALLBACK
  292. ] = []
  293. self._check_media_file_for_spam_callbacks: List[
  294. CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK
  295. ] = []
  296. def register_callbacks(
  297. self,
  298. check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
  299. should_drop_federated_event: Optional[
  300. SHOULD_DROP_FEDERATED_EVENT_CALLBACK
  301. ] = None,
  302. user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
  303. user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
  304. user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
  305. user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
  306. user_may_create_room_alias: Optional[
  307. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
  308. ] = None,
  309. user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None,
  310. check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None,
  311. check_registration_for_spam: Optional[
  312. CHECK_REGISTRATION_FOR_SPAM_CALLBACK
  313. ] = None,
  314. check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
  315. ) -> None:
  316. """Register callbacks from module for each hook."""
  317. if check_event_for_spam is not None:
  318. self._check_event_for_spam_callbacks.append(check_event_for_spam)
  319. if should_drop_federated_event is not None:
  320. self._should_drop_federated_event_callbacks.append(
  321. should_drop_federated_event
  322. )
  323. if user_may_join_room is not None:
  324. self._user_may_join_room_callbacks.append(user_may_join_room)
  325. if user_may_invite is not None:
  326. self._user_may_invite_callbacks.append(user_may_invite)
  327. if user_may_send_3pid_invite is not None:
  328. self._user_may_send_3pid_invite_callbacks.append(
  329. user_may_send_3pid_invite,
  330. )
  331. if user_may_create_room is not None:
  332. self._user_may_create_room_callbacks.append(user_may_create_room)
  333. if user_may_create_room_alias is not None:
  334. self._user_may_create_room_alias_callbacks.append(
  335. user_may_create_room_alias,
  336. )
  337. if user_may_publish_room is not None:
  338. self._user_may_publish_room_callbacks.append(user_may_publish_room)
  339. if check_username_for_spam is not None:
  340. self._check_username_for_spam_callbacks.append(check_username_for_spam)
  341. if check_registration_for_spam is not None:
  342. self._check_registration_for_spam_callbacks.append(
  343. check_registration_for_spam,
  344. )
  345. if check_media_file_for_spam is not None:
  346. self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam)
  347. @trace
  348. async def check_event_for_spam(
  349. self, event: "synapse.events.EventBase"
  350. ) -> Union[Tuple[Codes, JsonDict], str]:
  351. """Checks if a given event is considered "spammy" by this server.
  352. If the server considers an event spammy, then it will be rejected if
  353. sent by a local user. If it is sent by a user on another server, the
  354. event is soft-failed.
  355. Args:
  356. event: the event to be checked
  357. Returns:
  358. - `NOT_SPAM` if the event is considered good (non-spammy) and should be let
  359. through. Other spamcheck filters may still reject it.
  360. - A `Code` if the event is considered spammy and is rejected with a specific
  361. error message/code.
  362. - A string that isn't `NOT_SPAM` if the event is considered spammy and the
  363. string should be used as the client-facing error message. This usage is
  364. generally discouraged as it doesn't support internationalization.
  365. """
  366. for callback in self._check_event_for_spam_callbacks:
  367. with Measure(
  368. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  369. ):
  370. res = await delay_cancellation(callback(event))
  371. if res is False or res == self.NOT_SPAM:
  372. # This spam-checker accepts the event.
  373. # Other spam-checkers may reject it, though.
  374. continue
  375. elif res is True:
  376. # This spam-checker rejects the event with deprecated
  377. # return value `True`
  378. return synapse.api.errors.Codes.FORBIDDEN, {}
  379. elif (
  380. isinstance(res, tuple)
  381. and len(res) == 2
  382. and isinstance(res[0], synapse.api.errors.Codes)
  383. and isinstance(res[1], dict)
  384. ):
  385. return res
  386. elif isinstance(res, synapse.api.errors.Codes):
  387. return res, {}
  388. elif not isinstance(res, str):
  389. # mypy complains that we can't reach this code because of the
  390. # return type in CHECK_EVENT_FOR_SPAM_CALLBACK, but we don't know
  391. # for sure that the module actually returns it.
  392. logger.warning(
  393. "Module returned invalid value, rejecting message as spam"
  394. )
  395. res = "This message has been rejected as probable spam"
  396. else:
  397. # The module rejected the event either with a `Codes`
  398. # or some other `str`. In either case, we stop here.
  399. pass
  400. return res
  401. # No spam-checker has rejected the event, let it pass.
  402. return self.NOT_SPAM
  403. async def should_drop_federated_event(
  404. self, event: "synapse.events.EventBase"
  405. ) -> Union[bool, str]:
  406. """Checks if a given federated event is considered "spammy" by this
  407. server.
  408. If the server considers an event spammy, it will be silently dropped,
  409. and in doing so will split-brain our view of the room's DAG.
  410. Args:
  411. event: the event to be checked
  412. Returns:
  413. True if the event should be silently dropped
  414. """
  415. for callback in self._should_drop_federated_event_callbacks:
  416. with Measure(
  417. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  418. ):
  419. res: Union[bool, str] = await delay_cancellation(callback(event))
  420. if res:
  421. return res
  422. return False
  423. async def user_may_join_room(
  424. self, user_id: str, room_id: str, is_invited: bool
  425. ) -> Union[Tuple[Codes, JsonDict], Literal["NOT_SPAM"]]:
  426. """Checks if a given users is allowed to join a room.
  427. Not called when a user creates a room.
  428. Args:
  429. userid: The ID of the user wanting to join the room
  430. room_id: The ID of the room the user wants to join
  431. is_invited: Whether the user is invited into the room
  432. Returns:
  433. NOT_SPAM if the operation is permitted, [Codes, Dict] otherwise.
  434. """
  435. for callback in self._user_may_join_room_callbacks:
  436. with Measure(
  437. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  438. ):
  439. res = await delay_cancellation(callback(user_id, room_id, is_invited))
  440. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  441. if res is True or res is self.NOT_SPAM:
  442. continue
  443. elif res is False:
  444. return synapse.api.errors.Codes.FORBIDDEN, {}
  445. elif isinstance(res, synapse.api.errors.Codes):
  446. return res, {}
  447. elif (
  448. isinstance(res, tuple)
  449. and len(res) == 2
  450. and isinstance(res[0], synapse.api.errors.Codes)
  451. and isinstance(res[1], dict)
  452. ):
  453. return res
  454. else:
  455. logger.warning(
  456. "Module returned invalid value, rejecting join as spam"
  457. )
  458. return synapse.api.errors.Codes.FORBIDDEN, {}
  459. # No spam-checker has rejected the request, let it pass.
  460. return self.NOT_SPAM
  461. async def user_may_invite(
  462. self, inviter_userid: str, invitee_userid: str, room_id: str
  463. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  464. """Checks if a given user may send an invite
  465. Args:
  466. inviter_userid: The user ID of the sender of the invitation
  467. invitee_userid: The user ID targeted in the invitation
  468. room_id: The room ID
  469. Returns:
  470. NOT_SPAM if the operation is permitted, Codes otherwise.
  471. """
  472. for callback in self._user_may_invite_callbacks:
  473. with Measure(
  474. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  475. ):
  476. res = await delay_cancellation(
  477. callback(inviter_userid, invitee_userid, room_id)
  478. )
  479. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  480. if res is True or res is self.NOT_SPAM:
  481. continue
  482. elif res is False:
  483. return synapse.api.errors.Codes.FORBIDDEN, {}
  484. elif isinstance(res, synapse.api.errors.Codes):
  485. return res, {}
  486. elif (
  487. isinstance(res, tuple)
  488. and len(res) == 2
  489. and isinstance(res[0], synapse.api.errors.Codes)
  490. and isinstance(res[1], dict)
  491. ):
  492. return res
  493. else:
  494. logger.warning(
  495. "Module returned invalid value, rejecting invite as spam"
  496. )
  497. return synapse.api.errors.Codes.FORBIDDEN, {}
  498. # No spam-checker has rejected the request, let it pass.
  499. return self.NOT_SPAM
  500. async def user_may_send_3pid_invite(
  501. self, inviter_userid: str, medium: str, address: str, room_id: str
  502. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  503. """Checks if a given user may invite a given threepid into the room
  504. Note that if the threepid is already associated with a Matrix user ID, Synapse
  505. will call user_may_invite with said user ID instead.
  506. Args:
  507. inviter_userid: The user ID of the sender of the invitation
  508. medium: The 3PID's medium (e.g. "email")
  509. address: The 3PID's address (e.g. "alice@example.com")
  510. room_id: The room ID
  511. Returns:
  512. NOT_SPAM if the operation is permitted, Codes otherwise.
  513. """
  514. for callback in self._user_may_send_3pid_invite_callbacks:
  515. with Measure(
  516. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  517. ):
  518. res = await delay_cancellation(
  519. callback(inviter_userid, medium, address, room_id)
  520. )
  521. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  522. if res is True or res is self.NOT_SPAM:
  523. continue
  524. elif res is False:
  525. return synapse.api.errors.Codes.FORBIDDEN, {}
  526. elif isinstance(res, synapse.api.errors.Codes):
  527. return res, {}
  528. elif (
  529. isinstance(res, tuple)
  530. and len(res) == 2
  531. and isinstance(res[0], synapse.api.errors.Codes)
  532. and isinstance(res[1], dict)
  533. ):
  534. return res
  535. else:
  536. logger.warning(
  537. "Module returned invalid value, rejecting 3pid invite as spam"
  538. )
  539. return synapse.api.errors.Codes.FORBIDDEN, {}
  540. return self.NOT_SPAM
  541. async def user_may_create_room(
  542. self, userid: str
  543. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  544. """Checks if a given user may create a room
  545. Args:
  546. userid: The ID of the user attempting to create a room
  547. """
  548. for callback in self._user_may_create_room_callbacks:
  549. with Measure(
  550. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  551. ):
  552. res = await delay_cancellation(callback(userid))
  553. if res is True or res is self.NOT_SPAM:
  554. continue
  555. elif res is False:
  556. return synapse.api.errors.Codes.FORBIDDEN, {}
  557. elif isinstance(res, synapse.api.errors.Codes):
  558. return res, {}
  559. elif (
  560. isinstance(res, tuple)
  561. and len(res) == 2
  562. and isinstance(res[0], synapse.api.errors.Codes)
  563. and isinstance(res[1], dict)
  564. ):
  565. return res
  566. else:
  567. logger.warning(
  568. "Module returned invalid value, rejecting room creation as spam"
  569. )
  570. return synapse.api.errors.Codes.FORBIDDEN, {}
  571. return self.NOT_SPAM
  572. async def user_may_create_room_alias(
  573. self, userid: str, room_alias: RoomAlias
  574. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  575. """Checks if a given user may create a room alias
  576. Args:
  577. userid: The ID of the user attempting to create a room alias
  578. room_alias: The alias to be created
  579. """
  580. for callback in self._user_may_create_room_alias_callbacks:
  581. with Measure(
  582. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  583. ):
  584. res = await delay_cancellation(callback(userid, room_alias))
  585. if res is True or res is self.NOT_SPAM:
  586. continue
  587. elif res is False:
  588. return synapse.api.errors.Codes.FORBIDDEN, {}
  589. elif isinstance(res, synapse.api.errors.Codes):
  590. return res, {}
  591. elif (
  592. isinstance(res, tuple)
  593. and len(res) == 2
  594. and isinstance(res[0], synapse.api.errors.Codes)
  595. and isinstance(res[1], dict)
  596. ):
  597. return res
  598. else:
  599. logger.warning(
  600. "Module returned invalid value, rejecting room create as spam"
  601. )
  602. return synapse.api.errors.Codes.FORBIDDEN, {}
  603. return self.NOT_SPAM
  604. async def user_may_publish_room(
  605. self, userid: str, room_id: str
  606. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  607. """Checks if a given user may publish a room to the directory
  608. Args:
  609. userid: The user ID attempting to publish the room
  610. room_id: The ID of the room that would be published
  611. """
  612. for callback in self._user_may_publish_room_callbacks:
  613. with Measure(
  614. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  615. ):
  616. res = await delay_cancellation(callback(userid, room_id))
  617. if res is True or res is self.NOT_SPAM:
  618. continue
  619. elif res is False:
  620. return synapse.api.errors.Codes.FORBIDDEN, {}
  621. elif isinstance(res, synapse.api.errors.Codes):
  622. return res, {}
  623. elif (
  624. isinstance(res, tuple)
  625. and len(res) == 2
  626. and isinstance(res[0], synapse.api.errors.Codes)
  627. and isinstance(res[1], dict)
  628. ):
  629. return res
  630. else:
  631. logger.warning(
  632. "Module returned invalid value, rejecting room publication as spam"
  633. )
  634. return synapse.api.errors.Codes.FORBIDDEN, {}
  635. return self.NOT_SPAM
  636. async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
  637. """Checks if a user ID or display name are considered "spammy" by this server.
  638. If the server considers a username spammy, then it will not be included in
  639. user directory results.
  640. Args:
  641. user_profile: The user information to check, it contains the keys:
  642. * user_id
  643. * display_name
  644. * avatar_url
  645. Returns:
  646. True if the user is spammy.
  647. """
  648. for callback in self._check_username_for_spam_callbacks:
  649. with Measure(
  650. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  651. ):
  652. # Make a copy of the user profile object to ensure the spam checker cannot
  653. # modify it.
  654. res = await delay_cancellation(callback(user_profile.copy()))
  655. if res:
  656. return True
  657. return False
  658. async def check_registration_for_spam(
  659. self,
  660. email_threepid: Optional[dict],
  661. username: Optional[str],
  662. request_info: Collection[Tuple[str, str]],
  663. auth_provider_id: Optional[str] = None,
  664. ) -> RegistrationBehaviour:
  665. """Checks if we should allow the given registration request.
  666. Args:
  667. email_threepid: The email threepid used for registering, if any
  668. username: The request user name, if any
  669. request_info: List of tuples of user agent and IP that
  670. were used during the registration process.
  671. auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
  672. "cas". If any. Note this does not include users registered
  673. via a password provider.
  674. Returns:
  675. Enum for how the request should be handled
  676. """
  677. for callback in self._check_registration_for_spam_callbacks:
  678. with Measure(
  679. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  680. ):
  681. behaviour = await delay_cancellation(
  682. callback(email_threepid, username, request_info, auth_provider_id)
  683. )
  684. assert isinstance(behaviour, RegistrationBehaviour)
  685. if behaviour != RegistrationBehaviour.ALLOW:
  686. return behaviour
  687. return RegistrationBehaviour.ALLOW
  688. async def check_media_file_for_spam(
  689. self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
  690. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  691. """Checks if a piece of newly uploaded media should be blocked.
  692. This will be called for local uploads, downloads of remote media, each
  693. thumbnail generated for those, and web pages/images used for URL
  694. previews.
  695. Note that care should be taken to not do blocking IO operations in the
  696. main thread. For example, to get the contents of a file a module
  697. should do::
  698. async def check_media_file_for_spam(
  699. self, file: ReadableFileWrapper, file_info: FileInfo
  700. ) -> Union[Codes, Literal["NOT_SPAM"]]:
  701. buffer = BytesIO()
  702. await file.write_chunks_to(buffer.write)
  703. if buffer.getvalue() == b"Hello World":
  704. return synapse.module_api.NOT_SPAM
  705. return Codes.FORBIDDEN
  706. Args:
  707. file: An object that allows reading the contents of the media.
  708. file_info: Metadata about the file.
  709. """
  710. for callback in self._check_media_file_for_spam_callbacks:
  711. with Measure(
  712. self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
  713. ):
  714. res = await delay_cancellation(callback(file_wrapper, file_info))
  715. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  716. if res is False or res is self.NOT_SPAM:
  717. continue
  718. elif res is True:
  719. return synapse.api.errors.Codes.FORBIDDEN, {}
  720. elif isinstance(res, synapse.api.errors.Codes):
  721. return res, {}
  722. elif (
  723. isinstance(res, tuple)
  724. and len(res) == 2
  725. and isinstance(res[0], synapse.api.errors.Codes)
  726. and isinstance(res[1], dict)
  727. ):
  728. return res
  729. else:
  730. logger.warning(
  731. "Module returned invalid value, rejecting media file as spam"
  732. )
  733. return synapse.api.errors.Codes.FORBIDDEN, {}
  734. return self.NOT_SPAM