Skip to content

Commit 6a79db6

Browse files
committed
Split access tokens between refreshable and unrefreshable ones
1 parent ef419ef commit 6a79db6

File tree

4 files changed

+107
-61
lines changed

4 files changed

+107
-61
lines changed

crates/syn2mas/src/migration.rs

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ use uuid::Uuid;
2929

3030
use crate::{
3131
mas_writer::{
32-
self, MasNewCompatAccessToken, MasNewCompatSession, MasNewEmailThreepid,
33-
MasNewUnsupportedThreepid, MasNewUpstreamOauthLink, MasNewUser, MasNewUserPassword,
34-
MasWriteBuffer, MasWriter,
32+
self, MasNewCompatAccessToken, MasNewCompatRefreshToken, MasNewCompatSession,
33+
MasNewEmailThreepid, MasNewUnsupportedThreepid, MasNewUpstreamOauthLink, MasNewUser,
34+
MasNewUserPassword, MasWriteBuffer, MasWriter,
3535
},
3636
synapse_reader::{
37-
self, ExtractLocalpartError, FullUserId, SynapseAccessToken, SynapseDevice, SynapseExternalId, SynapseRefreshToken, SynapseThreepid, SynapseUser
37+
self, ExtractLocalpartError, FullUserId, SynapseAccessToken, SynapseDevice,
38+
SynapseExternalId, SynapseRefreshableTokenPair, SynapseThreepid, SynapseUser,
3839
},
3940
SynapseReader,
4041
};
@@ -139,7 +140,7 @@ pub async fn migrate(
139140
.expect("More than usize::MAX devices — unable to handle this many!"),
140141
);
141142

