def _get_current_version(txn, user_id): txn.execute( "SELECT MAX(version) FROM e2e_room_keys_versions " "WHERE user_id=? AND deleted=0", (user_id, )) row = txn.fetchone() if not row: raise StoreError(404, 'No current backup version') return row[0]
def get_user_by_token(self, token): try: return { "name": self.tokens_to_users[token], "admin": 0, "device_id": None, } except: raise StoreError(400, "User does not exist.")
def _simple_delete_one_txn(txn, table, keyvalues): """Executes a DELETE query on the named table, expecting to delete a single row. Args: table : string giving the table name keyvalues : dict of column names and values to select the row with """ sql = "DELETE FROM %s WHERE %s" % ( table, " AND ".join("%s = ?" % (k, ) for k in keyvalues) ) txn.execute(sql, keyvalues.values()) if txn.rowcount == 0: raise StoreError(404, "No row found") if txn.rowcount > 1: raise StoreError(500, "more than one row matched")
async def create_ui_auth_session( self, clientdict: JsonDict, uri: str, method: str, description: str, ) -> UIAuthSessionData: """ Creates a new user interactive authentication session. The session can be used to track the stages necessary to authenticate a user across multiple HTTP requests. Args: clientdict: The dictionary from the client root level, not the 'auth' key. uri: The URI this session was initiated with, this is checked at each stage of the authentication to ensure that the asked for operation has not changed. method: The method this session was initiated with, this is checked at each stage of the authentication to ensure that the asked for operation has not changed. description: A string description of the operation that the current authentication is authorising. Returns: The newly created session. Raises: StoreError if a unique session ID cannot be generated. """ # The clientdict gets stored as JSON. clientdict_json = json.dumps(clientdict) # autogen a session ID and try to create it. We may clash, so just # try a few times till one goes through, giving up eventually. attempts = 0 while attempts < 5: session_id = stringutils.random_string(24) try: await self.db.simple_insert( table="ui_auth_sessions", values={ "session_id": session_id, "clientdict": clientdict_json, "uri": uri, "method": method, "description": description, "serverdict": "{}", "creation_time": self.hs.get_clock().time_msec(), }, desc="create_ui_auth_session", ) return UIAuthSessionData( session_id, clientdict, uri, method, description ) except self.db.engine.module.IntegrityError: attempts += 1 raise StoreError(500, "Couldn't generate a session ID.")
def _query_for_auth(self, txn, token): txn.execute( "SELECT users.name FROM access_tokens LEFT JOIN users" + " ON users.id = access_tokens.user_id WHERE token = ?", [token]) row = txn.fetchone() if row: return row[0] raise StoreError(404, "Token not found.")
def store_room(self, room_id, room_creator_user_id, is_public): if room_id in self.rooms: raise StoreError(409, "Conflicting room!") room = MemoryDataStore.Room( room_id=room_id, is_public=is_public, creator=room_creator_user_id ) self.rooms[room_id] = room
def _simple_select_one_txn(txn, table, keyvalues, retcols, allow_none=False): select_sql = "SELECT %s FROM %s WHERE %s" % ( ", ".join(retcols), table, " AND ".join("%s = ?" % (k,) for k in keyvalues) ) txn.execute(select_sql, keyvalues.values()) row = txn.fetchone() if not row: if allow_none: return None raise StoreError(404, "No row found") if txn.rowcount > 1: raise StoreError(500, "More than one row matched") return dict(zip(retcols, row))
def _simple_update_one_txn(txn, table, keyvalues, updatevalues): if keyvalues: where = "WHERE %s" % " AND ".join("%s = ?" % k for k in keyvalues.iterkeys()) else: where = "" update_sql = "UPDATE %s SET %s %s" % ( table, ", ".join("%s = ?" % (k, ) for k in updatevalues), where, ) txn.execute(update_sql, updatevalues.values() + keyvalues.values()) if txn.rowcount == 0: raise StoreError(404, "No row found") if txn.rowcount > 1: raise StoreError(500, "More than one row matched")
def _get_current_version(txn: LoggingTransaction, user_id: str) -> int: txn.execute( "SELECT MAX(version) FROM e2e_room_keys_versions " "WHERE user_id=? AND deleted=0", (user_id, ), ) # `SELECT MAX() FROM ...` will always return 1 row. The value in that row will # be `NULL` when there are no available versions. row = cast(Tuple[Optional[int]], txn.fetchone()) if row[0] is None: raise StoreError(404, "No current backup version") return row[0]
def _query_for_auth(self, txn, token): sql = ("SELECT users.name, users.admin, access_tokens.device_id" " FROM users" " INNER JOIN access_tokens on users.id = access_tokens.user_id" " WHERE token = ?") cursor = txn.execute(sql, (token, )) rows = self.cursor_to_dict(cursor) if rows: return rows[0] raise StoreError(404, "Token not found.")
def store_room(self, room_id, room_creator_user_id, is_public): """Stores a room. Args: room_id (str): The desired room ID, can be None. room_creator_user_id (str): The user ID of the room creator. is_public (bool): True to indicate that this room should appear in public room lists. Raises: StoreError if the room could not be stored. """ try: yield self._simple_insert( RoomsTable.table_name, dict(room_id=room_id, creator=room_creator_user_id, is_public=is_public)) except IntegrityError: raise StoreError(409, "Room ID in use.") except Exception as e: logger.error("store_room with room_id=%s failed: %s", room_id, e) raise StoreError(500, "Problem creating room.")
async def add_e2e_room_keys( self, user_id: str, version: str, room_keys: Iterable[Tuple[str, str, RoomKey]]) -> None: """Bulk add room keys to a given backup. Args: user_id: the user whose backup we're adding to version: the version ID of the backup for the set of keys we're adding to room_keys: the keys to add, in the form (roomID, sessionID, keyData) """ try: version_int = int(version) except ValueError: # Our versions are all ints so if we can't convert it to an integer, # it doesn't exist. raise StoreError(404, "No backup with that version exists") values = [] for (room_id, session_id, room_key) in room_keys: values.append(( user_id, version_int, room_id, session_id, room_key["first_message_index"], room_key["forwarded_count"], room_key["is_verified"], json_encoder.encode(room_key["session_data"]), )) log_kv({ "message": "Set room key", "room_id": room_id, "session_id": session_id, StreamKeyType.ROOM: room_key, }) await self.db_pool.simple_insert_many( table="e2e_room_keys", keys=( "user_id", "version", "room_id", "session_id", "first_message_index", "forwarded_count", "is_verified", "session_data", ), values=values, desc="add_e2e_room_keys", )
async def get_access_token_for_user_id( self, user_id: str, device_id: Optional[str], valid_until_ms: Optional[int] ): """ Creates a new access token for the user with the given user ID. The user is assumed to have been authenticated by some other machanism (e.g. CAS), and the user_id converted to the canonical case. The device will be recorded in the table if it is not there already. Args: user_id: canonical User ID device_id: the device ID to associate with the tokens. None to leave the tokens unassociated with a device (deprecated: we should always have a device ID) valid_until_ms: when the token is valid until. None for no expiry. Returns: The access token for the user's session. Raises: StoreError if there was a problem storing the token. """ fmt_expiry = "" if valid_until_ms is not None: fmt_expiry = time.strftime( " until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0) ) logger.info("Logging in user %s on device %s%s", user_id, device_id, fmt_expiry) await self.auth.check_auth_blocking(user_id) access_token = self.macaroon_gen.generate_access_token(user_id) await self.store.add_access_token_to_user( user_id, access_token, device_id, valid_until_ms ) # the device *should* have been registered before we got here; however, # it's possible we raced against a DELETE operation. The thing we # really don't want is active access_tokens without a record of the # device, so we double-check it here. if device_id is not None: try: await self.store.get_device(user_id, device_id) except StoreError: await self.store.delete_access_token(access_token) raise StoreError(400, "Login raced against device deletion") return access_token
def _get_session(txn: LoggingTransaction, session_type: str, session_id: str, ts: int) -> JsonDict: # This includes the expiry time since items are only periodically # deleted, not upon expiry. select_sql = """ SELECT value FROM sessions WHERE session_type = ? AND session_id = ? AND expiry_time_ms > ? """ txn.execute(select_sql, [session_type, session_id, ts]) row = txn.fetchone() if not row: raise StoreError(404, "No session") return db_to_json(row[0])
def store_room( self, room_id: str, room_creator_user_id: str, is_public: bool, room_version: RoomVersion, ): """Stores a room. Args: room_id: The desired room ID, can be None. room_creator_user_id: The user ID of the room creator. is_public: True to indicate that this room should appear in public room lists. room_version: The version of the room Raises: StoreError if the room could not be stored. """ try: def store_room_txn(txn, next_id): self.db.simple_insert_txn( txn, "rooms", { "room_id": room_id, "creator": room_creator_user_id, "is_public": is_public, "room_version": room_version.identifier, }, ) if is_public: self.db.simple_insert_txn( txn, table="public_room_list_stream", values={ "stream_id": next_id, "room_id": room_id, "visibility": is_public, }, ) with self._public_room_id_gen.get_next() as next_id: yield self.db.runInteraction("store_room_txn", store_room_txn, next_id) except Exception as e: logger.error("store_room with room_id=%s failed: %s", room_id, e) raise StoreError(500, "Problem creating room.")
def _register(self, txn, user_id, token, password_hash): now = int(self.clock.time()) try: txn.execute("INSERT INTO users(name, password_hash, creation_ts) " "VALUES (?,?,?)", [user_id, password_hash, now]) except IntegrityError: raise StoreError( 400, "User ID already taken.", errcode=Codes.USER_IN_USE ) # it's possible for this to get a conflict, but only for a single user # since tokens are namespaced based on their user ID txn.execute("INSERT INTO access_tokens(user_id, token) " + "VALUES (?,?)", [txn.lastrowid, token])
def _simple_select_one_onecol_txn(cls, txn, table, keyvalues, retcol, allow_none=False): ret = cls._simple_select_onecol_txn( txn, table=table, keyvalues=keyvalues, retcol=retcol, ) if ret: return ret[0] else: if allow_none: return None else: raise StoreError(404, "No row found")
def _register(self, txn, user_id, token, password_hash, was_guest, make_guest, appservice_id): now = int(self.clock.time()) next_id = self._access_tokens_id_gen.get_next() try: if was_guest: txn.execute( "UPDATE users SET" " password_hash = ?," " upgrade_ts = ?," " is_guest = ?" " WHERE name = ?", [password_hash, now, 1 if make_guest else 0, user_id]) else: txn.execute( "INSERT INTO users " "(" " name," " password_hash," " creation_ts," " is_guest," " appservice_id" ") " "VALUES (?,?,?,?,?)", [ user_id, password_hash, now, 1 if make_guest else 0, appservice_id, ]) except self.database_engine.module.IntegrityError: raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE) if token: # it's possible for this to get a conflict, but only for a single user # since tokens are namespaced based on their user ID txn.execute( "INSERT INTO access_tokens(id, user_id, token)" " VALUES (?,?,?)", ( next_id, user_id, token, ))
def add_access_token_to_user(self, user_id, token): """Adds an access token for the given user. Args: user_id (str): The user ID. token (str): The new access token to add. Raises: StoreError if there was a problem adding this. """ row = yield self._simple_select_one("users", {"name": user_id}, ["id"]) if not row: raise StoreError(400, "Bad user ID supplied.") row_id = row["id"] yield self._simple_insert("access_tokens", { "user_id": row_id, "token": token })
async def create_session(self, session_type: str, value: JsonDict, expiry_ms: int) -> str: """ Creates a new pagination session for the room hierarchy endpoint. Args: session_type: The type for this session. value: The value to store. expiry_ms: How long before an item is evicted from the cache in milliseconds. Default is 0, indicating items never get evicted based on time. Returns: The newly created session ID. Raises: StoreError if a unique session ID cannot be generated. """ # autogen a session ID and try to create it. We may clash, so just # try a few times till one goes through, giving up eventually. attempts = 0 while attempts < 5: session_id = stringutils.random_string(24) try: await self.db_pool.simple_insert( table="sessions", values={ "session_id": session_id, "session_type": session_type, "value": json_encoder.encode(value), "expiry_time_ms": self.hs.get_clock().time_msec() + expiry_ms, }, desc="create_session", ) return session_id except self.db_pool.engine.module.IntegrityError: attempts += 1 raise StoreError(500, "Couldn't generate a session ID.")
def get_user_by_token(self, token): """ Get a registered user's ID. Args: token (str)- The access token to get the user by. Returns: UserID : User ID object of the user who has that access token. Raises: AuthError if no user by that token exists or the token is invalid. """ try: user_id = yield self.store.get_user_by_token(token=token) if not user_id: raise StoreError() defer.returnValue(self.hs.parse_userid(user_id)) except StoreError: raise AuthError(403, "Unrecognised access token.", errcode=Codes.UNKNOWN_TOKEN)
def _generate_room_id(self, creator_id, is_public): # autogen room IDs and try to create it. We may clash, so just # try a few times till one goes through, giving up eventually. attempts = 0 while attempts < 5: try: random_string = stringutils.random_string(18) gen_room_id = RoomID(random_string, self.hs.hostname).to_string() if isinstance(gen_room_id, bytes): gen_room_id = gen_room_id.decode("utf-8") yield self.store.store_room( room_id=gen_room_id, room_creator_user_id=creator_id, is_public=is_public, ) return gen_room_id except StoreError: attempts += 1 raise StoreError(500, "Couldn't generate a room ID.")
def _exchange_refresh_token(self, txn, old_token, token_generator): sql = "SELECT user_id FROM refresh_tokens WHERE token = ?" txn.execute(sql, (old_token, )) rows = self.cursor_to_dict(txn) if not rows: raise StoreError(403, "Did not recognize refresh token") user_id = rows[0]["user_id"] # TODO(danielwh): Maybe perform a validation on the macaroon that # macaroon.user_id == user_id. new_token = token_generator(user_id) sql = "UPDATE refresh_tokens SET token = ? WHERE token = ?" txn.execute(sql, ( new_token, old_token, )) return user_id, new_token
async def update_e2e_room_key( self, user_id: str, version: str, room_id: str, session_id: str, room_key: RoomKey, ) -> None: """Replaces the encrypted E2E room key for a given session in a given backup Args: user_id: the user whose backup we're setting version: the version ID of the backup we're updating room_id: the ID of the room whose keys we're setting session_id: the session whose room_key we're setting room_key: the room_key being set Raises: StoreError """ try: version_int = int(version) except ValueError: # Our versions are all ints so if we can't convert it to an integer, # it doesn't exist. raise StoreError(404, "No backup with that version exists") await self.db_pool.simple_update_one( table="e2e_room_keys", keyvalues={ "user_id": user_id, "version": version_int, "room_id": room_id, "session_id": session_id, }, updatevalues={ "first_message_index": room_key["first_message_index"], "forwarded_count": room_key["forwarded_count"], "is_verified": room_key["is_verified"], "session_data": json_encoder.encode(room_key["session_data"]), }, desc="update_e2e_room_key", )
def store_device(self, user_id, device_id, initial_device_display_name): """Ensure the given device is known; add it to the store if not Args: user_id (str): id of user associated with the device device_id (str): id of device initial_device_display_name (str): initial displayname of the device. Ignored if device exists. Returns: defer.Deferred: boolean whether the device was inserted or an existing device existed with that ID. """ key = (user_id, device_id) if self.device_id_exists_cache.get(key, None): return False try: inserted = yield self._simple_insert( "devices", values={ "user_id": user_id, "device_id": device_id, "display_name": initial_device_display_name, }, desc="store_device", or_ignore=True, ) self.device_id_exists_cache.prefill(key, True) return inserted except Exception as e: logger.error( "store_device with device_id=%s(%r) user_id=%s(%r)" " display_name=%s(%r) failed: %s", type(device_id).__name__, device_id, type(user_id).__name__, user_id, type(initial_device_display_name).__name__, initial_device_display_name, e, ) raise StoreError(500, "Problem storing device.")
def _delete_e2e_room_keys_version_txn(txn): if version is None: this_version = self._get_current_version(txn, user_id) if this_version is None: raise StoreError(404, "No current backup version") else: this_version = version self.db.simple_delete_txn( txn, table="e2e_room_keys", keyvalues={"user_id": user_id, "version": this_version}, ) return self.db.simple_update_one_txn( txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version}, updatevalues={"deleted": 1}, )
def add_pusher(self, user_name, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, pushkey_ts, lang, data): try: yield self._simple_upsert( PushersTable.table_name, dict( app_id=app_id, pushkey=pushkey, ), dict(user_name=user_name, kind=kind, profile_tag=profile_tag, app_display_name=app_display_name, device_display_name=device_display_name, ts=pushkey_ts, lang=lang, data=data)) except Exception as e: logger.error("create_pusher with failed: %s", e) raise StoreError(500, "Problem creating pusher.")
def _get_e2e_room_keys_version_info_txn(txn): if version is None: this_version = self._get_current_version(txn, user_id) else: try: this_version = int(version) except ValueError: # Our versions are all ints so if we can't convert it to an integer, # it isn't there. raise StoreError(404, "No row found") result = self._simple_select_one_txn( txn, table="e2e_room_keys_versions", keyvalues={"user_id": user_id, "version": this_version, "deleted": 0}, retcols=("version", "algorithm", "auth_data"), ) result["auth_data"] = json.loads(result["auth_data"]) result["version"] = str(result["version"]) return result
async def update_e2e_room_keys_version( self, user_id: str, version: str, info: Optional[JsonDict] = None, version_etag: Optional[int] = None, ) -> None: """Update a given backup version Args: user_id: the user whose backup version we're updating version: the version ID of the backup version we're updating info: the new backup version info to store. If None, then the backup version info is not updated. version_etag: etag of the keys in the backup. If None, then the etag is not updated. """ updatevalues: Dict[str, object] = {} if info is not None and "auth_data" in info: updatevalues["auth_data"] = json_encoder.encode(info["auth_data"]) if version_etag is not None: updatevalues["etag"] = version_etag if updatevalues: try: version_int = int(version) except ValueError: # Our versions are all ints so if we can't convert it to an integer, # it doesn't exist. raise StoreError(404, "No backup with that version exists") await self.db_pool.simple_update_one( table="e2e_room_keys_versions", keyvalues={ "user_id": user_id, "version": version_int }, updatevalues=updatevalues, desc="update_e2e_room_keys_version", )
def store_device(self, user_id, device_id, initial_device_display_name, ignore_if_known=True): """Ensure the given device is known; add it to the store if not Args: user_id (str): id of user associated with the device device_id (str): id of device initial_device_display_name (str): initial displayname of the device ignore_if_known (bool): ignore integrity errors which mean the device is already known Returns: defer.Deferred Raises: StoreError: if ignore_if_known is False and the device was already known """ try: yield self._simple_insert( "devices", values={ "user_id": user_id, "device_id": device_id, "display_name": initial_device_display_name }, desc="store_device", or_ignore=ignore_if_known, ) except Exception as e: logger.error( "store_device with device_id=%s(%r) user_id=%s(%r)" " display_name=%s(%r) failed: %s", type(device_id).__name__, device_id, type(user_id).__name__, user_id, type(initial_device_display_name).__name__, initial_device_display_name, e) raise StoreError(500, "Problem storing device.")