You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

879 lines
34 KiB

  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.media._base import FileInfo
  34. from synapse.media.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. CHECK_LOGIN_FOR_SPAM_CALLBACK = Callable[
  194. [
  195. str,
  196. Optional[str],
  197. Optional[str],
  198. Collection[Tuple[Optional[str], str]],
  199. Optional[str],
  200. ],
  201. Awaitable[
  202. Union[
  203. Literal["NOT_SPAM"],
  204. Codes,
  205. # Highly experimental, not officially part of the spamchecker API, may
  206. # disappear without warning depending on the results of ongoing
  207. # experiments.
  208. # Use this to return additional information as part of an error.
  209. Tuple[Codes, JsonDict],
  210. ]
  211. ],
  212. ]
  213. def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
  214. """Wrapper that loads spam checkers configured using the old configuration, and
  215. registers the spam checker hooks they implement.
  216. """
  217. spam_checkers: List[Any] = []
  218. api = hs.get_module_api()
  219. for module, config in hs.config.spamchecker.spam_checkers:
  220. # Older spam checkers don't accept the `api` argument, so we
  221. # try and detect support.
  222. spam_args = inspect.getfullargspec(module)
  223. if "api" in spam_args.args:
  224. spam_checkers.append(module(config=config, api=api))
  225. else:
  226. spam_checkers.append(module(config=config))
  227. # The known spam checker hooks. If a spam checker module implements a method
  228. # which name appears in this set, we'll want to register it.
  229. spam_checker_methods = {
  230. "check_event_for_spam",
  231. "user_may_invite",
  232. "user_may_create_room",
  233. "user_may_create_room_alias",
  234. "user_may_publish_room",
  235. "check_username_for_spam",
  236. "check_registration_for_spam",
  237. "check_media_file_for_spam",
  238. }
  239. for spam_checker in spam_checkers:
  240. # Methods on legacy spam checkers might not be async, so we wrap them around a
  241. # wrapper that will call maybe_awaitable on the result.
  242. def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]:
  243. # f might be None if the callback isn't implemented by the module. In this
  244. # case we don't want to register a callback at all so we return None.
  245. if f is None:
  246. return None
  247. wrapped_func = f
  248. if f.__name__ == "check_registration_for_spam":
  249. checker_args = inspect.signature(f)
  250. if len(checker_args.parameters) == 3:
  251. # Backwards compatibility; some modules might implement a hook that
  252. # doesn't expect a 4th argument. In this case, wrap it in a function
  253. # that gives it only 3 arguments and drops the auth_provider_id on
  254. # the floor.
  255. def wrapper(
  256. email_threepid: Optional[dict],
  257. username: Optional[str],
  258. request_info: Collection[Tuple[str, str]],
  259. auth_provider_id: Optional[str],
  260. ) -> Union[Awaitable[RegistrationBehaviour], RegistrationBehaviour]:
  261. # Assertion required because mypy can't prove we won't
  262. # change `f` back to `None`. See
  263. # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
  264. assert f is not None
  265. return f(
  266. email_threepid,
  267. username,
  268. request_info,
  269. )
  270. wrapped_func = wrapper
  271. elif len(checker_args.parameters) != 4:
  272. raise RuntimeError(
  273. "Bad signature for callback check_registration_for_spam",
  274. )
  275. def run(*args: Any, **kwargs: Any) -> Awaitable:
  276. # Assertion required because mypy can't prove we won't change `f`
  277. # back to `None`. See
  278. # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions
  279. assert wrapped_func is not None
  280. return maybe_awaitable(wrapped_func(*args, **kwargs))
  281. return run
  282. # Register the hooks through the module API.
  283. hooks = {
  284. hook: async_wrapper(getattr(spam_checker, hook, None))
  285. for hook in spam_checker_methods
  286. }
  287. api.register_spam_checker_callbacks(**hooks)
  288. class SpamCheckerModuleApiCallbacks:
  289. NOT_SPAM: Literal["NOT_SPAM"] = "NOT_SPAM"
  290. def __init__(self, hs: "synapse.server.HomeServer") -> None:
  291. self.clock = hs.get_clock()
  292. self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
  293. self._should_drop_federated_event_callbacks: List[
  294. SHOULD_DROP_FEDERATED_EVENT_CALLBACK
  295. ] = []
  296. self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
  297. self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
  298. self._user_may_send_3pid_invite_callbacks: List[
  299. USER_MAY_SEND_3PID_INVITE_CALLBACK
  300. ] = []
  301. self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
  302. self._user_may_create_room_alias_callbacks: List[
  303. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
  304. ] = []
  305. self._user_may_publish_room_callbacks: List[USER_MAY_PUBLISH_ROOM_CALLBACK] = []
  306. self._check_username_for_spam_callbacks: List[
  307. CHECK_USERNAME_FOR_SPAM_CALLBACK
  308. ] = []
  309. self._check_registration_for_spam_callbacks: List[
  310. CHECK_REGISTRATION_FOR_SPAM_CALLBACK
  311. ] = []
  312. self._check_media_file_for_spam_callbacks: List[
  313. CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK
  314. ] = []
  315. self._check_login_for_spam_callbacks: List[CHECK_LOGIN_FOR_SPAM_CALLBACK] = []
  316. def register_callbacks(
  317. self,
  318. check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
  319. should_drop_federated_event: Optional[
  320. SHOULD_DROP_FEDERATED_EVENT_CALLBACK
  321. ] = None,
  322. user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
  323. user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
  324. user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
  325. user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
  326. user_may_create_room_alias: Optional[
  327. USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
  328. ] = None,
  329. user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None,
  330. check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None,
  331. check_registration_for_spam: Optional[
  332. CHECK_REGISTRATION_FOR_SPAM_CALLBACK
  333. ] = None,
  334. check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
  335. check_login_for_spam: Optional[CHECK_LOGIN_FOR_SPAM_CALLBACK] = None,
  336. ) -> None:
  337. """Register callbacks from module for each hook."""
  338. if check_event_for_spam is not None:
  339. self._check_event_for_spam_callbacks.append(check_event_for_spam)
  340. if should_drop_federated_event is not None:
  341. self._should_drop_federated_event_callbacks.append(
  342. should_drop_federated_event
  343. )
  344. if user_may_join_room is not None:
  345. self._user_may_join_room_callbacks.append(user_may_join_room)
  346. if user_may_invite is not None:
  347. self._user_may_invite_callbacks.append(user_may_invite)
  348. if user_may_send_3pid_invite is not None:
  349. self._user_may_send_3pid_invite_callbacks.append(
  350. user_may_send_3pid_invite,
  351. )
  352. if user_may_create_room is not None:
  353. self._user_may_create_room_callbacks.append(user_may_create_room)
  354. if user_may_create_room_alias is not None:
  355. self._user_may_create_room_alias_callbacks.append(
  356. user_may_create_room_alias,
  357. )
  358. if user_may_publish_room is not None:
  359. self._user_may_publish_room_callbacks.append(user_may_publish_room)
  360. if check_username_for_spam is not None:
  361. self._check_username_for_spam_callbacks.append(check_username_for_spam)
  362. if check_registration_for_spam is not None:
  363. self._check_registration_for_spam_callbacks.append(
  364. check_registration_for_spam,
  365. )
  366. if check_media_file_for_spam is not None:
  367. self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam)
  368. if check_login_for_spam is not None:
  369. self._check_login_for_spam_callbacks.append(check_login_for_spam)
  370. @trace
  371. async def check_event_for_spam(
  372. self, event: "synapse.events.EventBase"
  373. ) -> Union[Tuple[Codes, JsonDict], str]:
  374. """Checks if a given event is considered "spammy" by this server.
  375. If the server considers an event spammy, then it will be rejected if
  376. sent by a local user. If it is sent by a user on another server, the
  377. event is soft-failed.
  378. Args:
  379. event: the event to be checked
  380. Returns:
  381. - `NOT_SPAM` if the event is considered good (non-spammy) and should be let
  382. through. Other spamcheck filters may still reject it.
  383. - A `Code` if the event is considered spammy and is rejected with a specific
  384. error message/code.
  385. - A string that isn't `NOT_SPAM` if the event is considered spammy and the
  386. string should be used as the client-facing error message. This usage is
  387. generally discouraged as it doesn't support internationalization.
  388. """
  389. for callback in self._check_event_for_spam_callbacks:
  390. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  391. res = await delay_cancellation(callback(event))
  392. if res is False or res == self.NOT_SPAM:
  393. # This spam-checker accepts the event.
  394. # Other spam-checkers may reject it, though.
  395. continue
  396. elif res is True:
  397. # This spam-checker rejects the event with deprecated
  398. # return value `True`
  399. return synapse.api.errors.Codes.FORBIDDEN, {}
  400. elif (
  401. isinstance(res, tuple)
  402. and len(res) == 2
  403. and isinstance(res[0], synapse.api.errors.Codes)
  404. and isinstance(res[1], dict)
  405. ):
  406. return res
  407. elif isinstance(res, synapse.api.errors.Codes):
  408. return res, {}
  409. elif not isinstance(res, str):
  410. # mypy complains that we can't reach this code because of the
  411. # return type in CHECK_EVENT_FOR_SPAM_CALLBACK, but we don't know
  412. # for sure that the module actually returns it.
  413. logger.warning(
  414. "Module returned invalid value, rejecting message as spam"
  415. )
  416. res = "This message has been rejected as probable spam"
  417. else:
  418. # The module rejected the event either with a `Codes`
  419. # or some other `str`. In either case, we stop here.
  420. pass
  421. return res
  422. # No spam-checker has rejected the event, let it pass.
  423. return self.NOT_SPAM
  424. async def should_drop_federated_event(
  425. self, event: "synapse.events.EventBase"
  426. ) -> Union[bool, str]:
  427. """Checks if a given federated event is considered "spammy" by this
  428. server.
  429. If the server considers an event spammy, it will be silently dropped,
  430. and in doing so will split-brain our view of the room's DAG.
  431. Args:
  432. event: the event to be checked
  433. Returns:
  434. True if the event should be silently dropped
  435. """
  436. for callback in self._should_drop_federated_event_callbacks:
  437. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  438. res: Union[bool, str] = await delay_cancellation(callback(event))
  439. if res:
  440. return res
  441. return False
  442. async def user_may_join_room(
  443. self, user_id: str, room_id: str, is_invited: bool
  444. ) -> Union[Tuple[Codes, JsonDict], Literal["NOT_SPAM"]]:
  445. """Checks if a given users is allowed to join a room.
  446. Not called when a user creates a room.
  447. Args:
  448. userid: The ID of the user wanting to join the room
  449. room_id: The ID of the room the user wants to join
  450. is_invited: Whether the user is invited into the room
  451. Returns:
  452. NOT_SPAM if the operation is permitted, [Codes, Dict] otherwise.
  453. """
  454. for callback in self._user_may_join_room_callbacks:
  455. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  456. res = await delay_cancellation(callback(user_id, room_id, is_invited))
  457. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  458. if res is True or res is self.NOT_SPAM:
  459. continue
  460. elif res is False:
  461. return synapse.api.errors.Codes.FORBIDDEN, {}
  462. elif isinstance(res, synapse.api.errors.Codes):
  463. return res, {}
  464. elif (
  465. isinstance(res, tuple)
  466. and len(res) == 2
  467. and isinstance(res[0], synapse.api.errors.Codes)
  468. and isinstance(res[1], dict)
  469. ):
  470. return res
  471. else:
  472. logger.warning(
  473. "Module returned invalid value, rejecting join as spam"
  474. )
  475. return synapse.api.errors.Codes.FORBIDDEN, {}
  476. # No spam-checker has rejected the request, let it pass.
  477. return self.NOT_SPAM
  478. async def user_may_invite(
  479. self, inviter_userid: str, invitee_userid: str, room_id: str
  480. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  481. """Checks if a given user may send an invite
  482. Args:
  483. inviter_userid: The user ID of the sender of the invitation
  484. invitee_userid: The user ID targeted in the invitation
  485. room_id: The room ID
  486. Returns:
  487. NOT_SPAM if the operation is permitted, Codes otherwise.
  488. """
  489. for callback in self._user_may_invite_callbacks:
  490. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  491. res = await delay_cancellation(
  492. callback(inviter_userid, invitee_userid, room_id)
  493. )
  494. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  495. if res is True or res is self.NOT_SPAM:
  496. continue
  497. elif res is False:
  498. return synapse.api.errors.Codes.FORBIDDEN, {}
  499. elif isinstance(res, synapse.api.errors.Codes):
  500. return res, {}
  501. elif (
  502. isinstance(res, tuple)
  503. and len(res) == 2
  504. and isinstance(res[0], synapse.api.errors.Codes)
  505. and isinstance(res[1], dict)
  506. ):
  507. return res
  508. else:
  509. logger.warning(
  510. "Module returned invalid value, rejecting invite as spam"
  511. )
  512. return synapse.api.errors.Codes.FORBIDDEN, {}
  513. # No spam-checker has rejected the request, let it pass.
  514. return self.NOT_SPAM
  515. async def user_may_send_3pid_invite(
  516. self, inviter_userid: str, medium: str, address: str, room_id: str
  517. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  518. """Checks if a given user may invite a given threepid into the room
  519. Note that if the threepid is already associated with a Matrix user ID, Synapse
  520. will call user_may_invite with said user ID instead.
  521. Args:
  522. inviter_userid: The user ID of the sender of the invitation
  523. medium: The 3PID's medium (e.g. "email")
  524. address: The 3PID's address (e.g. "alice@example.com")
  525. room_id: The room ID
  526. Returns:
  527. NOT_SPAM if the operation is permitted, Codes otherwise.
  528. """
  529. for callback in self._user_may_send_3pid_invite_callbacks:
  530. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  531. res = await delay_cancellation(
  532. callback(inviter_userid, medium, address, room_id)
  533. )
  534. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  535. if res is True or res is self.NOT_SPAM:
  536. continue
  537. elif res is False:
  538. return synapse.api.errors.Codes.FORBIDDEN, {}
  539. elif isinstance(res, synapse.api.errors.Codes):
  540. return res, {}
  541. elif (
  542. isinstance(res, tuple)
  543. and len(res) == 2
  544. and isinstance(res[0], synapse.api.errors.Codes)
  545. and isinstance(res[1], dict)
  546. ):
  547. return res
  548. else:
  549. logger.warning(
  550. "Module returned invalid value, rejecting 3pid invite as spam"
  551. )
  552. return synapse.api.errors.Codes.FORBIDDEN, {}
  553. return self.NOT_SPAM
  554. async def user_may_create_room(
  555. self, userid: str
  556. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  557. """Checks if a given user may create a room
  558. Args:
  559. userid: The ID of the user attempting to create a room
  560. """
  561. for callback in self._user_may_create_room_callbacks:
  562. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  563. res = await delay_cancellation(callback(userid))
  564. if res is True or res is self.NOT_SPAM:
  565. continue
  566. elif res is False:
  567. return synapse.api.errors.Codes.FORBIDDEN, {}
  568. elif isinstance(res, synapse.api.errors.Codes):
  569. return res, {}
  570. elif (
  571. isinstance(res, tuple)
  572. and len(res) == 2
  573. and isinstance(res[0], synapse.api.errors.Codes)
  574. and isinstance(res[1], dict)
  575. ):
  576. return res
  577. else:
  578. logger.warning(
  579. "Module returned invalid value, rejecting room creation as spam"
  580. )
  581. return synapse.api.errors.Codes.FORBIDDEN, {}
  582. return self.NOT_SPAM
  583. async def user_may_create_room_alias(
  584. self, userid: str, room_alias: RoomAlias
  585. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  586. """Checks if a given user may create a room alias
  587. Args:
  588. userid: The ID of the user attempting to create a room alias
  589. room_alias: The alias to be created
  590. """
  591. for callback in self._user_may_create_room_alias_callbacks:
  592. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  593. res = await delay_cancellation(callback(userid, room_alias))
  594. if res is True or res is self.NOT_SPAM:
  595. continue
  596. elif res is False:
  597. return synapse.api.errors.Codes.FORBIDDEN, {}
  598. elif isinstance(res, synapse.api.errors.Codes):
  599. return res, {}
  600. elif (
  601. isinstance(res, tuple)
  602. and len(res) == 2
  603. and isinstance(res[0], synapse.api.errors.Codes)
  604. and isinstance(res[1], dict)
  605. ):
  606. return res
  607. else:
  608. logger.warning(
  609. "Module returned invalid value, rejecting room create as spam"
  610. )
  611. return synapse.api.errors.Codes.FORBIDDEN, {}
  612. return self.NOT_SPAM
  613. async def user_may_publish_room(
  614. self, userid: str, room_id: str
  615. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  616. """Checks if a given user may publish a room to the directory
  617. Args:
  618. userid: The user ID attempting to publish the room
  619. room_id: The ID of the room that would be published
  620. """
  621. for callback in self._user_may_publish_room_callbacks:
  622. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  623. res = await delay_cancellation(callback(userid, room_id))
  624. if res is True or res is self.NOT_SPAM:
  625. continue
  626. elif res is False:
  627. return synapse.api.errors.Codes.FORBIDDEN, {}
  628. elif isinstance(res, synapse.api.errors.Codes):
  629. return res, {}
  630. elif (
  631. isinstance(res, tuple)
  632. and len(res) == 2
  633. and isinstance(res[0], synapse.api.errors.Codes)
  634. and isinstance(res[1], dict)
  635. ):
  636. return res
  637. else:
  638. logger.warning(
  639. "Module returned invalid value, rejecting room publication as spam"
  640. )
  641. return synapse.api.errors.Codes.FORBIDDEN, {}
  642. return self.NOT_SPAM
  643. async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
  644. """Checks if a user ID or display name are considered "spammy" by this server.
  645. If the server considers a username spammy, then it will not be included in
  646. user directory results.
  647. Args:
  648. user_profile: The user information to check, it contains the keys:
  649. * user_id
  650. * display_name
  651. * avatar_url
  652. Returns:
  653. True if the user is spammy.
  654. """
  655. for callback in self._check_username_for_spam_callbacks:
  656. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  657. # Make a copy of the user profile object to ensure the spam checker cannot
  658. # modify it.
  659. res = await delay_cancellation(callback(user_profile.copy()))
  660. if res:
  661. return True
  662. return False
  663. async def check_registration_for_spam(
  664. self,
  665. email_threepid: Optional[dict],
  666. username: Optional[str],
  667. request_info: Collection[Tuple[str, str]],
  668. auth_provider_id: Optional[str] = None,
  669. ) -> RegistrationBehaviour:
  670. """Checks if we should allow the given registration request.
  671. Args:
  672. email_threepid: The email threepid used for registering, if any
  673. username: The request user name, if any
  674. request_info: List of tuples of user agent and IP that
  675. were used during the registration process.
  676. auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
  677. "cas". If any. Note this does not include users registered
  678. via a password provider.
  679. Returns:
  680. Enum for how the request should be handled
  681. """
  682. for callback in self._check_registration_for_spam_callbacks:
  683. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  684. behaviour = await delay_cancellation(
  685. callback(email_threepid, username, request_info, auth_provider_id)
  686. )
  687. assert isinstance(behaviour, RegistrationBehaviour)
  688. if behaviour != RegistrationBehaviour.ALLOW:
  689. return behaviour
  690. return RegistrationBehaviour.ALLOW
  691. @trace
  692. async def check_media_file_for_spam(
  693. self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
  694. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  695. """Checks if a piece of newly uploaded media should be blocked.
  696. This will be called for local uploads, downloads of remote media, each
  697. thumbnail generated for those, and web pages/images used for URL
  698. previews.
  699. Note that care should be taken to not do blocking IO operations in the
  700. main thread. For example, to get the contents of a file a module
  701. should do::
  702. async def check_media_file_for_spam(
  703. self, file: ReadableFileWrapper, file_info: FileInfo
  704. ) -> Union[Codes, Literal["NOT_SPAM"]]:
  705. buffer = BytesIO()
  706. await file.write_chunks_to(buffer.write)
  707. if buffer.getvalue() == b"Hello World":
  708. return synapse.module_api.NOT_SPAM
  709. return Codes.FORBIDDEN
  710. Args:
  711. file: An object that allows reading the contents of the media.
  712. file_info: Metadata about the file.
  713. """
  714. for callback in self._check_media_file_for_spam_callbacks:
  715. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  716. res = await delay_cancellation(callback(file_wrapper, file_info))
  717. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  718. if res is False or res is self.NOT_SPAM:
  719. continue
  720. elif res is True:
  721. return synapse.api.errors.Codes.FORBIDDEN, {}
  722. elif isinstance(res, synapse.api.errors.Codes):
  723. return res, {}
  724. elif (
  725. isinstance(res, tuple)
  726. and len(res) == 2
  727. and isinstance(res[0], synapse.api.errors.Codes)
  728. and isinstance(res[1], dict)
  729. ):
  730. return res
  731. else:
  732. logger.warning(
  733. "Module returned invalid value, rejecting media file as spam"
  734. )
  735. return synapse.api.errors.Codes.FORBIDDEN, {}
  736. return self.NOT_SPAM
  737. async def check_login_for_spam(
  738. self,
  739. user_id: str,
  740. device_id: Optional[str],
  741. initial_display_name: Optional[str],
  742. request_info: Collection[Tuple[Optional[str], str]],
  743. auth_provider_id: Optional[str] = None,
  744. ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
  745. """Checks if we should allow the given registration request.
  746. Args:
  747. user_id: The request user ID
  748. request_info: List of tuples of user agent and IP that
  749. were used during the registration process.
  750. auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
  751. "cas". If any. Note this does not include users registered
  752. via a password provider.
  753. Returns:
  754. Enum for how the request should be handled
  755. """
  756. for callback in self._check_login_for_spam_callbacks:
  757. with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
  758. res = await delay_cancellation(
  759. callback(
  760. user_id,
  761. device_id,
  762. initial_display_name,
  763. request_info,
  764. auth_provider_id,
  765. )
  766. )
  767. # Normalize return values to `Codes` or `"NOT_SPAM"`.
  768. if res is self.NOT_SPAM:
  769. continue
  770. elif isinstance(res, synapse.api.errors.Codes):
  771. return res, {}
  772. elif (
  773. isinstance(res, tuple)
  774. and len(res) == 2
  775. and isinstance(res[0], synapse.api.errors.Codes)
  776. and isinstance(res[1], dict)
  777. ):
  778. return res
  779. else:
  780. logger.warning(
  781. "Module returned invalid value, rejecting login as spam"
  782. )
  783. return synapse.api.errors.Codes.FORBIDDEN, {}
  784. return self.NOT_SPAM