142-
migrate_access_tokens(
143+
migrate_unrefreshable_access_tokens(
143144
synapse,
144145
mas,
145146
server_name,
@@ -150,10 +151,11 @@ pub async fn migrate(
150151
)
151152
.await?;
152153

153-
migrate_refresh_tokens(
154+
migrate_refreshable_token_pairs(
154155
synapse,
155156
mas,
156157
server_name,
158+
clock,
157159
rng,
158160
&migrated_users.user_localparts_to_uuid,
159161
&mut devices_to_compat_sessions,
@@ -450,8 +452,10 @@ async fn migrate_devices(
450452
Ok(())
451453
}
452454

455+
/// Migrates unrefreshable access tokens (those without an associated refresh
456+
/// token). Some of these may be deviceless.
453457
#[tracing::instrument(skip_all, level = Level::INFO)]
454-
async fn migrate_access_tokens(
458+
async fn migrate_unrefreshable_access_tokens(
455459
synapse: &mut SynapseReader<'_>,
456460
mas: &mut MasWriter<'_>,
457461
server_name: &str,
@@ -460,7 +464,7 @@ async fn migrate_access_tokens(
460464
user_localparts_to_uuid: &HashMap<CompactString, Uuid>,
461465
devices: &mut HashMap<(Uuid, CompactString), Uuid>,
462466
) -> Result<(), Error> {
463-
let mut token_stream = pin!(synapse.read_access_tokens());
467+
let mut token_stream = pin!(synapse.read_unrefreshable_access_tokens());
464468
let mut write_buffer = MasWriteBuffer::new(MasWriter::write_compat_access_tokens);
465469
let mut deviceless_session_write_buffer = MasWriteBuffer::new(MasWriter::write_compat_sessions);
466470

@@ -479,7 +483,7 @@ async fn migrate_access_tokens(
479483
.to_owned();
480484
let Some(user_id) = user_localparts_to_uuid.get(username.as_str()).copied() else {
481485
return Err(Error::MissingUserFromDependentTable {
482-
table: "devices".to_owned(),
486+
table: "access_tokens".to_owned(),
483487
user: synapse_user_id,
484488
});
485489
};
@@ -553,37 +557,92 @@ async fn migrate_access_tokens(
553557
Ok(())
554558
}
555559

560+
/// Migrates (access token, refresh token) pairs.
561+
/// Does not migrate non-refreshable access tokens.
556562
#[tracing::instrument(skip_all, level = Level::INFO)]
557-
async fn migrate_refresh_tokens(
563+
async fn migrate_refreshable_token_pairs(
558564
synapse: &mut SynapseReader<'_>,
559565
mas: &mut MasWriter<'_>,
560566
server_name: &str,
567+
clock: &dyn Clock,
561568
rng: &mut impl RngCore,
562569
user_localparts_to_uuid: &HashMap<CompactString, Uuid>,
563570
devices: &mut HashMap<(Uuid, CompactString), Uuid>,
564571
) -> Result<(), Error> {
565-
let mut token_stream = pin!(synapse.read_refresh_tokens());
566-
let mut write_buffer = MasWriteBuffer::new(MasWriter::write_compat_refresh_tokens);
572+
let mut token_stream = pin!(synapse.read_refreshable_token_pairs());
573+
let mut access_token_write_buffer = MasWriteBuffer::new(MasWriter::write_compat_access_tokens);
574+
let mut refresh_token_write_buffer =
575+
MasWriteBuffer::new(MasWriter::write_compat_refresh_tokens);
567576

568577
while let Some(token_res) = token_stream.next().await {
569-
let SynapseRefreshToken {user_id:synapse_user_id,device_id,token, id }
570-
= token_res.into_synapse("reading Synapse refresh token")?;
578+
let SynapseRefreshableTokenPair {
579+
user_id: synapse_user_id,
580+
device_id,
581+
access_token,
582+
refresh_token,
583+
valid_until_ms,
584+
last_validated,
585+
} = token_res.into_synapse("reading Synapse refresh token")?;
571586

572587
let username = synapse_user_id
573588
.extract_localpart(server_name)
574589
.into_extract_localpart(synapse_user_id.clone())?
575590
.to_owned();
576591
let Some(user_id) = user_localparts_to_uuid.get(username.as_str()).copied() else {
577592
return Err(Error::MissingUserFromDependentTable {
578-
table: "devices".to_owned(),
593+
table: "refresh_tokens".to_owned(),
579594
user: synapse_user_id,
580595
});
581596
};
582597

583-
todo!()
598+
// It's not always accurate, but last_validated is *often* the creation time of
599+
// the device If we don't have one, then use the current time as a
600+
// fallback.
601+
let created_at = last_validated.map_or_else(|| clock.now(), DateTime::from);
602+
603+
// Use the existing device_id if this is the second token for a device
604+
let session_id = *devices
605+
.entry((user_id, CompactString::new(&device_id)))
606+
.or_insert_with(|| Uuid::from(Ulid::from_datetime_with_source(created_at.into(), rng)));
607+
608+
let access_token_id = Uuid::from(Ulid::from_datetime_with_source(created_at.into(), rng));
609+
let refresh_token_id = Uuid::from(Ulid::from_datetime_with_source(created_at.into(), rng));
610+
611+
// TODO skip access tokens for deactivated users
612+
access_token_write_buffer
613+
.write(
614+
mas,
615+
MasNewCompatAccessToken {
616+
token_id: access_token_id,
617+
session_id,
618+
access_token,
619+
created_at,
620+
expires_at: valid_until_ms.map(DateTime::from),
621+
},
622+
)
623+
.await
624+
.into_mas("writing compat access tokens")?;
625+
refresh_token_write_buffer
626+
.write(
627+
mas,
628+
MasNewCompatRefreshToken {
629+
refresh_token_id,
630+
session_id,
631+
access_token_id,
632+
refresh_token,
633+
created_at,
634+
},
635+
)
636+
.await
637+
.into_mas("writing compat refresh tokens")?;
584638
}
585639

586-
write_buffer
640+
access_token_write_buffer
641+
.finish(mas)
642+
.await
643+
.into_mas("writing compat access tokens")?;
644+
645+
refresh_token_write_buffer
587646
.finish(mas)
588647
.await
589648
.into_mas("writing compat refresh tokens")?;

crates/syn2mas/src/synapse_reader/mod.rs

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,13 @@ pub struct SynapseAccessToken {
232232

233233
/// Row of the `refresh_tokens` table in Synapse.
234234
#[derive(Clone, Debug, FromRow, PartialEq, Eq, PartialOrd, Ord)]
235-
pub struct SynapseRefreshToken {
236-
pub id: i64,
235+
pub struct SynapseRefreshableTokenPair {
237236
pub user_id: FullUserId,
238237
pub device_id: String,
239-
pub token: String,
238+
pub access_token: String,
239+
pub refresh_token: String,
240+
pub valid_until_ms: Option<MillisecondsTimestamp>,
241+
pub last_validated: Option<MillisecondsTimestamp>,
240242
}
241243

242244
/// List of Synapse tables that we should acquire an `EXCLUSIVE` lock on.
@@ -412,45 +414,42 @@ impl<'conn> SynapseReader<'conn> {
412414
.map_err(|err| err.into_database("reading Synapse devices"))
413415
}
414416

415-
/// Reads access tokens from the Synapse database.
416-
/// This does not include access tokens used for puppetting users, as those are not supported by MAS.
417-
/// This also does not include access tokens which have been made obsolete
418-
/// by using the associated refresh token and then acknowledging the
419-
/// successor access token by using it to authenticate a request.
420-
pub fn read_access_tokens(
417+
/// Reads unrefreshable access tokens from the Synapse database.
418+
/// This does not include access tokens used for puppetting users, as those
419+
/// are not supported by MAS.
420+
pub fn read_unrefreshable_access_tokens(
421421
&mut self,
422422
) -> impl Stream<Item = Result<SynapseAccessToken, Error>> + '_ {
423423
sqlx::query_as(
424424
"
425425
SELECT
426426
at0.user_id, at0.device_id, at0.token, at0.valid_until_ms, at0.last_validated
427427
FROM access_tokens at0
428-
LEFT JOIN refresh_tokens rt0 ON at0.refresh_token_id = rt0.id
429-
LEFT JOIN access_tokens at1 ON rt0.next_token_id = at1.refresh_token_id
430-
WHERE at0.puppets_user_id IS NULL AND (NOT at1.used OR at1.used IS NULL)
428+
WHERE at0.puppets_user_id IS NULL AND at0.refresh_token_id IS NULL
431429
",
432430
)
433431
.fetch(&mut *self.txn)
434432
.map_err(|err| err.into_database("reading Synapse access tokens"))
435433
}
436434

437-
/// Reads refresh tokens from the Synapse database.
438-
/// This also does not include refresh tokens which have been made obsolete
435+
/// Reads (access token, refresh token) pairs from the Synapse database.
436+
/// This does not include token pairs which have been made obsolete
439437
/// by using the refresh token and then acknowledging the
440438
/// successor access token by using it to authenticate a request.
441439
///
442440
/// The `expiry_ts` and `ultimate_session_expiry_ts` columns are ignored as
443441
/// they are not implemented in MAS.
444442
/// Further, they are unused by any real-world deployment to the best of
445443
/// our knowledge.
446-
pub fn read_refresh_tokens(
444+
pub fn read_refreshable_token_pairs(
447445
&mut self,
448-
) -> impl Stream<Item = Result<SynapseRefreshToken, Error>> + '_ {
446+
) -> impl Stream<Item = Result<SynapseRefreshableTokenPair, Error>> + '_ {
449447
sqlx::query_as(
450448
"
451449
SELECT
452-
rt0.id, rt0.user_id, rt0.device_id, rt0.token, rt0.next_token_id
450+
rt0.user_id, rt0.device_id, at0.token AS access_token, rt0.token AS refresh_token, at0.valid_until_ms, at0.last_validated
453451
FROM refresh_tokens rt0
452+
LEFT JOIN access_tokens at0 ON at0.refresh_token_id = rt0.id AND at0.user_id = rt0.user_id AND at0.device_id = rt0.device_id
454453
LEFT JOIN access_tokens at1 ON at1.refresh_token_id = rt0.next_token_id
455454
WHERE NOT at1.used OR at1.used IS NULL
456455
",
@@ -470,7 +469,7 @@ mod test {
470469

471470
use crate::{
472471
synapse_reader::{
473-
SynapseAccessToken, SynapseDevice, SynapseExternalId, SynapseRefreshToken,
472+
SynapseAccessToken, SynapseDevice, SynapseExternalId, SynapseRefreshableTokenPair,
474473
SynapseThreepid, SynapseUser,
475474
},
476475
SynapseReader,
@@ -551,7 +550,7 @@ mod test {
551550
.expect("failed to make SynapseReader");
552551

553552
let access_tokens: BTreeSet<SynapseAccessToken> = reader
554-
.read_access_tokens()
553+
.read_unrefreshable_access_tokens()
555554
.try_collect()
556555
.await
557556
.expect("failed to read Synapse access tokens");
@@ -571,7 +570,7 @@ mod test {
571570
.expect("failed to make SynapseReader");
572571

573572
let access_tokens: BTreeSet<SynapseAccessToken> = reader
574-
.read_access_tokens()
573+
.read_unrefreshable_access_tokens()
575574
.try_collect()
576575
.await
577576
.expect("failed to read Synapse access tokens");
@@ -590,18 +589,21 @@ mod test {
590589
.expect("failed to make SynapseReader");
591590

592591
let access_tokens: BTreeSet<SynapseAccessToken> = reader
593-
.read_access_tokens()
592+
.read_unrefreshable_access_tokens()
594593
.try_collect()
595594
.await
596595
.expect("failed to read Synapse access tokens");
597596

598-
let refresh_tokens: BTreeSet<SynapseRefreshToken> = reader
599-
.read_refresh_tokens()
597+
let refresh_tokens: BTreeSet<SynapseRefreshableTokenPair> = reader
598+
.read_refreshable_token_pairs()
600599
.try_collect()
601600
.await
602601
.expect("failed to read Synapse refresh tokens");
603602

604-
assert_debug_snapshot!(access_tokens);
603+
assert!(
604+
access_tokens.is_empty(),
605+
"there are no unrefreshable access tokens"
606+
);
605607
assert_debug_snapshot!(refresh_tokens);
606608
}
607609
}

crates/syn2mas/src/synapse_reader/snapshots/syn2mas__synapse_reader__test__read_access_and_refresh_tokens-2.snap

Lines changed: 0 additions & 14 deletions
This file was deleted.

crates/syn2mas/src/synapse_reader/snapshots/syn2mas__synapse_reader__test__read_access_and_refresh_tokens.snap

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
---
22
source: crates/syn2mas/src/synapse_reader/mod.rs
3-
expression: access_tokens
3+
expression: refresh_tokens
44
---
55
{
6-
SynapseAccessToken {
6+
SynapseRefreshableTokenPair {
77
user_id: FullUserId(
88
"@alice:example.com",
99
),
10-
device_id: Some(
11-
"ADEVICE",
12-
),
13-
token: "syt_AAAAAAAAAAAAAA_AAAA",
10+
device_id: "ADEVICE",
11+
access_token: "syt_AAAAAAAAAAAAAA_AAAA",
12+
refresh_token: "syr_cccccccccccc_cccc",
1413
valid_until_ms: None,
1514
last_validated: None,
1615
},

0 commit comments

Comments
 (0)