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.
 
 
 
 
 
 

2851 lines
100 KiB

  1. # Copyright 2014-2016 OpenMarket Ltd
  2. # Copyright 2017-2018 New Vector Ltd
  3. # Copyright 2019,2020 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import logging
  17. import random
  18. import re
  19. from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
  20. import attr
  21. from synapse.api.constants import UserTypes
  22. from synapse.api.errors import (
  23. Codes,
  24. NotFoundError,
  25. StoreError,
  26. SynapseError,
  27. ThreepidValidationError,
  28. )
  29. from synapse.config.homeserver import HomeServerConfig
  30. from synapse.metrics.background_process_metrics import wrap_as_background_process
  31. from synapse.storage.database import (
  32. DatabasePool,
  33. LoggingDatabaseConnection,
  34. LoggingTransaction,
  35. )
  36. from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore
  37. from synapse.storage.databases.main.stats import StatsStore
  38. from synapse.storage.types import Cursor
  39. from synapse.storage.util.id_generators import IdGenerator
  40. from synapse.storage.util.sequence import build_sequence_generator
  41. from synapse.types import JsonDict, UserID, UserInfo
  42. from synapse.util.caches.descriptors import cached
  43. if TYPE_CHECKING:
  44. from synapse.server import HomeServer
  45. THIRTY_MINUTES_IN_MS = 30 * 60 * 1000
  46. logger = logging.getLogger(__name__)
  47. class ExternalIDReuseException(Exception):
  48. """Exception if writing an external id for a user fails,
  49. because this external id is given to an other user."""
  50. class LoginTokenExpired(Exception):
  51. """Exception if the login token sent expired"""
  52. class LoginTokenReused(Exception):
  53. """Exception if the login token sent was already used"""
  54. @attr.s(frozen=True, slots=True, auto_attribs=True)
  55. class TokenLookupResult:
  56. """Result of looking up an access token.
  57. Attributes:
  58. user_id: The user that this token authenticates as
  59. is_guest
  60. shadow_banned
  61. token_id: The ID of the access token looked up
  62. device_id: The device associated with the token, if any.
  63. valid_until_ms: The timestamp the token expires, if any.
  64. token_owner: The "owner" of the token. This is either the same as the
  65. user, or a server admin who is logged in as the user.
  66. token_used: True if this token was used at least once in a request.
  67. This field can be out of date since `get_user_by_access_token` is
  68. cached.
  69. """
  70. user_id: str
  71. token_id: int
  72. is_guest: bool = False
  73. shadow_banned: bool = False
  74. device_id: Optional[str] = None
  75. valid_until_ms: Optional[int] = None
  76. token_owner: str = attr.ib()
  77. token_used: bool = False
  78. # Make the token owner default to the user ID, which is the common case.
  79. @token_owner.default
  80. def _default_token_owner(self) -> str:
  81. return self.user_id
  82. @attr.s(auto_attribs=True, frozen=True, slots=True)
  83. class RefreshTokenLookupResult:
  84. """Result of looking up a refresh token."""
  85. user_id: str
  86. """The user this token belongs to."""
  87. device_id: str
  88. """The device associated with this refresh token."""
  89. token_id: int
  90. """The ID of this refresh token."""
  91. next_token_id: Optional[int]
  92. """The ID of the refresh token which replaced this one."""
  93. has_next_refresh_token_been_refreshed: bool
  94. """True if the next refresh token was used for another refresh."""
  95. has_next_access_token_been_used: bool
  96. """True if the next access token was already used at least once."""
  97. expiry_ts: Optional[int]
  98. """The time at which the refresh token expires and can not be used.
  99. If None, the refresh token doesn't expire."""
  100. ultimate_session_expiry_ts: Optional[int]
  101. """The time at which the session comes to an end and can no longer be
  102. refreshed.
  103. If None, the session can be refreshed indefinitely."""
  104. @attr.s(auto_attribs=True, frozen=True, slots=True)
  105. class LoginTokenLookupResult:
  106. """Result of looking up a login token."""
  107. user_id: str
  108. """The user this token belongs to."""
  109. auth_provider_id: Optional[str]
  110. """The SSO Identity Provider that the user authenticated with, to get this token."""
  111. auth_provider_session_id: Optional[str]
  112. """The session ID advertised by the SSO Identity Provider."""
  113. class RegistrationWorkerStore(CacheInvalidationWorkerStore):
  114. def __init__(
  115. self,
  116. database: DatabasePool,
  117. db_conn: LoggingDatabaseConnection,
  118. hs: "HomeServer",
  119. ):
  120. super().__init__(database, db_conn, hs)
  121. self.config: HomeServerConfig = hs.config
  122. # Note: we don't check this sequence for consistency as we'd have to
  123. # call `find_max_generated_user_id_localpart` each time, which is
  124. # expensive if there are many entries.
  125. self._user_id_seq = build_sequence_generator(
  126. db_conn,
  127. database.engine,
  128. find_max_generated_user_id_localpart,
  129. "user_id_seq",
  130. table=None,
  131. id_column=None,
  132. )
  133. self._account_validity_enabled = (
  134. hs.config.account_validity.account_validity_enabled
  135. )
  136. self._account_validity_period = None
  137. self._account_validity_startup_job_max_delta = None
  138. if self._account_validity_enabled:
  139. self._account_validity_period = (
  140. hs.config.account_validity.account_validity_period
  141. )
  142. self._account_validity_startup_job_max_delta = (
  143. hs.config.account_validity.account_validity_startup_job_max_delta
  144. )
  145. if hs.config.worker.run_background_tasks:
  146. self._clock.call_later(
  147. 0.0,
  148. self._set_expiration_date_when_missing,
  149. )
  150. # Create a background job for culling expired 3PID validity tokens
  151. if hs.config.worker.run_background_tasks:
  152. self._clock.looping_call(
  153. self.cull_expired_threepid_validation_tokens, THIRTY_MINUTES_IN_MS
  154. )
  155. @cached()
  156. async def get_user_by_id(self, user_id: str) -> Optional[UserInfo]:
  157. """Returns info about the user account, if it exists."""
  158. def get_user_by_id_txn(txn: LoggingTransaction) -> Optional[Dict[str, Any]]:
  159. # We could technically use simple_select_one here, but it would not perform
  160. # the COALESCEs (unless hacked into the column names), which could yield
  161. # confusing results.
  162. txn.execute(
  163. """
  164. SELECT
  165. name, is_guest, admin, consent_version, consent_ts,
  166. consent_server_notice_sent, appservice_id, creation_ts, user_type,
  167. deactivated, COALESCE(shadow_banned, FALSE) AS shadow_banned,
  168. COALESCE(approved, TRUE) AS approved,
  169. COALESCE(locked, FALSE) AS locked
  170. FROM users
  171. WHERE name = ?
  172. """,
  173. (user_id,),
  174. )
  175. rows = self.db_pool.cursor_to_dict(txn)
  176. if len(rows) == 0:
  177. return None
  178. return rows[0]
  179. row = await self.db_pool.runInteraction(
  180. desc="get_user_by_id",
  181. func=get_user_by_id_txn,
  182. )
  183. if row is None:
  184. return None
  185. return UserInfo(
  186. appservice_id=row["appservice_id"],
  187. consent_server_notice_sent=row["consent_server_notice_sent"],
  188. consent_version=row["consent_version"],
  189. consent_ts=row["consent_ts"],
  190. creation_ts=row["creation_ts"],
  191. is_admin=bool(row["admin"]),
  192. is_deactivated=bool(row["deactivated"]),
  193. is_guest=bool(row["is_guest"]),
  194. is_shadow_banned=bool(row["shadow_banned"]),
  195. user_id=UserID.from_string(row["name"]),
  196. user_type=row["user_type"],
  197. approved=bool(row["approved"]),
  198. locked=bool(row["locked"]),
  199. )
  200. async def is_trial_user(self, user_id: str) -> bool:
  201. """Checks if user is in the "trial" period, i.e. within the first
  202. N days of registration defined by `mau_trial_days` config or the
  203. `mau_appservice_trial_days` config.
  204. Args:
  205. user_id: The user to check for trial status.
  206. """
  207. info = await self.get_user_by_id(user_id)
  208. if not info:
  209. return False
  210. now = self._clock.time_msec()
  211. days = self.config.server.mau_appservice_trial_days.get(
  212. info.appservice_id, self.config.server.mau_trial_days
  213. )
  214. trial_duration_ms = days * 24 * 60 * 60 * 1000
  215. is_trial = (now - info.creation_ts * 1000) < trial_duration_ms
  216. return is_trial
  217. @cached()
  218. async def get_user_by_access_token(self, token: str) -> Optional[TokenLookupResult]:
  219. """Get a user from the given access token.
  220. Args:
  221. token: The access token of a user.
  222. Returns:
  223. None, if the token did not match, otherwise a `TokenLookupResult`
  224. """
  225. return await self.db_pool.runInteraction(
  226. "get_user_by_access_token", self._query_for_auth, token
  227. )
  228. @cached()
  229. async def get_expiration_ts_for_user(self, user_id: str) -> Optional[int]:
  230. """Get the expiration timestamp for the account bearing a given user ID.
  231. Args:
  232. user_id: The ID of the user.
  233. Returns:
  234. None, if the account has no expiration timestamp, otherwise int
  235. representation of the timestamp (as a number of milliseconds since epoch).
  236. """
  237. return await self.db_pool.simple_select_one_onecol(
  238. table="account_validity",
  239. keyvalues={"user_id": user_id},
  240. retcol="expiration_ts_ms",
  241. allow_none=True,
  242. desc="get_expiration_ts_for_user",
  243. )
  244. async def is_account_expired(self, user_id: str, current_ts: int) -> bool:
  245. """
  246. Returns whether an user account is expired.
  247. Args:
  248. user_id: The user's ID
  249. current_ts: The current timestamp
  250. Returns:
  251. Whether the user account has expired
  252. """
  253. expiration_ts = await self.get_expiration_ts_for_user(user_id)
  254. return expiration_ts is not None and current_ts >= expiration_ts
  255. async def set_account_validity_for_user(
  256. self,
  257. user_id: str,
  258. expiration_ts: int,
  259. email_sent: bool,
  260. renewal_token: Optional[str] = None,
  261. token_used_ts: Optional[int] = None,
  262. ) -> None:
  263. """Updates the account validity properties of the given account, with the
  264. given values.
  265. Args:
  266. user_id: ID of the account to update properties for.
  267. expiration_ts: New expiration date, as a timestamp in milliseconds
  268. since epoch.
  269. email_sent: True means a renewal email has been sent for this account
  270. and there's no need to send another one for the current validity
  271. period.
  272. renewal_token: Renewal token the user can use to extend the validity
  273. of their account. Defaults to no token.
  274. token_used_ts: A timestamp of when the current token was used to renew
  275. the account.
  276. """
  277. def set_account_validity_for_user_txn(txn: LoggingTransaction) -> None:
  278. self.db_pool.simple_update_txn(
  279. txn=txn,
  280. table="account_validity",
  281. keyvalues={"user_id": user_id},
  282. updatevalues={
  283. "expiration_ts_ms": expiration_ts,
  284. "email_sent": email_sent,
  285. "renewal_token": renewal_token,
  286. "token_used_ts_ms": token_used_ts,
  287. },
  288. )
  289. self._invalidate_cache_and_stream(
  290. txn, self.get_expiration_ts_for_user, (user_id,)
  291. )
  292. await self.db_pool.runInteraction(
  293. "set_account_validity_for_user", set_account_validity_for_user_txn
  294. )
  295. async def set_renewal_token_for_user(
  296. self, user_id: str, renewal_token: str
  297. ) -> None:
  298. """Defines a renewal token for a given user, and clears the token_used timestamp.
  299. Args:
  300. user_id: ID of the user to set the renewal token for.
  301. renewal_token: Random unique string that will be used to renew the
  302. user's account.
  303. Raises:
  304. StoreError: The provided token is already set for another user.
  305. """
  306. await self.db_pool.simple_update_one(
  307. table="account_validity",
  308. keyvalues={"user_id": user_id},
  309. updatevalues={"renewal_token": renewal_token, "token_used_ts_ms": None},
  310. desc="set_renewal_token_for_user",
  311. )
  312. async def get_user_from_renewal_token(
  313. self, renewal_token: str
  314. ) -> Tuple[str, int, Optional[int]]:
  315. """Get a user ID and renewal status from a renewal token.
  316. Args:
  317. renewal_token: The renewal token to perform the lookup with.
  318. Returns:
  319. A tuple of containing the following values:
  320. * The ID of a user to which the token belongs.
  321. * An int representing the user's expiry timestamp as milliseconds since the
  322. epoch, or 0 if the token was invalid.
  323. * An optional int representing the timestamp of when the user renewed their
  324. account timestamp as milliseconds since the epoch. None if the account
  325. has not been renewed using the current token yet.
  326. """
  327. ret_dict = await self.db_pool.simple_select_one(
  328. table="account_validity",
  329. keyvalues={"renewal_token": renewal_token},
  330. retcols=["user_id", "expiration_ts_ms", "token_used_ts_ms"],
  331. desc="get_user_from_renewal_token",
  332. )
  333. return (
  334. ret_dict["user_id"],
  335. ret_dict["expiration_ts_ms"],
  336. ret_dict["token_used_ts_ms"],
  337. )
  338. async def get_renewal_token_for_user(self, user_id: str) -> str:
  339. """Get the renewal token associated with a given user ID.
  340. Args:
  341. user_id: The user ID to lookup a token for.
  342. Returns:
  343. The renewal token associated with this user ID.
  344. """
  345. return await self.db_pool.simple_select_one_onecol(
  346. table="account_validity",
  347. keyvalues={"user_id": user_id},
  348. retcol="renewal_token",
  349. desc="get_renewal_token_for_user",
  350. )
  351. async def get_users_expiring_soon(self) -> List[Tuple[str, int]]:
  352. """Selects users whose account will expire in the [now, now + renew_at] time
  353. window (see configuration for account_validity for information on what renew_at
  354. refers to).
  355. Returns:
  356. A list of tuples, each with a user ID and expiration time (in milliseconds).
  357. """
  358. def select_users_txn(
  359. txn: LoggingTransaction, now_ms: int, renew_at: int
  360. ) -> List[Tuple[str, int]]:
  361. sql = (
  362. "SELECT user_id, expiration_ts_ms FROM account_validity"
  363. " WHERE email_sent = FALSE AND (expiration_ts_ms - ?) <= ?"
  364. )
  365. values = [now_ms, renew_at]
  366. txn.execute(sql, values)
  367. return cast(List[Tuple[str, int]], txn.fetchall())
  368. return await self.db_pool.runInteraction(
  369. "get_users_expiring_soon",
  370. select_users_txn,
  371. self._clock.time_msec(),
  372. self.config.account_validity.account_validity_renew_at,
  373. )
  374. async def set_renewal_mail_status(self, user_id: str, email_sent: bool) -> None:
  375. """Sets or unsets the flag that indicates whether a renewal email has been sent
  376. to the user (and the user hasn't renewed their account yet).
  377. Args:
  378. user_id: ID of the user to set/unset the flag for.
  379. email_sent: Flag which indicates whether a renewal email has been sent
  380. to this user.
  381. """
  382. await self.db_pool.simple_update_one(
  383. table="account_validity",
  384. keyvalues={"user_id": user_id},
  385. updatevalues={"email_sent": email_sent},
  386. desc="set_renewal_mail_status",
  387. )
  388. async def delete_account_validity_for_user(self, user_id: str) -> None:
  389. """Deletes the entry for the given user in the account validity table, removing
  390. their expiration date and renewal token.
  391. Args:
  392. user_id: ID of the user to remove from the account validity table.
  393. """
  394. await self.db_pool.simple_delete_one(
  395. table="account_validity",
  396. keyvalues={"user_id": user_id},
  397. desc="delete_account_validity_for_user",
  398. )
  399. async def is_server_admin(self, user: UserID) -> bool:
  400. """Determines if a user is an admin of this homeserver.
  401. Args:
  402. user: user ID of the user to test
  403. Returns:
  404. true iff the user is a server admin, false otherwise.
  405. """
  406. res = await self.db_pool.simple_select_one_onecol(
  407. table="users",
  408. keyvalues={"name": user.to_string()},
  409. retcol="admin",
  410. allow_none=True,
  411. desc="is_server_admin",
  412. )
  413. return bool(res) if res else False
  414. async def set_server_admin(self, user: UserID, admin: bool) -> None:
  415. """Sets whether a user is an admin of this homeserver.
  416. Args:
  417. user: user ID of the user to test
  418. admin: true iff the user is to be a server admin, false otherwise.
  419. """
  420. def set_server_admin_txn(txn: LoggingTransaction) -> None:
  421. self.db_pool.simple_update_one_txn(
  422. txn, "users", {"name": user.to_string()}, {"admin": 1 if admin else 0}
  423. )
  424. self._invalidate_cache_and_stream(
  425. txn, self.get_user_by_id, (user.to_string(),)
  426. )
  427. await self.db_pool.runInteraction("set_server_admin", set_server_admin_txn)
  428. async def set_shadow_banned(self, user: UserID, shadow_banned: bool) -> None:
  429. """Sets whether a user shadow-banned.
  430. Args:
  431. user: user ID of the user to test
  432. shadow_banned: true iff the user is to be shadow-banned, false otherwise.
  433. """
  434. def set_shadow_banned_txn(txn: LoggingTransaction) -> None:
  435. user_id = user.to_string()
  436. self.db_pool.simple_update_one_txn(
  437. txn,
  438. table="users",
  439. keyvalues={"name": user_id},
  440. updatevalues={"shadow_banned": shadow_banned},
  441. )
  442. # In order for this to apply immediately, clear the cache for this user.
  443. tokens = self.db_pool.simple_select_onecol_txn(
  444. txn,
  445. table="access_tokens",
  446. keyvalues={"user_id": user_id},
  447. retcol="token",
  448. )
  449. for token in tokens:
  450. self._invalidate_cache_and_stream(
  451. txn, self.get_user_by_access_token, (token,)
  452. )
  453. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  454. await self.db_pool.runInteraction("set_shadow_banned", set_shadow_banned_txn)
  455. async def set_user_type(self, user: UserID, user_type: Optional[UserTypes]) -> None:
  456. """Sets the user type.
  457. Args:
  458. user: user ID of the user.
  459. user_type: type of the user or None for a user without a type.
  460. """
  461. def set_user_type_txn(txn: LoggingTransaction) -> None:
  462. self.db_pool.simple_update_one_txn(
  463. txn, "users", {"name": user.to_string()}, {"user_type": user_type}
  464. )
  465. self._invalidate_cache_and_stream(
  466. txn, self.get_user_by_id, (user.to_string(),)
  467. )
  468. await self.db_pool.runInteraction("set_user_type", set_user_type_txn)
  469. def _query_for_auth(
  470. self, txn: LoggingTransaction, token: str
  471. ) -> Optional[TokenLookupResult]:
  472. sql = """
  473. SELECT users.name as user_id,
  474. users.is_guest,
  475. users.shadow_banned,
  476. access_tokens.id as token_id,
  477. access_tokens.device_id,
  478. access_tokens.valid_until_ms,
  479. access_tokens.user_id as token_owner,
  480. access_tokens.used as token_used
  481. FROM users
  482. INNER JOIN access_tokens on users.name = COALESCE(puppets_user_id, access_tokens.user_id)
  483. WHERE token = ?
  484. """
  485. txn.execute(sql, (token,))
  486. rows = self.db_pool.cursor_to_dict(txn)
  487. if rows:
  488. row = rows[0]
  489. # This field is nullable, ensure it comes out as a boolean
  490. if row["token_used"] is None:
  491. row["token_used"] = False
  492. return TokenLookupResult(**row)
  493. return None
  494. @cached()
  495. async def is_real_user(self, user_id: str) -> bool:
  496. """Determines if the user is a real user, ie does not have a 'user_type'.
  497. Args:
  498. user_id: user id to test
  499. Returns:
  500. True if user 'user_type' is null or empty string
  501. """
  502. return await self.db_pool.runInteraction(
  503. "is_real_user", self.is_real_user_txn, user_id
  504. )
  505. @cached()
  506. async def is_support_user(self, user_id: str) -> bool:
  507. """Determines if the user is of type UserTypes.SUPPORT
  508. Args:
  509. user_id: user id to test
  510. Returns:
  511. True if user is of type UserTypes.SUPPORT
  512. """
  513. return await self.db_pool.runInteraction(
  514. "is_support_user", self.is_support_user_txn, user_id
  515. )
  516. def is_real_user_txn(self, txn: LoggingTransaction, user_id: str) -> bool:
  517. res = self.db_pool.simple_select_one_onecol_txn(
  518. txn=txn,
  519. table="users",
  520. keyvalues={"name": user_id},
  521. retcol="user_type",
  522. allow_none=True,
  523. )
  524. return res is None
  525. def is_support_user_txn(self, txn: LoggingTransaction, user_id: str) -> bool:
  526. res = self.db_pool.simple_select_one_onecol_txn(
  527. txn=txn,
  528. table="users",
  529. keyvalues={"name": user_id},
  530. retcol="user_type",
  531. allow_none=True,
  532. )
  533. return True if res == UserTypes.SUPPORT else False
  534. async def get_users_by_id_case_insensitive(self, user_id: str) -> Dict[str, str]:
  535. """Gets users that match user_id case insensitively.
  536. Returns:
  537. A mapping of user_id -> password_hash.
  538. """
  539. def f(txn: LoggingTransaction) -> Dict[str, str]:
  540. sql = "SELECT name, password_hash FROM users WHERE lower(name) = lower(?)"
  541. txn.execute(sql, (user_id,))
  542. result = cast(List[Tuple[str, str]], txn.fetchall())
  543. return dict(result)
  544. return await self.db_pool.runInteraction("get_users_by_id_case_insensitive", f)
  545. async def record_user_external_id(
  546. self, auth_provider: str, external_id: str, user_id: str
  547. ) -> None:
  548. """Record a mapping from an external user id to a mxid
  549. See notes in _record_user_external_id_txn about what constitutes valid data.
  550. Args:
  551. auth_provider: identifier for the remote auth provider
  552. external_id: id on that system
  553. user_id: complete mxid that it is mapped to
  554. Raises:
  555. ExternalIDReuseException if the new external_id could not be mapped.
  556. """
  557. try:
  558. await self.db_pool.runInteraction(
  559. "record_user_external_id",
  560. self._record_user_external_id_txn,
  561. auth_provider,
  562. external_id,
  563. user_id,
  564. )
  565. except self.database_engine.module.IntegrityError:
  566. raise ExternalIDReuseException()
  567. def _record_user_external_id_txn(
  568. self,
  569. txn: LoggingTransaction,
  570. auth_provider: str,
  571. external_id: str,
  572. user_id: str,
  573. ) -> None:
  574. """
  575. Record a mapping from an external user id to a mxid.
  576. Note that the auth provider IDs (and the external IDs) are not validated
  577. against configured IdPs as Synapse does not know its relationship to
  578. external systems. For example, it might be useful to pre-configure users
  579. before enabling a new IdP or an IdP might be temporarily offline, but
  580. still valid.
  581. Args:
  582. txn: The database transaction.
  583. auth_provider: identifier for the remote auth provider
  584. external_id: id on that system
  585. user_id: complete mxid that it is mapped to
  586. """
  587. self.db_pool.simple_insert_txn(
  588. txn,
  589. table="user_external_ids",
  590. values={
  591. "auth_provider": auth_provider,
  592. "external_id": external_id,
  593. "user_id": user_id,
  594. },
  595. )
  596. async def remove_user_external_id(
  597. self, auth_provider: str, external_id: str, user_id: str
  598. ) -> None:
  599. """Remove a mapping from an external user id to a mxid
  600. If the mapping is not found, this method does nothing.
  601. Args:
  602. auth_provider: identifier for the remote auth provider
  603. external_id: id on that system
  604. user_id: complete mxid that it is mapped to
  605. """
  606. await self.db_pool.simple_delete(
  607. table="user_external_ids",
  608. keyvalues={
  609. "auth_provider": auth_provider,
  610. "external_id": external_id,
  611. "user_id": user_id,
  612. },
  613. desc="remove_user_external_id",
  614. )
  615. async def replace_user_external_id(
  616. self,
  617. record_external_ids: List[Tuple[str, str]],
  618. user_id: str,
  619. ) -> None:
  620. """Replace mappings from external user ids to a mxid in a single transaction.
  621. All mappings are deleted and the new ones are created.
  622. See notes in _record_user_external_id_txn about what constitutes valid data.
  623. Args:
  624. record_external_ids:
  625. List with tuple of auth_provider and external_id to record
  626. user_id: complete mxid that it is mapped to
  627. Raises:
  628. ExternalIDReuseException if the new external_id could not be mapped.
  629. """
  630. def _remove_user_external_ids_txn(
  631. txn: LoggingTransaction,
  632. user_id: str,
  633. ) -> None:
  634. """Remove all mappings from external user ids to a mxid
  635. If these mappings are not found, this method does nothing.
  636. Args:
  637. user_id: complete mxid that it is mapped to
  638. """
  639. self.db_pool.simple_delete_txn(
  640. txn,
  641. table="user_external_ids",
  642. keyvalues={"user_id": user_id},
  643. )
  644. def _replace_user_external_id_txn(
  645. txn: LoggingTransaction,
  646. ) -> None:
  647. _remove_user_external_ids_txn(txn, user_id)
  648. for auth_provider, external_id in record_external_ids:
  649. self._record_user_external_id_txn(
  650. txn,
  651. auth_provider,
  652. external_id,
  653. user_id,
  654. )
  655. try:
  656. await self.db_pool.runInteraction(
  657. "replace_user_external_id",
  658. _replace_user_external_id_txn,
  659. )
  660. except self.database_engine.module.IntegrityError:
  661. raise ExternalIDReuseException()
  662. async def get_user_by_external_id(
  663. self, auth_provider: str, external_id: str
  664. ) -> Optional[str]:
  665. """Look up a user by their external auth id
  666. Args:
  667. auth_provider: identifier for the remote auth provider
  668. external_id: id on that system
  669. Returns:
  670. the mxid of the user, or None if they are not known
  671. """
  672. return await self.db_pool.simple_select_one_onecol(
  673. table="user_external_ids",
  674. keyvalues={"auth_provider": auth_provider, "external_id": external_id},
  675. retcol="user_id",
  676. allow_none=True,
  677. desc="get_user_by_external_id",
  678. )
  679. async def get_external_ids_by_user(self, mxid: str) -> List[Tuple[str, str]]:
  680. """Look up external ids for the given user
  681. Args:
  682. mxid: the MXID to be looked up
  683. Returns:
  684. Tuples of (auth_provider, external_id)
  685. """
  686. res = await self.db_pool.simple_select_list(
  687. table="user_external_ids",
  688. keyvalues={"user_id": mxid},
  689. retcols=("auth_provider", "external_id"),
  690. desc="get_external_ids_by_user",
  691. )
  692. return [(r["auth_provider"], r["external_id"]) for r in res]
  693. async def count_all_users(self) -> int:
  694. """Counts all users registered on the homeserver."""
  695. def _count_users(txn: LoggingTransaction) -> int:
  696. txn.execute("SELECT COUNT(*) AS users FROM users")
  697. rows = self.db_pool.cursor_to_dict(txn)
  698. if rows:
  699. return rows[0]["users"]
  700. return 0
  701. return await self.db_pool.runInteraction("count_users", _count_users)
  702. async def count_daily_user_type(self) -> Dict[str, int]:
  703. """
  704. Counts 1) native non guest users
  705. 2) native guests users
  706. 3) bridged users
  707. who registered on the homeserver in the past 24 hours
  708. """
  709. def _count_daily_user_type(txn: LoggingTransaction) -> Dict[str, int]:
  710. yesterday = int(self._clock.time()) - (60 * 60 * 24)
  711. sql = """
  712. SELECT user_type, COUNT(*) AS count FROM (
  713. SELECT
  714. CASE
  715. WHEN is_guest=0 AND appservice_id IS NULL THEN 'native'
  716. WHEN is_guest=1 AND appservice_id IS NULL THEN 'guest'
  717. WHEN is_guest=0 AND appservice_id IS NOT NULL THEN 'bridged'
  718. END AS user_type
  719. FROM users
  720. WHERE creation_ts > ?
  721. ) AS t GROUP BY user_type
  722. """
  723. results = {"native": 0, "guest": 0, "bridged": 0}
  724. txn.execute(sql, (yesterday,))
  725. for row in txn:
  726. results[row[0]] = row[1]
  727. return results
  728. return await self.db_pool.runInteraction(
  729. "count_daily_user_type", _count_daily_user_type
  730. )
  731. async def count_nonbridged_users(self) -> int:
  732. def _count_users(txn: LoggingTransaction) -> int:
  733. txn.execute(
  734. """
  735. SELECT COUNT(*) FROM users
  736. WHERE appservice_id IS NULL
  737. """
  738. )
  739. (count,) = cast(Tuple[int], txn.fetchone())
  740. return count
  741. return await self.db_pool.runInteraction("count_users", _count_users)
  742. async def count_real_users(self) -> int:
  743. """Counts all users without a special user_type registered on the homeserver."""
  744. def _count_users(txn: LoggingTransaction) -> int:
  745. txn.execute("SELECT COUNT(*) AS users FROM users where user_type is null")
  746. rows = self.db_pool.cursor_to_dict(txn)
  747. if rows:
  748. return rows[0]["users"]
  749. return 0
  750. return await self.db_pool.runInteraction("count_real_users", _count_users)
  751. async def generate_user_id(self) -> str:
  752. """Generate a suitable localpart for a guest user
  753. Returns: a (hopefully) free localpart
  754. """
  755. next_id = await self.db_pool.runInteraction(
  756. "generate_user_id", self._user_id_seq.get_next_id_txn
  757. )
  758. return str(next_id)
  759. async def get_user_id_by_threepid(self, medium: str, address: str) -> Optional[str]:
  760. """Returns user id from threepid
  761. Args:
  762. medium: threepid medium e.g. email
  763. address: threepid address e.g. me@example.com. This must already be
  764. in canonical form.
  765. Returns:
  766. The user ID or None if no user id/threepid mapping exists
  767. """
  768. user_id = await self.db_pool.runInteraction(
  769. "get_user_id_by_threepid", self.get_user_id_by_threepid_txn, medium, address
  770. )
  771. return user_id
  772. def get_user_id_by_threepid_txn(
  773. self, txn: LoggingTransaction, medium: str, address: str
  774. ) -> Optional[str]:
  775. """Returns user id from threepid
  776. Args:
  777. txn:
  778. medium: threepid medium e.g. email
  779. address: threepid address e.g. me@example.com
  780. Returns:
  781. user id, or None if no user id/threepid mapping exists
  782. """
  783. ret = self.db_pool.simple_select_one_txn(
  784. txn,
  785. "user_threepids",
  786. {"medium": medium, "address": address},
  787. ["user_id"],
  788. True,
  789. )
  790. if ret:
  791. return ret["user_id"]
  792. return None
  793. async def user_add_threepid(
  794. self,
  795. user_id: str,
  796. medium: str,
  797. address: str,
  798. validated_at: int,
  799. added_at: int,
  800. ) -> None:
  801. await self.db_pool.simple_upsert(
  802. "user_threepids",
  803. {"medium": medium, "address": address},
  804. {"user_id": user_id, "validated_at": validated_at, "added_at": added_at},
  805. )
  806. async def user_get_threepids(self, user_id: str) -> List[Dict[str, Any]]:
  807. return await self.db_pool.simple_select_list(
  808. "user_threepids",
  809. {"user_id": user_id},
  810. ["medium", "address", "validated_at", "added_at"],
  811. "user_get_threepids",
  812. )
  813. async def user_delete_threepid(
  814. self, user_id: str, medium: str, address: str
  815. ) -> None:
  816. await self.db_pool.simple_delete(
  817. "user_threepids",
  818. keyvalues={"user_id": user_id, "medium": medium, "address": address},
  819. desc="user_delete_threepid",
  820. )
  821. async def add_user_bound_threepid(
  822. self, user_id: str, medium: str, address: str, id_server: str
  823. ) -> None:
  824. """The server proxied a bind request to the given identity server on
  825. behalf of the given user. We need to remember this in case the user
  826. asks us to unbind the threepid.
  827. Args:
  828. user_id
  829. medium
  830. address
  831. id_server
  832. """
  833. # We need to use an upsert, in case they user had already bound the
  834. # threepid
  835. await self.db_pool.simple_upsert(
  836. table="user_threepid_id_server",
  837. keyvalues={
  838. "user_id": user_id,
  839. "medium": medium,
  840. "address": address,
  841. "id_server": id_server,
  842. },
  843. values={},
  844. insertion_values={},
  845. desc="add_user_bound_threepid",
  846. )
  847. async def user_get_bound_threepids(self, user_id: str) -> List[Dict[str, Any]]:
  848. """Get the threepids that a user has bound to an identity server through the homeserver
  849. The homeserver remembers where binds to an identity server occurred. Using this
  850. method can retrieve those threepids.
  851. Args:
  852. user_id: The ID of the user to retrieve threepids for
  853. Returns:
  854. List of dictionaries containing the following keys:
  855. medium (str): The medium of the threepid (e.g "email")
  856. address (str): The address of the threepid (e.g "bob@example.com")
  857. """
  858. return await self.db_pool.simple_select_list(
  859. table="user_threepid_id_server",
  860. keyvalues={"user_id": user_id},
  861. retcols=["medium", "address"],
  862. desc="user_get_bound_threepids",
  863. )
  864. async def remove_user_bound_threepid(
  865. self, user_id: str, medium: str, address: str, id_server: str
  866. ) -> None:
  867. """The server proxied an unbind request to the given identity server on
  868. behalf of the given user, so we remove the mapping of threepid to
  869. identity server.
  870. Args:
  871. user_id
  872. medium
  873. address
  874. id_server
  875. """
  876. await self.db_pool.simple_delete(
  877. table="user_threepid_id_server",
  878. keyvalues={
  879. "user_id": user_id,
  880. "medium": medium,
  881. "address": address,
  882. "id_server": id_server,
  883. },
  884. desc="remove_user_bound_threepid",
  885. )
  886. async def get_id_servers_user_bound(
  887. self, user_id: str, medium: str, address: str
  888. ) -> List[str]:
  889. """Get the list of identity servers that the server proxied bind
  890. requests to for given user and threepid
  891. Args:
  892. user_id: The user to query for identity servers.
  893. medium: The medium to query for identity servers.
  894. address: The address to query for identity servers.
  895. Returns:
  896. A list of identity servers
  897. """
  898. return await self.db_pool.simple_select_onecol(
  899. table="user_threepid_id_server",
  900. keyvalues={"user_id": user_id, "medium": medium, "address": address},
  901. retcol="id_server",
  902. desc="get_id_servers_user_bound",
  903. )
  904. @cached()
  905. async def get_user_deactivated_status(self, user_id: str) -> bool:
  906. """Retrieve the value for the `deactivated` property for the provided user.
  907. Args:
  908. user_id: The ID of the user to retrieve the status for.
  909. Returns:
  910. True if the user was deactivated, false if the user is still active.
  911. """
  912. res = await self.db_pool.simple_select_one_onecol(
  913. table="users",
  914. keyvalues={"name": user_id},
  915. retcol="deactivated",
  916. desc="get_user_deactivated_status",
  917. )
  918. # Convert the integer into a boolean.
  919. return res == 1
  920. @cached()
  921. async def get_user_locked_status(self, user_id: str) -> bool:
  922. """Retrieve the value for the `locked` property for the provided user.
  923. Args:
  924. user_id: The ID of the user to retrieve the status for.
  925. Returns:
  926. True if the user was locked, false if the user is still active.
  927. """
  928. res = await self.db_pool.simple_select_one_onecol(
  929. table="users",
  930. keyvalues={"name": user_id},
  931. retcol="locked",
  932. desc="get_user_locked_status",
  933. )
  934. # Convert the potential integer into a boolean.
  935. return bool(res)
  936. async def get_threepid_validation_session(
  937. self,
  938. medium: Optional[str],
  939. client_secret: str,
  940. address: Optional[str] = None,
  941. sid: Optional[str] = None,
  942. validated: Optional[bool] = True,
  943. ) -> Optional[Dict[str, Any]]:
  944. """Gets a session_id and last_send_attempt (if available) for a
  945. combination of validation metadata
  946. Args:
  947. medium: The medium of the 3PID
  948. client_secret: A unique string provided by the client to help identify this
  949. validation attempt
  950. address: The address of the 3PID
  951. sid: The ID of the validation session
  952. validated: Whether sessions should be filtered by
  953. whether they have been validated already or not. None to
  954. perform no filtering
  955. Returns:
  956. A dict containing the following:
  957. * address - address of the 3pid
  958. * medium - medium of the 3pid
  959. * client_secret - a secret provided by the client for this validation session
  960. * session_id - ID of the validation session
  961. * send_attempt - a number serving to dedupe send attempts for this session
  962. * validated_at - timestamp of when this session was validated if so
  963. Otherwise None if a validation session is not found
  964. """
  965. if not client_secret:
  966. raise SynapseError(
  967. 400, "Missing parameter: client_secret", errcode=Codes.MISSING_PARAM
  968. )
  969. keyvalues = {"client_secret": client_secret}
  970. if medium:
  971. keyvalues["medium"] = medium
  972. if address:
  973. keyvalues["address"] = address
  974. if sid:
  975. keyvalues["session_id"] = sid
  976. assert address or sid
  977. def get_threepid_validation_session_txn(
  978. txn: LoggingTransaction,
  979. ) -> Optional[Dict[str, Any]]:
  980. sql = """
  981. SELECT address, session_id, medium, client_secret,
  982. last_send_attempt, validated_at
  983. FROM threepid_validation_session WHERE %s
  984. """ % (
  985. " AND ".join("%s = ?" % k for k in keyvalues.keys()),
  986. )
  987. if validated is not None:
  988. sql += " AND validated_at IS " + ("NOT NULL" if validated else "NULL")
  989. sql += " LIMIT 1"
  990. txn.execute(sql, list(keyvalues.values()))
  991. rows = self.db_pool.cursor_to_dict(txn)
  992. if not rows:
  993. return None
  994. return rows[0]
  995. return await self.db_pool.runInteraction(
  996. "get_threepid_validation_session", get_threepid_validation_session_txn
  997. )
  998. async def delete_threepid_session(self, session_id: str) -> None:
  999. """Removes a threepid validation session from the database. This can
  1000. be done after validation has been performed and whatever action was
  1001. waiting on it has been carried out
  1002. Args:
  1003. session_id: The ID of the session to delete
  1004. """
  1005. def delete_threepid_session_txn(txn: LoggingTransaction) -> None:
  1006. self.db_pool.simple_delete_txn(
  1007. txn,
  1008. table="threepid_validation_token",
  1009. keyvalues={"session_id": session_id},
  1010. )
  1011. self.db_pool.simple_delete_txn(
  1012. txn,
  1013. table="threepid_validation_session",
  1014. keyvalues={"session_id": session_id},
  1015. )
  1016. await self.db_pool.runInteraction(
  1017. "delete_threepid_session", delete_threepid_session_txn
  1018. )
  1019. @wrap_as_background_process("cull_expired_threepid_validation_tokens")
  1020. async def cull_expired_threepid_validation_tokens(self) -> None:
  1021. """Remove threepid validation tokens with expiry dates that have passed"""
  1022. def cull_expired_threepid_validation_tokens_txn(
  1023. txn: LoggingTransaction, ts: int
  1024. ) -> None:
  1025. sql = """
  1026. DELETE FROM threepid_validation_token WHERE
  1027. expires < ?
  1028. """
  1029. txn.execute(sql, (ts,))
  1030. await self.db_pool.runInteraction(
  1031. "cull_expired_threepid_validation_tokens",
  1032. cull_expired_threepid_validation_tokens_txn,
  1033. self._clock.time_msec(),
  1034. )
  1035. @wrap_as_background_process("account_validity_set_expiration_dates")
  1036. async def _set_expiration_date_when_missing(self) -> None:
  1037. """
  1038. Retrieves the list of registered users that don't have an expiration date, and
  1039. adds an expiration date for each of them.
  1040. """
  1041. def select_users_with_no_expiration_date_txn(txn: LoggingTransaction) -> None:
  1042. """Retrieves the list of registered users with no expiration date from the
  1043. database, filtering out deactivated users.
  1044. """
  1045. sql = (
  1046. "SELECT users.name FROM users"
  1047. " LEFT JOIN account_validity ON (users.name = account_validity.user_id)"
  1048. " WHERE account_validity.user_id is NULL AND users.deactivated = 0;"
  1049. )
  1050. txn.execute(sql, [])
  1051. res = self.db_pool.cursor_to_dict(txn)
  1052. if res:
  1053. for user in res:
  1054. self.set_expiration_date_for_user_txn(
  1055. txn, user["name"], use_delta=True
  1056. )
  1057. await self.db_pool.runInteraction(
  1058. "get_users_with_no_expiration_date",
  1059. select_users_with_no_expiration_date_txn,
  1060. )
  1061. def set_expiration_date_for_user_txn(
  1062. self, txn: LoggingTransaction, user_id: str, use_delta: bool = False
  1063. ) -> None:
  1064. """Sets an expiration date to the account with the given user ID.
  1065. Args:
  1066. user_id: User ID to set an expiration date for.
  1067. use_delta: If set to False, the expiration date for the user will be
  1068. now + validity period. If set to True, this expiration date will be a
  1069. random value in the [now + period - d ; now + period] range, d being a
  1070. delta equal to 10% of the validity period.
  1071. """
  1072. now_ms = self._clock.time_msec()
  1073. assert self._account_validity_period is not None
  1074. expiration_ts = now_ms + self._account_validity_period
  1075. if use_delta:
  1076. assert self._account_validity_startup_job_max_delta is not None
  1077. expiration_ts = random.randrange(
  1078. int(expiration_ts - self._account_validity_startup_job_max_delta),
  1079. expiration_ts,
  1080. )
  1081. self.db_pool.simple_upsert_txn(
  1082. txn,
  1083. "account_validity",
  1084. keyvalues={"user_id": user_id},
  1085. values={"expiration_ts_ms": expiration_ts, "email_sent": False},
  1086. )
  1087. async def get_user_pending_deactivation(self) -> Optional[str]:
  1088. """
  1089. Gets one user from the table of users waiting to be parted from all the rooms
  1090. they're in.
  1091. """
  1092. return await self.db_pool.simple_select_one_onecol(
  1093. "users_pending_deactivation",
  1094. keyvalues={},
  1095. retcol="user_id",
  1096. allow_none=True,
  1097. desc="get_users_pending_deactivation",
  1098. )
  1099. async def del_user_pending_deactivation(self, user_id: str) -> None:
  1100. """
  1101. Removes the given user to the table of users who need to be parted from all the
  1102. rooms they're in, effectively marking that user as fully deactivated.
  1103. """
  1104. # XXX: This should be simple_delete_one but we failed to put a unique index on
  1105. # the table, so somehow duplicate entries have ended up in it.
  1106. await self.db_pool.simple_delete(
  1107. "users_pending_deactivation",
  1108. keyvalues={"user_id": user_id},
  1109. desc="del_user_pending_deactivation",
  1110. )
  1111. async def get_access_token_last_validated(self, token_id: int) -> int:
  1112. """Retrieves the time (in milliseconds) of the last validation of an access token.
  1113. Args:
  1114. token_id: The ID of the access token to update.
  1115. Raises:
  1116. StoreError if the access token was not found.
  1117. Returns:
  1118. The last validation time.
  1119. """
  1120. result = await self.db_pool.simple_select_one_onecol(
  1121. "access_tokens", {"id": token_id}, "last_validated"
  1122. )
  1123. # If this token has not been validated (since starting to track this),
  1124. # return 0 instead of None.
  1125. return result or 0
  1126. async def update_access_token_last_validated(self, token_id: int) -> None:
  1127. """Updates the last time an access token was validated.
  1128. Args:
  1129. token_id: The ID of the access token to update.
  1130. Raises:
  1131. StoreError if there was a problem updating this.
  1132. """
  1133. now = self._clock.time_msec()
  1134. await self.db_pool.simple_update_one(
  1135. "access_tokens",
  1136. {"id": token_id},
  1137. {"last_validated": now},
  1138. desc="update_access_token_last_validated",
  1139. )
  1140. async def registration_token_is_valid(self, token: str) -> bool:
  1141. """Checks if a token can be used to authenticate a registration.
  1142. Args:
  1143. token: The registration token to be checked
  1144. Returns:
  1145. True if the token is valid, False otherwise.
  1146. """
  1147. res = await self.db_pool.simple_select_one(
  1148. "registration_tokens",
  1149. keyvalues={"token": token},
  1150. retcols=["uses_allowed", "pending", "completed", "expiry_time"],
  1151. allow_none=True,
  1152. )
  1153. # Check if the token exists
  1154. if res is None:
  1155. return False
  1156. # Check if the token has expired
  1157. now = self._clock.time_msec()
  1158. if res["expiry_time"] and res["expiry_time"] < now:
  1159. return False
  1160. # Check if the token has been used up
  1161. if (
  1162. res["uses_allowed"]
  1163. and res["pending"] + res["completed"] >= res["uses_allowed"]
  1164. ):
  1165. return False
  1166. # Otherwise, the token is valid
  1167. return True
  1168. async def set_registration_token_pending(self, token: str) -> None:
  1169. """Increment the pending registrations counter for a token.
  1170. Args:
  1171. token: The registration token pending use
  1172. """
  1173. def _set_registration_token_pending_txn(txn: LoggingTransaction) -> None:
  1174. pending = self.db_pool.simple_select_one_onecol_txn(
  1175. txn,
  1176. "registration_tokens",
  1177. keyvalues={"token": token},
  1178. retcol="pending",
  1179. )
  1180. self.db_pool.simple_update_one_txn(
  1181. txn,
  1182. "registration_tokens",
  1183. keyvalues={"token": token},
  1184. updatevalues={"pending": pending + 1},
  1185. )
  1186. await self.db_pool.runInteraction(
  1187. "set_registration_token_pending", _set_registration_token_pending_txn
  1188. )
  1189. async def use_registration_token(self, token: str) -> None:
  1190. """Complete a use of the given registration token.
  1191. The `pending` counter will be decremented, and the `completed`
  1192. counter will be incremented.
  1193. Args:
  1194. token: The registration token to be 'used'
  1195. """
  1196. def _use_registration_token_txn(txn: LoggingTransaction) -> None:
  1197. # Normally, res is Optional[Dict[str, Any]].
  1198. # Override type because the return type is only optional if
  1199. # allow_none is True, and we don't want mypy throwing errors
  1200. # about None not being indexable.
  1201. res = cast(
  1202. Dict[str, Any],
  1203. self.db_pool.simple_select_one_txn(
  1204. txn,
  1205. "registration_tokens",
  1206. keyvalues={"token": token},
  1207. retcols=["pending", "completed"],
  1208. ),
  1209. )
  1210. # Decrement pending and increment completed
  1211. self.db_pool.simple_update_one_txn(
  1212. txn,
  1213. "registration_tokens",
  1214. keyvalues={"token": token},
  1215. updatevalues={
  1216. "completed": res["completed"] + 1,
  1217. "pending": res["pending"] - 1,
  1218. },
  1219. )
  1220. await self.db_pool.runInteraction(
  1221. "use_registration_token", _use_registration_token_txn
  1222. )
  1223. async def get_registration_tokens(
  1224. self, valid: Optional[bool] = None
  1225. ) -> List[Dict[str, Any]]:
  1226. """List all registration tokens. Used by the admin API.
  1227. Args:
  1228. valid: If True, only valid tokens are returned.
  1229. If False, only invalid tokens are returned.
  1230. Default is None: return all tokens regardless of validity.
  1231. Returns:
  1232. A list of dicts, each containing details of a token.
  1233. """
  1234. def select_registration_tokens_txn(
  1235. txn: LoggingTransaction, now: int, valid: Optional[bool]
  1236. ) -> List[Dict[str, Any]]:
  1237. if valid is None:
  1238. # Return all tokens regardless of validity
  1239. txn.execute("SELECT * FROM registration_tokens")
  1240. elif valid:
  1241. # Select valid tokens only
  1242. sql = (
  1243. "SELECT * FROM registration_tokens WHERE "
  1244. "(uses_allowed > pending + completed OR uses_allowed IS NULL) "
  1245. "AND (expiry_time > ? OR expiry_time IS NULL)"
  1246. )
  1247. txn.execute(sql, [now])
  1248. else:
  1249. # Select invalid tokens only
  1250. sql = (
  1251. "SELECT * FROM registration_tokens WHERE "
  1252. "uses_allowed <= pending + completed OR expiry_time <= ?"
  1253. )
  1254. txn.execute(sql, [now])
  1255. return self.db_pool.cursor_to_dict(txn)
  1256. return await self.db_pool.runInteraction(
  1257. "select_registration_tokens",
  1258. select_registration_tokens_txn,
  1259. self._clock.time_msec(),
  1260. valid,
  1261. )
  1262. async def get_one_registration_token(self, token: str) -> Optional[Dict[str, Any]]:
  1263. """Get info about the given registration token. Used by the admin API.
  1264. Args:
  1265. token: The token to retrieve information about.
  1266. Returns:
  1267. A dict, or None if token doesn't exist.
  1268. """
  1269. return await self.db_pool.simple_select_one(
  1270. "registration_tokens",
  1271. keyvalues={"token": token},
  1272. retcols=["token", "uses_allowed", "pending", "completed", "expiry_time"],
  1273. allow_none=True,
  1274. desc="get_one_registration_token",
  1275. )
  1276. async def generate_registration_token(
  1277. self, length: int, chars: str
  1278. ) -> Optional[str]:
  1279. """Generate a random registration token. Used by the admin API.
  1280. Args:
  1281. length: The length of the token to generate.
  1282. chars: A string of the characters allowed in the generated token.
  1283. Returns:
  1284. The generated token.
  1285. Raises:
  1286. SynapseError if a unique registration token could still not be
  1287. generated after a few tries.
  1288. """
  1289. # Make a few attempts at generating a unique token of the required
  1290. # length before failing.
  1291. for _i in range(3):
  1292. # Generate token
  1293. token = "".join(random.choices(chars, k=length))
  1294. # Check if the token already exists
  1295. existing_token = await self.db_pool.simple_select_one_onecol(
  1296. "registration_tokens",
  1297. keyvalues={"token": token},
  1298. retcol="token",
  1299. allow_none=True,
  1300. desc="check_if_registration_token_exists",
  1301. )
  1302. if existing_token is None:
  1303. # The generated token doesn't exist yet, return it
  1304. return token
  1305. raise SynapseError(
  1306. 500,
  1307. "Unable to generate a unique registration token. Try again with a greater length",
  1308. Codes.UNKNOWN,
  1309. )
  1310. async def create_registration_token(
  1311. self, token: str, uses_allowed: Optional[int], expiry_time: Optional[int]
  1312. ) -> bool:
  1313. """Create a new registration token. Used by the admin API.
  1314. Args:
  1315. token: The token to create.
  1316. uses_allowed: The number of times the token can be used to complete
  1317. a registration before it becomes invalid. A value of None indicates
  1318. unlimited uses.
  1319. expiry_time: The latest time the token is valid. Given as the
  1320. number of milliseconds since 1970-01-01 00:00:00 UTC. A value of
  1321. None indicates that the token does not expire.
  1322. Returns:
  1323. Whether the row was inserted or not.
  1324. """
  1325. def _create_registration_token_txn(txn: LoggingTransaction) -> bool:
  1326. row = self.db_pool.simple_select_one_txn(
  1327. txn,
  1328. "registration_tokens",
  1329. keyvalues={"token": token},
  1330. retcols=["token"],
  1331. allow_none=True,
  1332. )
  1333. if row is not None:
  1334. # Token already exists
  1335. return False
  1336. self.db_pool.simple_insert_txn(
  1337. txn,
  1338. "registration_tokens",
  1339. values={
  1340. "token": token,
  1341. "uses_allowed": uses_allowed,
  1342. "pending": 0,
  1343. "completed": 0,
  1344. "expiry_time": expiry_time,
  1345. },
  1346. )
  1347. return True
  1348. return await self.db_pool.runInteraction(
  1349. "create_registration_token", _create_registration_token_txn
  1350. )
  1351. async def update_registration_token(
  1352. self, token: str, updatevalues: Dict[str, Optional[int]]
  1353. ) -> Optional[Dict[str, Any]]:
  1354. """Update a registration token. Used by the admin API.
  1355. Args:
  1356. token: The token to update.
  1357. updatevalues: A dict with the fields to update. E.g.:
  1358. `{"uses_allowed": 3}` to update just uses_allowed, or
  1359. `{"uses_allowed": 3, "expiry_time": None}` to update both.
  1360. This is passed straight to simple_update_one.
  1361. Returns:
  1362. A dict with all info about the token, or None if token doesn't exist.
  1363. """
  1364. def _update_registration_token_txn(
  1365. txn: LoggingTransaction,
  1366. ) -> Optional[Dict[str, Any]]:
  1367. try:
  1368. self.db_pool.simple_update_one_txn(
  1369. txn,
  1370. "registration_tokens",
  1371. keyvalues={"token": token},
  1372. updatevalues=updatevalues,
  1373. )
  1374. except StoreError:
  1375. # Update failed because token does not exist
  1376. return None
  1377. # Get all info about the token so it can be sent in the response
  1378. return self.db_pool.simple_select_one_txn(
  1379. txn,
  1380. "registration_tokens",
  1381. keyvalues={"token": token},
  1382. retcols=[
  1383. "token",
  1384. "uses_allowed",
  1385. "pending",
  1386. "completed",
  1387. "expiry_time",
  1388. ],
  1389. allow_none=True,
  1390. )
  1391. return await self.db_pool.runInteraction(
  1392. "update_registration_token", _update_registration_token_txn
  1393. )
  1394. async def delete_registration_token(self, token: str) -> bool:
  1395. """Delete a registration token. Used by the admin API.
  1396. Args:
  1397. token: The token to delete.
  1398. Returns:
  1399. Whether the token was successfully deleted or not.
  1400. """
  1401. try:
  1402. await self.db_pool.simple_delete_one(
  1403. "registration_tokens",
  1404. keyvalues={"token": token},
  1405. desc="delete_registration_token",
  1406. )
  1407. except StoreError:
  1408. # Deletion failed because token does not exist
  1409. return False
  1410. return True
  1411. @cached()
  1412. async def mark_access_token_as_used(self, token_id: int) -> None:
  1413. """
  1414. Mark the access token as used, which invalidates the refresh token used
  1415. to obtain it.
  1416. Because get_user_by_access_token is cached, this function might be
  1417. called multiple times for the same token, effectively doing unnecessary
  1418. SQL updates. Because updating the `used` field only goes one way (from
  1419. False to True) it is safe to cache this function as well to avoid this
  1420. issue.
  1421. Args:
  1422. token_id: The ID of the access token to update.
  1423. Raises:
  1424. StoreError if there was a problem updating this.
  1425. """
  1426. await self.db_pool.simple_update_one(
  1427. "access_tokens",
  1428. {"id": token_id},
  1429. {"used": True},
  1430. desc="mark_access_token_as_used",
  1431. )
  1432. async def lookup_refresh_token(
  1433. self, token: str
  1434. ) -> Optional[RefreshTokenLookupResult]:
  1435. """Lookup a refresh token with hints about its validity."""
  1436. def _lookup_refresh_token_txn(
  1437. txn: LoggingTransaction,
  1438. ) -> Optional[RefreshTokenLookupResult]:
  1439. txn.execute(
  1440. """
  1441. SELECT
  1442. rt.id token_id,
  1443. rt.user_id,
  1444. rt.device_id,
  1445. rt.next_token_id,
  1446. (nrt.next_token_id IS NOT NULL) AS has_next_refresh_token_been_refreshed,
  1447. at.used AS has_next_access_token_been_used,
  1448. rt.expiry_ts,
  1449. rt.ultimate_session_expiry_ts
  1450. FROM refresh_tokens rt
  1451. LEFT JOIN refresh_tokens nrt ON rt.next_token_id = nrt.id
  1452. LEFT JOIN access_tokens at ON at.refresh_token_id = nrt.id
  1453. WHERE rt.token = ?
  1454. """,
  1455. (token,),
  1456. )
  1457. row = txn.fetchone()
  1458. if row is None:
  1459. return None
  1460. return RefreshTokenLookupResult(
  1461. token_id=row[0],
  1462. user_id=row[1],
  1463. device_id=row[2],
  1464. next_token_id=row[3],
  1465. # SQLite returns 0 or 1 for false/true, so convert to a bool.
  1466. has_next_refresh_token_been_refreshed=bool(row[4]),
  1467. # This column is nullable, ensure it's a boolean
  1468. has_next_access_token_been_used=(row[5] or False),
  1469. expiry_ts=row[6],
  1470. ultimate_session_expiry_ts=row[7],
  1471. )
  1472. return await self.db_pool.runInteraction(
  1473. "lookup_refresh_token", _lookup_refresh_token_txn
  1474. )
  1475. async def replace_refresh_token(self, token_id: int, next_token_id: int) -> None:
  1476. """
  1477. Set the successor of a refresh token, removing the existing successor
  1478. if any.
  1479. This also deletes the predecessor refresh and access tokens,
  1480. since they cannot be valid anymore.
  1481. Args:
  1482. token_id: ID of the refresh token to update.
  1483. next_token_id: ID of its successor.
  1484. """
  1485. def _replace_refresh_token_txn(txn: LoggingTransaction) -> None:
  1486. # First check if there was an existing refresh token
  1487. old_next_token_id = self.db_pool.simple_select_one_onecol_txn(
  1488. txn,
  1489. "refresh_tokens",
  1490. {"id": token_id},
  1491. "next_token_id",
  1492. allow_none=True,
  1493. )
  1494. self.db_pool.simple_update_one_txn(
  1495. txn,
  1496. "refresh_tokens",
  1497. {"id": token_id},
  1498. {"next_token_id": next_token_id},
  1499. )
  1500. # Delete the old "next" token if it exists. This should cascade and
  1501. # delete the associated access_token
  1502. if old_next_token_id is not None:
  1503. self.db_pool.simple_delete_one_txn(
  1504. txn,
  1505. "refresh_tokens",
  1506. {"id": old_next_token_id},
  1507. )
  1508. # Delete the previous refresh token, since we only want to keep the
  1509. # last 2 refresh tokens in the database.
  1510. # (The predecessor of the latest refresh token is still useful in
  1511. # case the refresh was interrupted and the client re-uses the old
  1512. # one.)
  1513. # This cascades to delete the associated access token.
  1514. self.db_pool.simple_delete_txn(
  1515. txn, "refresh_tokens", {"next_token_id": token_id}
  1516. )
  1517. await self.db_pool.runInteraction(
  1518. "replace_refresh_token", _replace_refresh_token_txn
  1519. )
  1520. async def add_login_token_to_user(
  1521. self,
  1522. user_id: str,
  1523. token: str,
  1524. expiry_ts: int,
  1525. auth_provider_id: Optional[str],
  1526. auth_provider_session_id: Optional[str],
  1527. ) -> None:
  1528. """Adds a short-term login token for the given user.
  1529. Args:
  1530. user_id: The user ID.
  1531. token: The new login token to add.
  1532. expiry_ts (milliseconds since the epoch): Time after which the login token
  1533. cannot be used.
  1534. auth_provider_id: The SSO Identity Provider that the user authenticated with
  1535. to get this token, if any
  1536. auth_provider_session_id: The session ID advertised by the SSO Identity
  1537. Provider, if any.
  1538. """
  1539. await self.db_pool.simple_insert(
  1540. "login_tokens",
  1541. {
  1542. "token": token,
  1543. "user_id": user_id,
  1544. "expiry_ts": expiry_ts,
  1545. "auth_provider_id": auth_provider_id,
  1546. "auth_provider_session_id": auth_provider_session_id,
  1547. },
  1548. desc="add_login_token_to_user",
  1549. )
  1550. def _consume_login_token(
  1551. self,
  1552. txn: LoggingTransaction,
  1553. token: str,
  1554. ts: int,
  1555. ) -> LoginTokenLookupResult:
  1556. values = self.db_pool.simple_select_one_txn(
  1557. txn,
  1558. "login_tokens",
  1559. keyvalues={"token": token},
  1560. retcols=(
  1561. "user_id",
  1562. "expiry_ts",
  1563. "used_ts",
  1564. "auth_provider_id",
  1565. "auth_provider_session_id",
  1566. ),
  1567. allow_none=True,
  1568. )
  1569. if values is None:
  1570. raise NotFoundError()
  1571. self.db_pool.simple_update_one_txn(
  1572. txn,
  1573. "login_tokens",
  1574. keyvalues={"token": token},
  1575. updatevalues={"used_ts": ts},
  1576. )
  1577. user_id = values["user_id"]
  1578. expiry_ts = values["expiry_ts"]
  1579. used_ts = values["used_ts"]
  1580. auth_provider_id = values["auth_provider_id"]
  1581. auth_provider_session_id = values["auth_provider_session_id"]
  1582. # Token was already used
  1583. if used_ts is not None:
  1584. raise LoginTokenReused()
  1585. # Token expired
  1586. if ts > int(expiry_ts):
  1587. raise LoginTokenExpired()
  1588. return LoginTokenLookupResult(
  1589. user_id=user_id,
  1590. auth_provider_id=auth_provider_id,
  1591. auth_provider_session_id=auth_provider_session_id,
  1592. )
  1593. async def consume_login_token(self, token: str) -> LoginTokenLookupResult:
  1594. """Lookup a login token and consume it.
  1595. Args:
  1596. token: The login token.
  1597. Returns:
  1598. The data stored with that token, including the `user_id`. Returns `None` if
  1599. the token does not exist or if it expired.
  1600. Raises:
  1601. NotFound if the login token was not found in database
  1602. LoginTokenExpired if the login token expired
  1603. LoginTokenReused if the login token was already used
  1604. """
  1605. return await self.db_pool.runInteraction(
  1606. "consume_login_token",
  1607. self._consume_login_token,
  1608. token,
  1609. self._clock.time_msec(),
  1610. )
  1611. async def invalidate_login_tokens_by_session_id(
  1612. self, auth_provider_id: str, auth_provider_session_id: str
  1613. ) -> None:
  1614. """Invalidate login tokens with the given IdP session ID.
  1615. Args:
  1616. auth_provider_id: The SSO Identity Provider that the user authenticated with
  1617. to get this token
  1618. auth_provider_session_id: The session ID advertised by the SSO Identity
  1619. Provider
  1620. """
  1621. await self.db_pool.simple_update(
  1622. table="login_tokens",
  1623. keyvalues={
  1624. "auth_provider_id": auth_provider_id,
  1625. "auth_provider_session_id": auth_provider_session_id,
  1626. },
  1627. updatevalues={"used_ts": self._clock.time_msec()},
  1628. desc="invalidate_login_tokens_by_session_id",
  1629. )
  1630. @cached()
  1631. async def is_guest(self, user_id: str) -> bool:
  1632. res = await self.db_pool.simple_select_one_onecol(
  1633. table="users",
  1634. keyvalues={"name": user_id},
  1635. retcol="is_guest",
  1636. allow_none=True,
  1637. desc="is_guest",
  1638. )
  1639. return res if res else False
  1640. @cached()
  1641. async def is_user_approved(self, user_id: str) -> bool:
  1642. """Checks if a user is approved and therefore can be allowed to log in.
  1643. If the user's 'approved' column is NULL, we consider it as true given it means
  1644. the user was registered when support for an approval flow was either disabled
  1645. or nonexistent.
  1646. Args:
  1647. user_id: the user to check the approval status of.
  1648. Returns:
  1649. A boolean that is True if the user is approved, False otherwise.
  1650. """
  1651. def is_user_approved_txn(txn: LoggingTransaction) -> bool:
  1652. txn.execute(
  1653. """
  1654. SELECT COALESCE(approved, TRUE) AS approved FROM users WHERE name = ?
  1655. """,
  1656. (user_id,),
  1657. )
  1658. rows = self.db_pool.cursor_to_dict(txn)
  1659. # We cast to bool because the value returned by the database engine might
  1660. # be an integer if we're using SQLite.
  1661. return bool(rows[0]["approved"])
  1662. return await self.db_pool.runInteraction(
  1663. desc="is_user_pending_approval",
  1664. func=is_user_approved_txn,
  1665. )
  1666. class RegistrationBackgroundUpdateStore(RegistrationWorkerStore):
  1667. def __init__(
  1668. self,
  1669. database: DatabasePool,
  1670. db_conn: LoggingDatabaseConnection,
  1671. hs: "HomeServer",
  1672. ):
  1673. super().__init__(database, db_conn, hs)
  1674. self._clock = hs.get_clock()
  1675. self.config = hs.config
  1676. self.db_pool.updates.register_background_index_update(
  1677. "access_tokens_device_index",
  1678. index_name="access_tokens_device_id",
  1679. table="access_tokens",
  1680. columns=["user_id", "device_id"],
  1681. )
  1682. self.db_pool.updates.register_background_index_update(
  1683. "users_creation_ts",
  1684. index_name="users_creation_ts",
  1685. table="users",
  1686. columns=["creation_ts"],
  1687. )
  1688. self.db_pool.updates.register_background_update_handler(
  1689. "users_set_deactivated_flag", self._background_update_set_deactivated_flag
  1690. )
  1691. self.db_pool.updates.register_background_index_update(
  1692. "user_external_ids_user_id_idx",
  1693. index_name="user_external_ids_user_id_idx",
  1694. table="user_external_ids",
  1695. columns=["user_id"],
  1696. unique=False,
  1697. )
  1698. async def _background_update_set_deactivated_flag(
  1699. self, progress: JsonDict, batch_size: int
  1700. ) -> int:
  1701. """Retrieves a list of all deactivated users and sets the 'deactivated' flag to 1
  1702. for each of them.
  1703. """
  1704. last_user = progress.get("user_id", "")
  1705. def _background_update_set_deactivated_flag_txn(
  1706. txn: LoggingTransaction,
  1707. ) -> Tuple[bool, int]:
  1708. txn.execute(
  1709. """
  1710. SELECT
  1711. users.name,
  1712. COUNT(access_tokens.token) AS count_tokens,
  1713. COUNT(user_threepids.address) AS count_threepids
  1714. FROM users
  1715. LEFT JOIN access_tokens ON (access_tokens.user_id = users.name)
  1716. LEFT JOIN user_threepids ON (user_threepids.user_id = users.name)
  1717. WHERE (users.password_hash IS NULL OR users.password_hash = '')
  1718. AND (users.appservice_id IS NULL OR users.appservice_id = '')
  1719. AND users.is_guest = 0
  1720. AND users.name > ?
  1721. GROUP BY users.name
  1722. ORDER BY users.name ASC
  1723. LIMIT ?;
  1724. """,
  1725. (last_user, batch_size),
  1726. )
  1727. rows = self.db_pool.cursor_to_dict(txn)
  1728. if not rows:
  1729. return True, 0
  1730. rows_processed_nb = 0
  1731. for user in rows:
  1732. if not user["count_tokens"] and not user["count_threepids"]:
  1733. self.set_user_deactivated_status_txn(txn, user["name"], True)
  1734. rows_processed_nb += 1
  1735. logger.info("Marked %d rows as deactivated", rows_processed_nb)
  1736. self.db_pool.updates._background_update_progress_txn(
  1737. txn, "users_set_deactivated_flag", {"user_id": rows[-1]["name"]}
  1738. )
  1739. if batch_size > len(rows):
  1740. return True, len(rows)
  1741. else:
  1742. return False, len(rows)
  1743. end, nb_processed = await self.db_pool.runInteraction(
  1744. "users_set_deactivated_flag", _background_update_set_deactivated_flag_txn
  1745. )
  1746. if end:
  1747. await self.db_pool.updates._end_background_update(
  1748. "users_set_deactivated_flag"
  1749. )
  1750. return nb_processed
  1751. async def set_user_deactivated_status(
  1752. self, user_id: str, deactivated: bool
  1753. ) -> None:
  1754. """Set the `deactivated` property for the provided user to the provided value.
  1755. Args:
  1756. user_id: The ID of the user to set the status for.
  1757. deactivated: The value to set for `deactivated`.
  1758. """
  1759. await self.db_pool.runInteraction(
  1760. "set_user_deactivated_status",
  1761. self.set_user_deactivated_status_txn,
  1762. user_id,
  1763. deactivated,
  1764. )
  1765. def set_user_deactivated_status_txn(
  1766. self, txn: LoggingTransaction, user_id: str, deactivated: bool
  1767. ) -> None:
  1768. self.db_pool.simple_update_one_txn(
  1769. txn=txn,
  1770. table="users",
  1771. keyvalues={"name": user_id},
  1772. updatevalues={"deactivated": 1 if deactivated else 0},
  1773. )
  1774. self._invalidate_cache_and_stream(
  1775. txn, self.get_user_deactivated_status, (user_id,)
  1776. )
  1777. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  1778. txn.call_after(self.is_guest.invalidate, (user_id,))
  1779. async def set_user_locked_status(self, user_id: str, locked: bool) -> None:
  1780. """Set the `locked` property for the provided user to the provided value.
  1781. Args:
  1782. user_id: The ID of the user to set the status for.
  1783. locked: The value to set for `locked`.
  1784. """
  1785. await self.db_pool.runInteraction(
  1786. "set_user_locked_status",
  1787. self.set_user_locked_status_txn,
  1788. user_id,
  1789. locked,
  1790. )
  1791. def set_user_locked_status_txn(
  1792. self, txn: LoggingTransaction, user_id: str, locked: bool
  1793. ) -> None:
  1794. self.db_pool.simple_update_one_txn(
  1795. txn=txn,
  1796. table="users",
  1797. keyvalues={"name": user_id},
  1798. updatevalues={"locked": locked},
  1799. )
  1800. self._invalidate_cache_and_stream(txn, self.get_user_locked_status, (user_id,))
  1801. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  1802. def update_user_approval_status_txn(
  1803. self, txn: LoggingTransaction, user_id: str, approved: bool
  1804. ) -> None:
  1805. """Set the user's 'approved' flag to the given value.
  1806. The boolean is turned into an int because the column is a smallint.
  1807. Args:
  1808. txn: the current database transaction.
  1809. user_id: the user to update the flag for.
  1810. approved: the value to set the flag to.
  1811. """
  1812. self.db_pool.simple_update_one_txn(
  1813. txn=txn,
  1814. table="users",
  1815. keyvalues={"name": user_id},
  1816. updatevalues={"approved": approved},
  1817. )
  1818. # Invalidate the caches of methods that read the value of the 'approved' flag.
  1819. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  1820. self._invalidate_cache_and_stream(txn, self.is_user_approved, (user_id,))
  1821. class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
  1822. def __init__(
  1823. self,
  1824. database: DatabasePool,
  1825. db_conn: LoggingDatabaseConnection,
  1826. hs: "HomeServer",
  1827. ):
  1828. super().__init__(database, db_conn, hs)
  1829. self._ignore_unknown_session_error = (
  1830. hs.config.server.request_token_inhibit_3pid_errors
  1831. )
  1832. self._access_tokens_id_gen = IdGenerator(db_conn, "access_tokens", "id")
  1833. self._refresh_tokens_id_gen = IdGenerator(db_conn, "refresh_tokens", "id")
  1834. # If support for MSC3866 is enabled and configured to require approval for new
  1835. # account, we will create new users with an 'approved' flag set to false.
  1836. self._require_approval = (
  1837. hs.config.experimental.msc3866.enabled
  1838. and hs.config.experimental.msc3866.require_approval_for_new_accounts
  1839. )
  1840. # Create a background job for removing expired login tokens
  1841. if hs.config.worker.run_background_tasks:
  1842. self._clock.looping_call(
  1843. self._delete_expired_login_tokens, THIRTY_MINUTES_IN_MS
  1844. )
  1845. async def add_access_token_to_user(
  1846. self,
  1847. user_id: str,
  1848. token: str,
  1849. device_id: Optional[str],
  1850. valid_until_ms: Optional[int],
  1851. puppets_user_id: Optional[str] = None,
  1852. refresh_token_id: Optional[int] = None,
  1853. ) -> int:
  1854. """Adds an access token for the given user.
  1855. Args:
  1856. user_id: The user ID.
  1857. token: The new access token to add.
  1858. device_id: ID of the device to associate with the access token.
  1859. valid_until_ms: when the token is valid until. None for no expiry.
  1860. puppets_user_id
  1861. refresh_token_id: ID of the refresh token generated alongside this
  1862. access token.
  1863. Raises:
  1864. StoreError if there was a problem adding this.
  1865. Returns:
  1866. The token ID
  1867. """
  1868. next_id = self._access_tokens_id_gen.get_next()
  1869. now = self._clock.time_msec()
  1870. await self.db_pool.simple_insert(
  1871. "access_tokens",
  1872. {
  1873. "id": next_id,
  1874. "user_id": user_id,
  1875. "token": token,
  1876. "device_id": device_id,
  1877. "valid_until_ms": valid_until_ms,
  1878. "puppets_user_id": puppets_user_id,
  1879. "last_validated": now,
  1880. "refresh_token_id": refresh_token_id,
  1881. "used": False,
  1882. },
  1883. desc="add_access_token_to_user",
  1884. )
  1885. return next_id
  1886. async def add_refresh_token_to_user(
  1887. self,
  1888. user_id: str,
  1889. token: str,
  1890. device_id: Optional[str],
  1891. expiry_ts: Optional[int],
  1892. ultimate_session_expiry_ts: Optional[int],
  1893. ) -> int:
  1894. """Adds a refresh token for the given user.
  1895. Args:
  1896. user_id: The user ID.
  1897. token: The new access token to add.
  1898. device_id: ID of the device to associate with the refresh token.
  1899. expiry_ts (milliseconds since the epoch): Time after which the
  1900. refresh token cannot be used.
  1901. If None, the refresh token never expires until it has been used.
  1902. ultimate_session_expiry_ts (milliseconds since the epoch):
  1903. Time at which the session will end and can not be extended any
  1904. further.
  1905. If None, the session can be refreshed indefinitely.
  1906. Raises:
  1907. StoreError if there was a problem adding this.
  1908. Returns:
  1909. The token ID
  1910. """
  1911. next_id = self._refresh_tokens_id_gen.get_next()
  1912. await self.db_pool.simple_insert(
  1913. "refresh_tokens",
  1914. {
  1915. "id": next_id,
  1916. "user_id": user_id,
  1917. "device_id": device_id,
  1918. "token": token,
  1919. "next_token_id": None,
  1920. "expiry_ts": expiry_ts,
  1921. "ultimate_session_expiry_ts": ultimate_session_expiry_ts,
  1922. },
  1923. desc="add_refresh_token_to_user",
  1924. )
  1925. return next_id
  1926. async def set_device_for_refresh_token(
  1927. self, user_id: str, old_device_id: str, device_id: str
  1928. ) -> None:
  1929. """Moves refresh tokens from old device to current device
  1930. Args:
  1931. user_id: The user of the devices.
  1932. old_device_id: The old device.
  1933. device_id: The new device ID.
  1934. Returns:
  1935. None
  1936. """
  1937. await self.db_pool.simple_update(
  1938. "refresh_tokens",
  1939. keyvalues={"user_id": user_id, "device_id": old_device_id},
  1940. updatevalues={"device_id": device_id},
  1941. desc="set_device_for_refresh_token",
  1942. )
  1943. def _set_device_for_access_token_txn(
  1944. self, txn: LoggingTransaction, token: str, device_id: str
  1945. ) -> str:
  1946. old_device_id = self.db_pool.simple_select_one_onecol_txn(
  1947. txn, "access_tokens", {"token": token}, "device_id"
  1948. )
  1949. self.db_pool.simple_update_txn(
  1950. txn, "access_tokens", {"token": token}, {"device_id": device_id}
  1951. )
  1952. self._invalidate_cache_and_stream(txn, self.get_user_by_access_token, (token,))
  1953. return old_device_id
  1954. async def set_device_for_access_token(self, token: str, device_id: str) -> str:
  1955. """Sets the device ID associated with an access token.
  1956. Args:
  1957. token: The access token to modify.
  1958. device_id: The new device ID.
  1959. Returns:
  1960. The old device ID associated with the access token.
  1961. """
  1962. return await self.db_pool.runInteraction(
  1963. "set_device_for_access_token",
  1964. self._set_device_for_access_token_txn,
  1965. token,
  1966. device_id,
  1967. )
  1968. async def register_user(
  1969. self,
  1970. user_id: str,
  1971. password_hash: Optional[str] = None,
  1972. was_guest: bool = False,
  1973. make_guest: bool = False,
  1974. appservice_id: Optional[str] = None,
  1975. create_profile_with_displayname: Optional[str] = None,
  1976. admin: bool = False,
  1977. user_type: Optional[str] = None,
  1978. shadow_banned: bool = False,
  1979. approved: bool = False,
  1980. ) -> None:
  1981. """Attempts to register an account.
  1982. Args:
  1983. user_id: The desired user ID to register.
  1984. password_hash: Optional. The password hash for this user.
  1985. was_guest: Whether this is a guest account being upgraded to a
  1986. non-guest account.
  1987. make_guest: True if the the new user should be guest, false to add a
  1988. regular user account.
  1989. appservice_id: The ID of the appservice registering the user.
  1990. create_profile_with_displayname: Optionally create a profile for
  1991. the user, setting their displayname to the given value
  1992. admin: is an admin user?
  1993. user_type: type of user. One of the values from api.constants.UserTypes,
  1994. or None for a normal user.
  1995. shadow_banned: Whether the user is shadow-banned, i.e. they may be
  1996. told their requests succeeded but we ignore them.
  1997. approved: Whether to consider the user has already been approved by an
  1998. administrator.
  1999. Raises:
  2000. StoreError if the user_id could not be registered.
  2001. """
  2002. await self.db_pool.runInteraction(
  2003. "register_user",
  2004. self._register_user,
  2005. user_id,
  2006. password_hash,
  2007. was_guest,
  2008. make_guest,
  2009. appservice_id,
  2010. create_profile_with_displayname,
  2011. admin,
  2012. user_type,
  2013. shadow_banned,
  2014. approved,
  2015. )
  2016. def _register_user(
  2017. self,
  2018. txn: LoggingTransaction,
  2019. user_id: str,
  2020. password_hash: Optional[str],
  2021. was_guest: bool,
  2022. make_guest: bool,
  2023. appservice_id: Optional[str],
  2024. create_profile_with_displayname: Optional[str],
  2025. admin: bool,
  2026. user_type: Optional[str],
  2027. shadow_banned: bool,
  2028. approved: bool,
  2029. ) -> None:
  2030. user_id_obj = UserID.from_string(user_id)
  2031. now = int(self._clock.time())
  2032. user_approved = approved or not self._require_approval
  2033. try:
  2034. if was_guest:
  2035. # Ensure that the guest user actually exists
  2036. # ``allow_none=False`` makes this raise an exception
  2037. # if the row isn't in the database.
  2038. self.db_pool.simple_select_one_txn(
  2039. txn,
  2040. "users",
  2041. keyvalues={"name": user_id, "is_guest": 1},
  2042. retcols=("name",),
  2043. allow_none=False,
  2044. )
  2045. self.db_pool.simple_update_one_txn(
  2046. txn,
  2047. "users",
  2048. keyvalues={"name": user_id, "is_guest": 1},
  2049. updatevalues={
  2050. "password_hash": password_hash,
  2051. "upgrade_ts": now,
  2052. "is_guest": 1 if make_guest else 0,
  2053. "appservice_id": appservice_id,
  2054. "admin": 1 if admin else 0,
  2055. "user_type": user_type,
  2056. "shadow_banned": shadow_banned,
  2057. "approved": user_approved,
  2058. },
  2059. )
  2060. else:
  2061. self.db_pool.simple_insert_txn(
  2062. txn,
  2063. "users",
  2064. values={
  2065. "name": user_id,
  2066. "password_hash": password_hash,
  2067. "creation_ts": now,
  2068. "is_guest": 1 if make_guest else 0,
  2069. "appservice_id": appservice_id,
  2070. "admin": 1 if admin else 0,
  2071. "user_type": user_type,
  2072. "shadow_banned": shadow_banned,
  2073. "approved": user_approved,
  2074. },
  2075. )
  2076. except self.database_engine.module.IntegrityError:
  2077. raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE)
  2078. if self._account_validity_enabled:
  2079. self.set_expiration_date_for_user_txn(txn, user_id)
  2080. if create_profile_with_displayname:
  2081. # set a default displayname serverside to avoid ugly race
  2082. # between auto-joins and clients trying to set displaynames
  2083. #
  2084. # *obviously* the 'profiles' table uses localpart for user_id
  2085. # while everything else uses the full mxid.
  2086. txn.execute(
  2087. "INSERT INTO profiles(full_user_id, user_id, displayname) VALUES (?,?,?)",
  2088. (user_id, user_id_obj.localpart, create_profile_with_displayname),
  2089. )
  2090. if self.hs.config.stats.stats_enabled:
  2091. # we create a new completed user statistics row
  2092. # we don't strictly need current_token since this user really can't
  2093. # have any state deltas before now (as it is a new user), but still,
  2094. # we include it for completeness.
  2095. current_token = self._get_max_stream_id_in_current_state_deltas_txn(txn)
  2096. self._update_stats_delta_txn(
  2097. txn, now, "user", user_id, {}, complete_with_stream_id=current_token
  2098. )
  2099. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  2100. async def user_set_password_hash(
  2101. self, user_id: str, password_hash: Optional[str]
  2102. ) -> None:
  2103. """
  2104. NB. This does *not* evict any cache because the one use for this
  2105. removes most of the entries subsequently anyway so it would be
  2106. pointless. Use flush_user separately.
  2107. """
  2108. def user_set_password_hash_txn(txn: LoggingTransaction) -> None:
  2109. self.db_pool.simple_update_one_txn(
  2110. txn, "users", {"name": user_id}, {"password_hash": password_hash}
  2111. )
  2112. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  2113. await self.db_pool.runInteraction(
  2114. "user_set_password_hash", user_set_password_hash_txn
  2115. )
  2116. async def user_set_consent_version(
  2117. self, user_id: str, consent_version: str
  2118. ) -> None:
  2119. """Updates the user table to record privacy policy consent
  2120. Args:
  2121. user_id: full mxid of the user to update
  2122. consent_version: version of the policy the user has consented to
  2123. Raises:
  2124. StoreError(404) if user not found
  2125. """
  2126. def f(txn: LoggingTransaction) -> None:
  2127. self.db_pool.simple_update_one_txn(
  2128. txn,
  2129. table="users",
  2130. keyvalues={"name": user_id},
  2131. updatevalues={
  2132. "consent_version": consent_version,
  2133. "consent_ts": self._clock.time_msec(),
  2134. },
  2135. )
  2136. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  2137. await self.db_pool.runInteraction("user_set_consent_version", f)
  2138. async def user_set_consent_server_notice_sent(
  2139. self, user_id: str, consent_version: str
  2140. ) -> None:
  2141. """Updates the user table to record that we have sent the user a server
  2142. notice about privacy policy consent
  2143. Args:
  2144. user_id: full mxid of the user to update
  2145. consent_version: version of the policy we have notified the user about
  2146. Raises:
  2147. StoreError(404) if user not found
  2148. """
  2149. def f(txn: LoggingTransaction) -> None:
  2150. self.db_pool.simple_update_one_txn(
  2151. txn,
  2152. table="users",
  2153. keyvalues={"name": user_id},
  2154. updatevalues={"consent_server_notice_sent": consent_version},
  2155. )
  2156. self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
  2157. await self.db_pool.runInteraction("user_set_consent_server_notice_sent", f)
  2158. async def user_delete_access_tokens(
  2159. self,
  2160. user_id: str,
  2161. except_token_id: Optional[int] = None,
  2162. device_id: Optional[str] = None,
  2163. ) -> List[Tuple[str, int, Optional[str]]]:
  2164. """
  2165. Invalidate access and refresh tokens belonging to a user
  2166. Args:
  2167. user_id: ID of user the tokens belong to
  2168. except_token_id: access_tokens ID which should *not* be deleted
  2169. device_id: ID of device the tokens are associated with.
  2170. If None, tokens associated with any device (or no device) will
  2171. be deleted
  2172. Returns:
  2173. A tuple of (token, token id, device id) for each of the deleted tokens
  2174. """
  2175. def f(txn: LoggingTransaction) -> List[Tuple[str, int, Optional[str]]]:
  2176. keyvalues = {"user_id": user_id}
  2177. if device_id is not None:
  2178. keyvalues["device_id"] = device_id
  2179. items = keyvalues.items()
  2180. where_clause = " AND ".join(k + " = ?" for k, _ in items)
  2181. values: List[Union[str, int]] = [v for _, v in items]
  2182. # Conveniently, refresh_tokens and access_tokens both use the user_id and device_id fields. Only caveat
  2183. # is the `except_token_id` param that is tricky to get right, so for now we're just using the same where
  2184. # clause and values before we handle that. This seems to be only used in the "set password" handler.
  2185. refresh_where_clause = where_clause
  2186. refresh_values = values.copy()
  2187. if except_token_id:
  2188. # TODO: support that for refresh tokens
  2189. where_clause += " AND id != ?"
  2190. values.append(except_token_id)
  2191. txn.execute(
  2192. "SELECT token, id, device_id FROM access_tokens WHERE %s"
  2193. % where_clause,
  2194. values,
  2195. )
  2196. tokens_and_devices = [(r[0], r[1], r[2]) for r in txn]
  2197. for token, _, _ in tokens_and_devices:
  2198. self._invalidate_cache_and_stream(
  2199. txn, self.get_user_by_access_token, (token,)
  2200. )
  2201. txn.execute("DELETE FROM access_tokens WHERE %s" % where_clause, values)
  2202. txn.execute(
  2203. "DELETE FROM refresh_tokens WHERE %s" % refresh_where_clause,
  2204. refresh_values,
  2205. )
  2206. return tokens_and_devices
  2207. return await self.db_pool.runInteraction("user_delete_access_tokens", f)
  2208. async def delete_access_token(self, access_token: str) -> None:
  2209. def f(txn: LoggingTransaction) -> None:
  2210. self.db_pool.simple_delete_one_txn(
  2211. txn, table="access_tokens", keyvalues={"token": access_token}
  2212. )
  2213. self._invalidate_cache_and_stream(
  2214. txn, self.get_user_by_access_token, (access_token,)
  2215. )
  2216. await self.db_pool.runInteraction("delete_access_token", f)
  2217. async def delete_refresh_token(self, refresh_token: str) -> None:
  2218. def f(txn: LoggingTransaction) -> None:
  2219. self.db_pool.simple_delete_one_txn(
  2220. txn, table="refresh_tokens", keyvalues={"token": refresh_token}
  2221. )
  2222. await self.db_pool.runInteraction("delete_refresh_token", f)
  2223. async def add_user_pending_deactivation(self, user_id: str) -> None:
  2224. """
  2225. Adds a user to the table of users who need to be parted from all the rooms they're
  2226. in
  2227. """
  2228. await self.db_pool.simple_insert(
  2229. "users_pending_deactivation",
  2230. values={"user_id": user_id},
  2231. desc="add_user_pending_deactivation",
  2232. )
  2233. async def validate_threepid_session(
  2234. self, session_id: str, client_secret: str, token: str, current_ts: int
  2235. ) -> Optional[str]:
  2236. """Attempt to validate a threepid session using a token
  2237. Args:
  2238. session_id: The id of a validation session
  2239. client_secret: A unique string provided by the client to help identify
  2240. this validation attempt
  2241. token: A validation token
  2242. current_ts: The current unix time in milliseconds. Used for checking
  2243. token expiry status
  2244. Raises:
  2245. ThreepidValidationError: if a matching validation token was not found or has
  2246. expired
  2247. Returns:
  2248. A str representing a link to redirect the user to if there is one.
  2249. """
  2250. # Insert everything into a transaction in order to run atomically
  2251. def validate_threepid_session_txn(txn: LoggingTransaction) -> Optional[str]:
  2252. row = self.db_pool.simple_select_one_txn(
  2253. txn,
  2254. table="threepid_validation_session",
  2255. keyvalues={"session_id": session_id},
  2256. retcols=["client_secret", "validated_at"],
  2257. allow_none=True,
  2258. )
  2259. if not row:
  2260. if self._ignore_unknown_session_error:
  2261. # If we need to inhibit the error caused by an incorrect session ID,
  2262. # use None as placeholder values for the client secret and the
  2263. # validation timestamp.
  2264. # It shouldn't be an issue because they're both only checked after
  2265. # the token check, which should fail. And if it doesn't for some
  2266. # reason, the next check is on the client secret, which is NOT NULL,
  2267. # so we don't have to worry about the client secret matching by
  2268. # accident.
  2269. row = {"client_secret": None, "validated_at": None}
  2270. else:
  2271. raise ThreepidValidationError("Unknown session_id")
  2272. retrieved_client_secret = row["client_secret"]
  2273. validated_at = row["validated_at"]
  2274. row = self.db_pool.simple_select_one_txn(
  2275. txn,
  2276. table="threepid_validation_token",
  2277. keyvalues={"session_id": session_id, "token": token},
  2278. retcols=["expires", "next_link"],
  2279. allow_none=True,
  2280. )
  2281. if not row:
  2282. raise ThreepidValidationError(
  2283. "Validation token not found or has expired"
  2284. )
  2285. expires = row["expires"]
  2286. next_link = row["next_link"]
  2287. if retrieved_client_secret != client_secret:
  2288. raise ThreepidValidationError(
  2289. "This client_secret does not match the provided session_id"
  2290. )
  2291. # If the session is already validated, no need to revalidate
  2292. if validated_at:
  2293. return next_link
  2294. if expires <= current_ts:
  2295. raise ThreepidValidationError(
  2296. "This token has expired. Please request a new one"
  2297. )
  2298. # Looks good. Validate the session
  2299. self.db_pool.simple_update_txn(
  2300. txn,
  2301. table="threepid_validation_session",
  2302. keyvalues={"session_id": session_id},
  2303. updatevalues={"validated_at": self._clock.time_msec()},
  2304. )
  2305. return next_link
  2306. # Return next_link if it exists
  2307. return await self.db_pool.runInteraction(
  2308. "validate_threepid_session_txn", validate_threepid_session_txn
  2309. )
  2310. async def start_or_continue_validation_session(
  2311. self,
  2312. medium: str,
  2313. address: str,
  2314. session_id: str,
  2315. client_secret: str,
  2316. send_attempt: int,
  2317. next_link: Optional[str],
  2318. token: str,
  2319. token_expires: int,
  2320. ) -> None:
  2321. """Creates a new threepid validation session if it does not already
  2322. exist and associates a new validation token with it
  2323. Args:
  2324. medium: The medium of the 3PID
  2325. address: The address of the 3PID
  2326. session_id: The id of this validation session
  2327. client_secret: A unique string provided by the client to help
  2328. identify this validation attempt
  2329. send_attempt: The latest send_attempt on this session
  2330. next_link: The link to redirect the user to upon successful validation
  2331. token: The validation token
  2332. token_expires: The timestamp for which after the token will no
  2333. longer be valid
  2334. """
  2335. def start_or_continue_validation_session_txn(txn: LoggingTransaction) -> None:
  2336. # Create or update a validation session
  2337. self.db_pool.simple_upsert_txn(
  2338. txn,
  2339. table="threepid_validation_session",
  2340. keyvalues={"session_id": session_id},
  2341. values={"last_send_attempt": send_attempt},
  2342. insertion_values={
  2343. "medium": medium,
  2344. "address": address,
  2345. "client_secret": client_secret,
  2346. },
  2347. )
  2348. # Create a new validation token with this session ID
  2349. self.db_pool.simple_insert_txn(
  2350. txn,
  2351. table="threepid_validation_token",
  2352. values={
  2353. "session_id": session_id,
  2354. "token": token,
  2355. "next_link": next_link,
  2356. "expires": token_expires,
  2357. },
  2358. )
  2359. await self.db_pool.runInteraction(
  2360. "start_or_continue_validation_session",
  2361. start_or_continue_validation_session_txn,
  2362. )
  2363. async def update_user_approval_status(
  2364. self, user_id: UserID, approved: bool
  2365. ) -> None:
  2366. """Set the user's 'approved' flag to the given value.
  2367. The boolean will be turned into an int (in update_user_approval_status_txn)
  2368. because the column is a smallint.
  2369. Args:
  2370. user_id: the user to update the flag for.
  2371. approved: the value to set the flag to.
  2372. """
  2373. await self.db_pool.runInteraction(
  2374. "update_user_approval_status",
  2375. self.update_user_approval_status_txn,
  2376. user_id.to_string(),
  2377. approved,
  2378. )
  2379. @wrap_as_background_process("delete_expired_login_tokens")
  2380. async def _delete_expired_login_tokens(self) -> None:
  2381. """Remove login tokens with expiry dates that have passed."""
  2382. def _delete_expired_login_tokens_txn(txn: LoggingTransaction, ts: int) -> None:
  2383. sql = "DELETE FROM login_tokens WHERE expiry_ts <= ?"
  2384. txn.execute(sql, (ts,))
  2385. # We keep the expired tokens for an extra 5 minutes so we can measure how many
  2386. # times a token is being used after its expiry
  2387. now = self._clock.time_msec()
  2388. await self.db_pool.runInteraction(
  2389. "delete_expired_login_tokens",
  2390. _delete_expired_login_tokens_txn,
  2391. now - (5 * 60 * 1000),
  2392. )
  2393. def find_max_generated_user_id_localpart(cur: Cursor) -> int:
  2394. """
  2395. Gets the localpart of the max current generated user ID.
  2396. Generated user IDs are integers, so we find the largest integer user ID
  2397. already taken and return that.
  2398. """
  2399. # We bound between '@0' and '@a' to avoid pulling the entire table
  2400. # out.
  2401. cur.execute("SELECT name FROM users WHERE '@0' <= name AND name < '@a'")
  2402. regex = re.compile(r"^@(\d+):")
  2403. max_found = 0
  2404. for (user_id,) in cur:
  2405. match = regex.search(user_id)
  2406. if match:
  2407. max_found = max(int(match.group(1)), max_found)
  2408. return max_found