Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 5b70f24

Browse files
authored
Make cleaning up pushers depend on the device_id instead of the token_id (#15280)
This makes it so that we rely on the `device_id` to delete pushers on logout, instead of relying on the `access_token_id`. This ensures we're not removing pushers on token refresh, and prepares for a world without access token IDs (also known as the OIDC). This actually runs the `set_device_id_for_pushers` background update, which was forgotten in #13831. Note that for backwards compatibility it still deletes pushers based on the `access_token` until the background update finishes.
1 parent 68a6717 commit 5b70f24

File tree

15 files changed

+142
-65
lines changed

15 files changed

+142
-65
lines changed

changelog.d/15280.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make the pushers rely on the `device_id` instead of the `access_token_id` for various operations.

synapse/_scripts/synapse_port_db.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@
6868
MediaRepositoryBackgroundUpdateStore,
6969
)
7070
from synapse.storage.databases.main.presence import PresenceBackgroundUpdateStore
71-
from synapse.storage.databases.main.pusher import PusherWorkerStore
71+
from synapse.storage.databases.main.pusher import (
72+
PusherBackgroundUpdatesStore,
73+
PusherWorkerStore,
74+
)
7275
from synapse.storage.databases.main.receipts import ReceiptsBackgroundUpdateStore
7376
from synapse.storage.databases.main.registration import (
7477
RegistrationBackgroundUpdateStore,
@@ -226,6 +229,7 @@ class Store(
226229
AccountDataWorkerStore,
227230
PushRuleStore,
228231
PusherWorkerStore,
232+
PusherBackgroundUpdatesStore,
229233
PresenceBackgroundUpdateStore,
230234
ReceiptsBackgroundUpdateStore,
231235
RelationsWorkerStore,

synapse/handlers/auth.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,8 +1504,10 @@ async def delete_access_token(self, access_token: str) -> None:
15041504
)
15051505

15061506
# delete pushers associated with this access token
1507+
# XXX(quenting): This is only needed until the 'set_device_id_for_pushers'
1508+
# background update completes.
15071509
if token.token_id is not None:
1508-
await self.hs.get_pusherpool().remove_pushers_by_access_token(
1510+
await self.hs.get_pusherpool().remove_pushers_by_access_tokens(
15091511
token.user_id, (token.token_id,)
15101512
)
15111513

@@ -1535,7 +1537,9 @@ async def delete_access_tokens_for_user(
15351537
)
15361538

15371539
# delete pushers associated with the access tokens
1538-
await self.hs.get_pusherpool().remove_pushers_by_access_token(
1540+
# XXX(quenting): This is only needed until the 'set_device_id_for_pushers'
1541+
# background update completes.
1542+
await self.hs.get_pusherpool().remove_pushers_by_access_tokens(
15391543
user_id, (token_id for _, token_id, _ in tokens_and_devices)
15401544
)
15411545

synapse/handlers/device.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,8 @@ async def delete_devices(self, user_id: str, device_ids: List[str]) -> None:
503503
else:
504504
raise
505505

506+
await self.hs.get_pusherpool().remove_pushers_by_devices(user_id, device_ids)
507+
506508
# Delete data specific to each device. Not optimised as it is not
507509
# considered as part of a critical path.
508510
for device_id in device_ids:

synapse/handlers/register.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,11 +1013,11 @@ async def _register_email_threepid(
10131013
user_tuple = await self.store.get_user_by_access_token(token)
10141014
# The token better still exist.
10151015
assert user_tuple
1016-
token_id = user_tuple.token_id
1016+
device_id = user_tuple.device_id
10171017

10181018
await self.pusher_pool.add_or_update_pusher(
10191019
user_id=user_id,
1020-
access_token=token_id,
1020+
device_id=device_id,
10211021
kind="email",
10221022
app_id="m.email",
10231023
app_display_name="Email Notifications",

synapse/push/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class PusherConfig:
103103

104104
id: Optional[str]
105105
user_name: str
106-
access_token: Optional[int]
106+
107107
profile_tag: str
108108
kind: str
109109
app_id: str
@@ -119,6 +119,11 @@ class PusherConfig:
119119
enabled: bool
120120
device_id: Optional[str]
121121

122+
# XXX(quenting): The access_token is not persisted anymore for new pushers, but we
123+
# keep it when reading from the database, so that we don't get stale pushers
124+
# while the "set_device_id_for_pushers" background update is running.
125+
access_token: Optional[int]
126+
122127
def as_dict(self) -> Dict[str, Any]:
123128
"""Information that can be retrieved about a pusher after creation."""
124129
return {

synapse/push/pusherpool.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from synapse.push import Pusher, PusherConfig, PusherConfigException
2626
from synapse.push.pusher import PusherFactory
2727
from synapse.replication.http.push import ReplicationRemovePusherRestServlet
28-
from synapse.types import JsonDict, RoomStreamToken
28+
from synapse.types import JsonDict, RoomStreamToken, StrCollection
2929
from synapse.util.async_helpers import concurrently_execute
3030
from synapse.util.threepids import canonicalise_email
3131

@@ -97,7 +97,6 @@ def start(self) -> None:
9797
async def add_or_update_pusher(
9898
self,
9999
user_id: str,
100-
access_token: Optional[int],
101100
kind: str,
102101
app_id: str,
103102
app_display_name: str,
@@ -128,6 +127,22 @@ async def add_or_update_pusher(
128127
# stream ordering, so it will process pushes from this point onwards.
129128
last_stream_ordering = self.store.get_room_max_stream_ordering()
130129

130+
# Before we actually persist the pusher, we check if the user already has one
131+
# for this app ID and pushkey. If so, we want to keep the access token and
132+
# device ID in place, since this could be one device modifying
133+
# (e.g. enabling/disabling) another device's pusher.
134+
# XXX(quenting): Even though we're not persisting the access_token_id for new
135+
# pushers anymore, we still need to copy existing access_token_ids over when
136+
# updating a pusher, in case the "set_device_id_for_pushers" background update
137+
# hasn't run yet.
138+
access_token_id = None
139+
existing_config = await self._get_pusher_config_for_user_by_app_id_and_pushkey(
140+
user_id, app_id, pushkey
141+
)
142+
if existing_config:
143+
device_id = existing_config.device_id
144+
access_token_id = existing_config.access_token
145+
131146
# we try to create the pusher just to validate the config: it
132147
# will then get pulled out of the database,
133148
# recreated, added and started: this means we have only one
@@ -136,7 +151,6 @@ async def add_or_update_pusher(
136151
PusherConfig(
137152
id=None,
138153
user_name=user_id,
139-
access_token=access_token,
140154
profile_tag=profile_tag,
141155
kind=kind,
142156
app_id=app_id,
@@ -151,23 +165,12 @@ async def add_or_update_pusher(
151165
failing_since=None,
152166
enabled=enabled,
153167
device_id=device_id,
168+
access_token=access_token_id,
154169
)
155170
)
156171

157-
# Before we actually persist the pusher, we check if the user already has one
158-
# this app ID and pushkey. If so, we want to keep the access token and device ID
159-
# in place, since this could be one device modifying (e.g. enabling/disabling)
160-
# another device's pusher.
161-
existing_config = await self._get_pusher_config_for_user_by_app_id_and_pushkey(
162-
user_id, app_id, pushkey
163-
)
164-
if existing_config:
165-
access_token = existing_config.access_token
166-
device_id = existing_config.device_id
167-
168172
await self.store.add_pusher(
169173
user_id=user_id,
170-
access_token=access_token,
171174
kind=kind,
172175
app_id=app_id,
173176
app_display_name=app_display_name,
@@ -180,6 +183,7 @@ async def add_or_update_pusher(
180183
profile_tag=profile_tag,
181184
enabled=enabled,
182185
device_id=device_id,
186+
access_token_id=access_token_id,
183187
)
184188
pusher = await self.process_pusher_change_by_id(app_id, pushkey, user_id)
185189

@@ -199,7 +203,7 @@ async def remove_pushers_by_app_id_and_pushkey_not_user(
199203
)
200204
await self.remove_pusher(p.app_id, p.pushkey, p.user_name)
201205

202-
async def remove_pushers_by_access_token(
206+
async def remove_pushers_by_access_tokens(
203207
self, user_id: str, access_tokens: Iterable[int]
204208
) -> None:
205209
"""Remove the pushers for a given user corresponding to a set of
@@ -209,6 +213,8 @@ async def remove_pushers_by_access_token(
209213
user_id: user to remove pushers for
210214
access_tokens: access token *ids* to remove pushers for
211215
"""
216+
# XXX(quenting): This is only needed until the "set_device_id_for_pushers"
217+
# background update finishes
212218
tokens = set(access_tokens)
213219
for p in await self.store.get_pushers_by_user_id(user_id):
214220
if p.access_token in tokens:
@@ -220,6 +226,26 @@ async def remove_pushers_by_access_token(
220226
)
221227
await self.remove_pusher(p.app_id, p.pushkey, p.user_name)
222228

229+
async def remove_pushers_by_devices(
230+
self, user_id: str, devices: StrCollection
231+
) -> None:
232+
"""Remove the pushers for a given user corresponding to a set of devices
233+
234+
Args:
235+
user_id: user to remove pushers for
236+
devices: device IDs to remove pushers for
237+
"""
238+
device_ids = set(devices)
239+
for p in await self.store.get_pushers_by_user_id(user_id):
240+
if p.device_id in device_ids:
241+
logger.info(
242+
"Removing pusher for app id %s, pushkey %s, user %s",
243+
p.app_id,
244+
p.pushkey,
245+
p.user_name,
246+
)
247+
await self.remove_pusher(p.app_id, p.pushkey, p.user_name)
248+
223249
def on_new_notifications(self, max_token: RoomStreamToken) -> None:
224250
if not self.pushers:
225251
# nothing to do here.

synapse/rest/admin/users.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ async def on_PUT(
425425
):
426426
await self.pusher_pool.add_or_update_pusher(
427427
user_id=user_id,
428-
access_token=None,
429428
kind="email",
430429
app_id="m.email",
431430
app_display_name="Email Notifications",

synapse/rest/client/pusher.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
126126
try:
127127
await self.pusher_pool.add_or_update_pusher(
128128
user_id=user.to_string(),
129-
access_token=requester.access_token_id,
130129
kind=content["kind"],
131130
app_id=content["app_id"],
132131
app_display_name=content["app_display_name"],

synapse/storage/databases/main/pusher.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -509,19 +509,24 @@ def __init__(
509509
async def _set_device_id_for_pushers(
510510
self, progress: JsonDict, batch_size: int
511511
) -> int:
512-
"""Background update to populate the device_id column of the pushers table."""
512+
"""
513+
Background update to populate the device_id column and clear the access_token
514+
column for the pushers table.
515+
"""
513516
last_pusher_id = progress.get("pusher_id", 0)
514517

515518
def set_device_id_for_pushers_txn(txn: LoggingTransaction) -> int:
516519
txn.execute(
517520
"""
518-
SELECT p.id, at.device_id
521+
SELECT
522+
p.id AS pusher_id,
523+
p.device_id AS pusher_device_id,
524+
at.device_id AS token_device_id
519525
FROM pushers AS p
520-
INNER JOIN access_tokens AS at
526+
LEFT JOIN access_tokens AS at
521527
ON p.access_token = at.id
522528
WHERE
523529
p.access_token IS NOT NULL
524-
AND at.device_id IS NOT NULL
525530
AND p.id > ?
526531
ORDER BY p.id
527532
LIMIT ?
@@ -533,13 +538,27 @@ def set_device_id_for_pushers_txn(txn: LoggingTransaction) -> int:
533538
if len(rows) == 0:
534539
return 0
535540

541+
# The reason we're clearing the access_token column here is a bit subtle.
542+
# When a user logs out, we:
543+
# (1) delete the access token
544+
# (2) delete the device
545+
#
546+
# Ideally, we would delete the pushers only via its link to the device
547+
# during (2), but since this background update might not have fully run yet,
548+
# we're still deleting the pushers via the access token during (1).
536549
self.db_pool.simple_update_many_txn(
537550
txn=txn,
538551
table="pushers",
539552
key_names=("id",),
540-
key_values=[(row["id"],) for row in rows],
541-
value_names=("device_id",),
542-
value_values=[(row["device_id"],) for row in rows],
553+
key_values=[(row["pusher_id"],) for row in rows],
554+
value_names=("device_id", "access_token"),
555+
# If there was already a device_id on the pusher, we only want to clear
556+
# the access_token column, so we keep the existing device_id. Otherwise,
557+
# we set the device_id we got from joining the access_tokens table.
558+
value_values=[
559+
(row["pusher_device_id"] or row["token_device_id"], None)
560+
for row in rows
561+
],
543562
)
544563

545564
self.db_pool.updates._background_update_progress_txn(
@@ -568,7 +587,6 @@ class PusherStore(PusherWorkerStore, PusherBackgroundUpdatesStore):
568587
async def add_pusher(
569588
self,
570589
user_id: str,
571-
access_token: Optional[int],
572590
kind: str,
573591
app_id: str,
574592
app_display_name: str,
@@ -581,13 +599,13 @@ async def add_pusher(
581599
profile_tag: str = "",
582600
enabled: bool = True,
583601
device_id: Optional[str] = None,
602+
access_token_id: Optional[int] = None,
584603
) -> None:
585604
async with self._pushers_id_gen.get_next() as stream_id:
586605
await self.db_pool.simple_upsert(
587606
table="pushers",
588607
keyvalues={"app_id": app_id, "pushkey": pushkey, "user_name": user_id},
589608
values={
590-
"access_token": access_token,
591609
"kind": kind,
592610
"app_display_name": app_display_name,
593611
"device_display_name": device_display_name,
@@ -599,6 +617,10 @@ async def add_pusher(
599617
"id": stream_id,
600618
"enabled": enabled,
601619
"device_id": device_id,
620+
# XXX(quenting): We're only really persisting the access token ID
621+
# when updating an existing pusher. This is in case the
622+
# 'set_device_id_for_pushers' background update hasn't finished yet.
623+
"access_token": access_token_id,
602624
},
603625
desc="add_pusher",
604626
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* Copyright 2023 The Matrix.org Foundation C.I.C
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- Triggers the background update to set the device_id for pushers
17+
-- that don't have one, and clear the access_token column.
18+
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
19+
(7402, 'set_device_id_for_pushers', '{}');

tests/push/test_email.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
105105
self.hs.get_datastores().main.get_user_by_access_token(self.access_token)
106106
)
107107
assert user_tuple is not None
108-
self.token_id = user_tuple.token_id
108+
self.device_id = user_tuple.device_id
109109

110110
# We need to add email to account before we can create a pusher.
111111
self.get_success(
@@ -117,7 +117,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
117117
pusher = self.get_success(
118118
self.hs.get_pusherpool().add_or_update_pusher(
119119
user_id=self.user_id,
120-
access_token=self.token_id,
120+
device_id=self.device_id,
121121
kind="email",
122122
app_id="m.email",
123123
app_display_name="Email Notifications",
@@ -141,7 +141,7 @@ def test_need_validated_email(self) -> None:
141141
self.get_success_or_raise(
142142
self.hs.get_pusherpool().add_or_update_pusher(
143143
user_id=self.user_id,
144-
access_token=self.token_id,
144+
device_id=self.device_id,
145145
kind="email",
146146
app_id="m.email",
147147
app_display_name="Email Notifications",

0 commit comments

Comments
 (0)