account_validity.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. # Copyright 2019 New Vector Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import email.mime.multipart
  15. import email.utils
  16. import logging
  17. from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Tuple
  18. from twisted.web.http import Request
  19. from synapse.api.errors import AuthError, StoreError, SynapseError
  20. from synapse.metrics.background_process_metrics import wrap_as_background_process
  21. from synapse.types import UserID
  22. from synapse.util import stringutils
  23. from synapse.util.async_helpers import delay_cancellation
  24. if TYPE_CHECKING:
  25. from synapse.server import HomeServer
  26. logger = logging.getLogger(__name__)
  27. # Types for callbacks to be registered via the module api
  28. IS_USER_EXPIRED_CALLBACK = Callable[[str], Awaitable[Optional[bool]]]
  29. ON_USER_REGISTRATION_CALLBACK = Callable[[str], Awaitable]
  30. # Temporary hooks to allow for a transition from `/_matrix/client` endpoints
  31. # to `/_synapse/client/account_validity`. See `register_account_validity_callbacks`.
  32. ON_LEGACY_SEND_MAIL_CALLBACK = Callable[[str], Awaitable]
  33. ON_LEGACY_RENEW_CALLBACK = Callable[[str], Awaitable[Tuple[bool, bool, int]]]
  34. ON_LEGACY_ADMIN_REQUEST = Callable[[Request], Awaitable]
  35. class AccountValidityHandler:
  36. def __init__(self, hs: "HomeServer"):
  37. self.hs = hs
  38. self.config = hs.config
  39. self.store = self.hs.get_datastores().main
  40. self.send_email_handler = self.hs.get_send_email_handler()
  41. self.clock = self.hs.get_clock()
  42. self._app_name = self.hs.config.email.email_app_name
  43. self._account_validity_enabled = (
  44. hs.config.account_validity.account_validity_enabled
  45. )
  46. self._account_validity_renew_by_email_enabled = (
  47. hs.config.account_validity.account_validity_renew_by_email_enabled
  48. )
  49. self._account_validity_period = None
  50. if self._account_validity_enabled:
  51. self._account_validity_period = (
  52. hs.config.account_validity.account_validity_period
  53. )
  54. if (
  55. self._account_validity_enabled
  56. and self._account_validity_renew_by_email_enabled
  57. ):
  58. # Don't do email-specific configuration if renewal by email is disabled.
  59. self._template_html = hs.config.email.account_validity_template_html
  60. self._template_text = hs.config.email.account_validity_template_text
  61. self._renew_email_subject = (
  62. hs.config.account_validity.account_validity_renew_email_subject
  63. )
  64. # Check the renewal emails to send and send them every 30min.
  65. if hs.config.worker.run_background_tasks:
  66. self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000)
  67. self._is_user_expired_callbacks: List[IS_USER_EXPIRED_CALLBACK] = []
  68. self._on_user_registration_callbacks: List[ON_USER_REGISTRATION_CALLBACK] = []
  69. self._on_legacy_send_mail_callback: Optional[
  70. ON_LEGACY_SEND_MAIL_CALLBACK
  71. ] = None
  72. self._on_legacy_renew_callback: Optional[ON_LEGACY_RENEW_CALLBACK] = None
  73. # The legacy admin requests callback isn't a protected attribute because we need
  74. # to access it from the admin servlet, which is outside of this handler.
  75. self.on_legacy_admin_request_callback: Optional[ON_LEGACY_ADMIN_REQUEST] = None
  76. def register_account_validity_callbacks(
  77. self,
  78. is_user_expired: Optional[IS_USER_EXPIRED_CALLBACK] = None,
  79. on_user_registration: Optional[ON_USER_REGISTRATION_CALLBACK] = None,
  80. on_legacy_send_mail: Optional[ON_LEGACY_SEND_MAIL_CALLBACK] = None,
  81. on_legacy_renew: Optional[ON_LEGACY_RENEW_CALLBACK] = None,
  82. on_legacy_admin_request: Optional[ON_LEGACY_ADMIN_REQUEST] = None,
  83. ) -> None:
  84. """Register callbacks from module for each hook."""
  85. if is_user_expired is not None:
  86. self._is_user_expired_callbacks.append(is_user_expired)
  87. if on_user_registration is not None:
  88. self._on_user_registration_callbacks.append(on_user_registration)
  89. # The builtin account validity feature exposes 3 endpoints (send_mail, renew, and
  90. # an admin one). As part of moving the feature into a module, we need to change
  91. # the path from /_matrix/client/unstable/account_validity/... to
  92. # /_synapse/client/account_validity, because:
  93. #
  94. # * the feature isn't part of the Matrix spec thus shouldn't live under /_matrix
  95. # * the way we register servlets means that modules can't register resources
  96. # under /_matrix/client
  97. #
  98. # We need to allow for a transition period between the old and new endpoints
  99. # in order to allow for clients to update (and for emails to be processed).
  100. #
  101. # Once the email-account-validity module is loaded, it will take control of account
  102. # validity by moving the rows from our `account_validity` table into its own table.
  103. #
  104. # Therefore, we need to allow modules (in practice just the one implementing the
  105. # email-based account validity) to temporarily hook into the legacy endpoints so we
  106. # can route the traffic coming into the old endpoints into the module, which is
  107. # why we have the following three temporary hooks.
  108. if on_legacy_send_mail is not None:
  109. if self._on_legacy_send_mail_callback is not None:
  110. raise RuntimeError("Tried to register on_legacy_send_mail twice")
  111. self._on_legacy_send_mail_callback = on_legacy_send_mail
  112. if on_legacy_renew is not None:
  113. if self._on_legacy_renew_callback is not None:
  114. raise RuntimeError("Tried to register on_legacy_renew twice")
  115. self._on_legacy_renew_callback = on_legacy_renew
  116. if on_legacy_admin_request is not None:
  117. if self.on_legacy_admin_request_callback is not None:
  118. raise RuntimeError("Tried to register on_legacy_admin_request twice")
  119. self.on_legacy_admin_request_callback = on_legacy_admin_request
  120. async def is_user_expired(self, user_id: str) -> bool:
  121. """Checks if a user has expired against third-party modules.
  122. Args:
  123. user_id: The user to check the expiry of.
  124. Returns:
  125. Whether the user has expired.
  126. """
  127. for callback in self._is_user_expired_callbacks:
  128. expired = await delay_cancellation(callback(user_id))
  129. if expired is not None:
  130. return expired
  131. if self._account_validity_enabled:
  132. # If no module could determine whether the user has expired and the legacy
  133. # configuration is enabled, fall back to it.
  134. return await self.store.is_account_expired(user_id, self.clock.time_msec())
  135. return False
  136. async def on_user_registration(self, user_id: str) -> None:
  137. """Tell third-party modules about a user's registration.
  138. Args:
  139. user_id: The ID of the newly registered user.
  140. """
  141. for callback in self._on_user_registration_callbacks:
  142. await callback(user_id)
  143. @wrap_as_background_process("send_renewals")
  144. async def _send_renewal_emails(self) -> None:
  145. """Gets the list of users whose account is expiring in the amount of time
  146. configured in the ``renew_at`` parameter from the ``account_validity``
  147. configuration, and sends renewal emails to all of these users as long as they
  148. have an email 3PID attached to their account.
  149. """
  150. expiring_users = await self.store.get_users_expiring_soon()
  151. if expiring_users:
  152. for user_id, expiration_ts_ms in expiring_users:
  153. await self._send_renewal_email(
  154. user_id=user_id, expiration_ts=expiration_ts_ms
  155. )
  156. async def send_renewal_email_to_user(self, user_id: str) -> None:
  157. """
  158. Send a renewal email for a specific user.
  159. Args:
  160. user_id: The user ID to send a renewal email for.
  161. Raises:
  162. SynapseError if the user is not set to renew.
  163. """
  164. # If a module supports sending a renewal email from here, do that, otherwise do
  165. # the legacy dance.
  166. if self._on_legacy_send_mail_callback is not None:
  167. await self._on_legacy_send_mail_callback(user_id)
  168. return
  169. if not self._account_validity_renew_by_email_enabled:
  170. raise AuthError(
  171. 403, "Account renewal via email is disabled on this server."
  172. )
  173. expiration_ts = await self.store.get_expiration_ts_for_user(user_id)
  174. # If this user isn't set to be expired, raise an error.
  175. if expiration_ts is None:
  176. raise SynapseError(400, "User has no expiration time: %s" % (user_id,))
  177. await self._send_renewal_email(user_id, expiration_ts)
  178. async def _send_renewal_email(self, user_id: str, expiration_ts: int) -> None:
  179. """Sends out a renewal email to every email address attached to the given user
  180. with a unique link allowing them to renew their account.
  181. Args:
  182. user_id: ID of the user to send email(s) to.
  183. expiration_ts: Timestamp in milliseconds for the expiration date of
  184. this user's account (used in the email templates).
  185. """
  186. addresses = await self._get_email_addresses_for_user(user_id)
  187. # Stop right here if the user doesn't have at least one email address.
  188. # In this case, they will have to ask their server admin to renew their
  189. # account manually.
  190. # We don't need to do a specific check to make sure the account isn't
  191. # deactivated, as a deactivated account isn't supposed to have any
  192. # email address attached to it.
  193. if not addresses:
  194. return
  195. try:
  196. user_display_name = await self.store.get_profile_displayname(
  197. UserID.from_string(user_id).localpart
  198. )
  199. if user_display_name is None:
  200. user_display_name = user_id
  201. except StoreError:
  202. user_display_name = user_id
  203. renewal_token = await self._get_renewal_token(user_id)
  204. url = "%s_matrix/client/unstable/account_validity/renew?token=%s" % (
  205. self.hs.config.server.public_baseurl,
  206. renewal_token,
  207. )
  208. template_vars = {
  209. "display_name": user_display_name,
  210. "expiration_ts": expiration_ts,
  211. "url": url,
  212. }
  213. html_text = self._template_html.render(**template_vars)
  214. plain_text = self._template_text.render(**template_vars)
  215. for address in addresses:
  216. raw_to = email.utils.parseaddr(address)[1]
  217. await self.send_email_handler.send_email(
  218. email_address=raw_to,
  219. subject=self._renew_email_subject,
  220. app_name=self._app_name,
  221. html=html_text,
  222. text=plain_text,
  223. )
  224. await self.store.set_renewal_mail_status(user_id=user_id, email_sent=True)
  225. async def _get_email_addresses_for_user(self, user_id: str) -> List[str]:
  226. """Retrieve the list of email addresses attached to a user's account.
  227. Args:
  228. user_id: ID of the user to lookup email addresses for.
  229. Returns:
  230. Email addresses for this account.
  231. """
  232. threepids = await self.store.user_get_threepids(user_id)
  233. addresses = []
  234. for threepid in threepids:
  235. if threepid["medium"] == "email":
  236. addresses.append(threepid["address"])
  237. return addresses
  238. async def _get_renewal_token(self, user_id: str) -> str:
  239. """Generates a 32-byte long random string that will be inserted into the
  240. user's renewal email's unique link, then saves it into the database.
  241. Args:
  242. user_id: ID of the user to generate a string for.
  243. Returns:
  244. The generated string.
  245. Raises:
  246. StoreError(500): Couldn't generate a unique string after 5 attempts.
  247. """
  248. attempts = 0
  249. while attempts < 5:
  250. try:
  251. renewal_token = stringutils.random_string(32)
  252. await self.store.set_renewal_token_for_user(user_id, renewal_token)
  253. return renewal_token
  254. except StoreError:
  255. attempts += 1
  256. raise StoreError(500, "Couldn't generate a unique string as refresh string.")
  257. async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]:
  258. """Renews the account attached to a given renewal token by pushing back the
  259. expiration date by the current validity period in the server's configuration.
  260. If it turns out that the token is valid but has already been used, then the
  261. token is considered stale. A token is stale if the 'token_used_ts_ms' db column
  262. is non-null.
  263. This method exists to support handling the legacy account validity /renew
  264. endpoint. If a module implements the on_legacy_renew callback, then this process
  265. is delegated to the module instead.
  266. Args:
  267. renewal_token: Token sent with the renewal request.
  268. Returns:
  269. A tuple containing:
  270. * A bool representing whether the token is valid and unused.
  271. * A bool which is `True` if the token is valid, but stale.
  272. * An int representing the user's expiry timestamp as milliseconds since the
  273. epoch, or 0 if the token was invalid.
  274. """
  275. # If a module supports triggering a renew from here, do that, otherwise do the
  276. # legacy dance.
  277. if self._on_legacy_renew_callback is not None:
  278. return await self._on_legacy_renew_callback(renewal_token)
  279. try:
  280. (
  281. user_id,
  282. current_expiration_ts,
  283. token_used_ts,
  284. ) = await self.store.get_user_from_renewal_token(renewal_token)
  285. except StoreError:
  286. return False, False, 0
  287. # Check whether this token has already been used.
  288. if token_used_ts:
  289. logger.info(
  290. "User '%s' attempted to use previously used token '%s' to renew account",
  291. user_id,
  292. renewal_token,
  293. )
  294. return False, True, current_expiration_ts
  295. logger.debug("Renewing an account for user %s", user_id)
  296. # Renew the account. Pass the renewal_token here so that it is not cleared.
  297. # We want to keep the token around in case the user attempts to renew their
  298. # account with the same token twice (clicking the email link twice).
  299. #
  300. # In that case, the token will be accepted, but the account's expiration ts
  301. # will remain unchanged.
  302. new_expiration_ts = await self.renew_account_for_user(
  303. user_id, renewal_token=renewal_token
  304. )
  305. return True, False, new_expiration_ts
  306. async def renew_account_for_user(
  307. self,
  308. user_id: str,
  309. expiration_ts: Optional[int] = None,
  310. email_sent: bool = False,
  311. renewal_token: Optional[str] = None,
  312. ) -> int:
  313. """Renews the account attached to a given user by pushing back the
  314. expiration date by the current validity period in the server's
  315. configuration.
  316. Args:
  317. user_id: The ID of the user to renew.
  318. expiration_ts: New expiration date. Defaults to now + validity period.
  319. email_sent: Whether an email has been sent for this validity period.
  320. renewal_token: Token sent with the renewal request. The user's token
  321. will be cleared if this is None.
  322. Returns:
  323. New expiration date for this account, as a timestamp in
  324. milliseconds since epoch.
  325. """
  326. now = self.clock.time_msec()
  327. if expiration_ts is None:
  328. assert self._account_validity_period is not None
  329. expiration_ts = now + self._account_validity_period
  330. await self.store.set_account_validity_for_user(
  331. user_id=user_id,
  332. expiration_ts=expiration_ts,
  333. email_sent=email_sent,
  334. renewal_token=renewal_token,
  335. token_used_ts=now,
  336. )
  337. return expiration_ts