diff --git a/crates/storage-pg/migrations/20250130170011_user_is_guest.sql b/crates/storage-pg/migrations/20250130170011_user_is_guest.sql new file mode 100644 index 000000000..1ca8ce573 --- /dev/null +++ b/crates/storage-pg/migrations/20250130170011_user_is_guest.sql @@ -0,0 +1,10 @@ +-- Copyright 2025 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only +-- Please see LICENSE in the repository root for full details. + +ALTER TABLE users + -- Track whether users are guests. + -- Although guest support is not present in MAS yet, syn2mas should import + -- these users and therefore we should track their state. + ADD COLUMN is_guest BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/crates/syn2mas/.sqlx/query-06cd6bff12000db3e64e98c344cc9e3b5de7af6a497ad84036ae104576ae0575.json b/crates/syn2mas/.sqlx/query-06cd6bff12000db3e64e98c344cc9e3b5de7af6a497ad84036ae104576ae0575.json new file mode 100644 index 000000000..b52cece0d --- /dev/null +++ b/crates/syn2mas/.sqlx/query-06cd6bff12000db3e64e98c344cc9e3b5de7af6a497ad84036ae104576ae0575.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO syn2mas__users (\n user_id, username,\n created_at, locked_at,\n can_request_admin, is_guest)\n SELECT * FROM UNNEST(\n $1::UUID[], $2::TEXT[],\n $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[],\n $5::BOOL[], $6::BOOL[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "UuidArray", + "TextArray", + "TimestamptzArray", + "TimestamptzArray", + "BoolArray", + "BoolArray" + ] + }, + "nullable": [] + }, + "hash": "06cd6bff12000db3e64e98c344cc9e3b5de7af6a497ad84036ae104576ae0575" +} diff --git a/crates/syn2mas/.sqlx/query-c7d2277606b4b326b0c375a056cd57488c930fe431311e53e5e1af6fb1d4e56f.json b/crates/syn2mas/.sqlx/query-c7d2277606b4b326b0c375a056cd57488c930fe431311e53e5e1af6fb1d4e56f.json deleted file mode 100644 index d8be21736..000000000 --- a/crates/syn2mas/.sqlx/query-c7d2277606b4b326b0c375a056cd57488c930fe431311e53e5e1af6fb1d4e56f.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO syn2mas__users\n (user_id, username, created_at, locked_at, can_request_admin)\n SELECT * FROM UNNEST($1::UUID[], $2::TEXT[], $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[], $5::BOOL[])\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "TextArray", - "TimestamptzArray", - "TimestamptzArray", - "BoolArray" - ] - }, - "nullable": [] - }, - "hash": "c7d2277606b4b326b0c375a056cd57488c930fe431311e53e5e1af6fb1d4e56f" -} diff --git a/crates/syn2mas/src/mas_writer/mod.rs b/crates/syn2mas/src/mas_writer/mod.rs index 6706b15b2..f1362702c 100644 --- a/crates/syn2mas/src/mas_writer/mod.rs +++ b/crates/syn2mas/src/mas_writer/mod.rs @@ -199,6 +199,10 @@ pub struct MasNewUser { pub created_at: DateTime, pub locked_at: Option>, pub can_request_admin: bool, + /// Whether the user was a Synapse guest. + /// Although MAS doesn't support guest access, it's still useful to track + /// for the future. + pub is_guest: bool, } pub struct MasNewUserPassword { @@ -563,52 +567,66 @@ impl<'conn> MasWriter<'conn> { #[allow(clippy::missing_panics_doc)] // not a real panic #[tracing::instrument(skip_all, level = Level::DEBUG)] pub fn write_users(&mut self, users: Vec) -> BoxFuture<'_, Result<(), Error>> { - self.writer_pool.spawn_with_connection(move |conn| Box::pin(async move { - // `UNNEST` is a fast way to do bulk inserts, as it lets us send multiple rows in one statement - // without having to change the statement SQL thus altering the query plan. - // See . - // In the future we could consider using sqlx's support for `PgCopyIn` / the `COPY FROM STDIN` statement, - // which is allegedly the best for insert performance, but is less simple to encode. - if users.is_empty() { - return Ok(()); - } + self.writer_pool + .spawn_with_connection(move |conn| { + Box::pin(async move { + // `UNNEST` is a fast way to do bulk inserts, as it lets us send multiple rows + // in one statement without having to change the statement + // SQL thus altering the query plan. See . + // In the future we could consider using sqlx's support for `PgCopyIn` / the + // `COPY FROM STDIN` statement, which is allegedly the best + // for insert performance, but is less simple to encode. + let mut user_ids: Vec = Vec::with_capacity(users.len()); + let mut usernames: Vec = Vec::with_capacity(users.len()); + let mut created_ats: Vec> = Vec::with_capacity(users.len()); + let mut locked_ats: Vec>> = + Vec::with_capacity(users.len()); + let mut can_request_admins: Vec = Vec::with_capacity(users.len()); + let mut is_guests: Vec = Vec::with_capacity(users.len()); + for MasNewUser { + user_id, + username, + created_at, + locked_at, + can_request_admin, + is_guest, + } in users + { + user_ids.push(user_id); + usernames.push(username); + created_ats.push(created_at); + locked_ats.push(locked_at); + can_request_admins.push(can_request_admin); + is_guests.push(is_guest); + } - let mut user_ids: Vec = Vec::with_capacity(users.len()); - let mut usernames: Vec = Vec::with_capacity(users.len()); - let mut created_ats: Vec> = Vec::with_capacity(users.len()); - let mut locked_ats: Vec>> = Vec::with_capacity(users.len()); - let mut can_request_admins: Vec = Vec::with_capacity(users.len()); - for MasNewUser { - user_id, - username, - created_at, - locked_at, - can_request_admin, - } in users - { - user_ids.push(user_id); - usernames.push(username); - created_ats.push(created_at); - locked_ats.push(locked_at); - can_request_admins.push(can_request_admin); - } + sqlx::query!( + r#" + INSERT INTO syn2mas__users ( + user_id, username, + created_at, locked_at, + can_request_admin, is_guest) + SELECT * FROM UNNEST( + $1::UUID[], $2::TEXT[], + $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[], + $5::BOOL[], $6::BOOL[]) + "#, + &user_ids[..], + &usernames[..], + &created_ats[..], + // We need to override the typing for arrays of optionals (sqlx limitation) + &locked_ats[..] as &[Option>], + &can_request_admins[..], + &is_guests[..], + ) + .execute(&mut *conn) + .await + .into_database("writing users to MAS")?; - sqlx::query!( - r#" - INSERT INTO syn2mas__users - (user_id, username, created_at, locked_at, can_request_admin) - SELECT * FROM UNNEST($1::UUID[], $2::TEXT[], $3::TIMESTAMP WITH TIME ZONE[], $4::TIMESTAMP WITH TIME ZONE[], $5::BOOL[]) - "#, - &user_ids[..], - &usernames[..], - &created_ats[..], - // We need to override the typing for arrays of optionals (sqlx limitation) - &locked_ats[..] as &[Option>], - &can_request_admins[..], - ).execute(&mut *conn).await.into_database("writing users to MAS")?; - - Ok(()) - })).boxed() + Ok(()) + }) + }) + .boxed() } /// Write a batch of user passwords to the database. @@ -1197,6 +1215,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1221,6 +1240,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1252,6 +1272,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1285,6 +1306,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1319,6 +1341,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1352,6 +1375,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1389,6 +1413,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); @@ -1438,6 +1463,7 @@ mod test { created_at: DateTime::default(), locked_at: None, can_request_admin: false, + is_guest: false, }]) .await .expect("failed to write user"); diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user.snap index 62d12ad5a..3bb6d1c07 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user.snap @@ -5,6 +5,7 @@ expression: db_snapshot users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_access_token.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_access_token.snap index 81cb99515..e1c069c2e 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_access_token.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_access_token.snap @@ -23,6 +23,7 @@ compat_sessions: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_device.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_device.snap index 6e9dd8e14..1e7e95d9e 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_device.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_device.snap @@ -17,6 +17,7 @@ compat_sessions: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_email.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_email.snap index 6d0e5b6a9..c4f7d2247 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_email.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_email.snap @@ -11,6 +11,7 @@ user_emails: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_password.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_password.snap index 13f8db6a8..4c1253026 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_password.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_password.snap @@ -12,6 +12,7 @@ user_passwords: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_refresh_token.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_refresh_token.snap index 0e7714f96..71ad9efee 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_refresh_token.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_refresh_token.snap @@ -30,6 +30,7 @@ compat_sessions: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_unsupported_threepid.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_unsupported_threepid.snap index 79805555a..3b70125f8 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_unsupported_threepid.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_unsupported_threepid.snap @@ -10,6 +10,7 @@ user_unsupported_third_party_ids: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap index 76393c6ca..821eb9e17 100644 --- a/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap +++ b/crates/syn2mas/src/mas_writer/snapshots/syn2mas__mas_writer__test__write_user_with_upstream_provider_link.snap @@ -36,6 +36,7 @@ upstream_oauth_providers: users: - can_request_admin: "false" created_at: "1970-01-01 00:00:00+00" + is_guest: "false" locked_at: ~ primary_user_email_id: ~ user_id: 00000000-0000-0000-0000-000000000001 diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index 90e06b43e..dff034ca9 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -687,6 +687,7 @@ fn transform_user( created_at: user.creation_ts.into(), locked_at: bool::from(user.deactivated).then_some(user.creation_ts.into()), can_request_admin: bool::from(user.admin), + is_guest: bool::from(user.is_guest), }; let mas_password = user diff --git a/crates/syn2mas/src/synapse_reader/mod.rs b/crates/syn2mas/src/synapse_reader/mod.rs index 93f7fcf53..5d5d3303f 100644 --- a/crates/syn2mas/src/synapse_reader/mod.rs +++ b/crates/syn2mas/src/synapse_reader/mod.rs @@ -187,8 +187,10 @@ pub struct SynapseUser { pub deactivated: SynapseBool, /// When the user was created pub creation_ts: SecondsTimestamp, - // TODO ... - // TODO is_guest + /// Whether the user is a guest. + /// Note that not all numeric user IDs are guests; guests can upgrade their + /// account! + pub is_guest: SynapseBool, // TODO do we care about upgrade_ts (users who upgraded from guest accounts to real accounts) } @@ -335,7 +337,7 @@ impl<'conn> SynapseReader<'conn> { let users: i64 = sqlx::query_scalar( " SELECT COUNT(1) FROM users - WHERE appservice_id IS NULL AND is_guest = 0 + WHERE appservice_id IS NULL ", ) .fetch_one(&mut *self.txn) @@ -361,9 +363,9 @@ impl<'conn> SynapseReader<'conn> { sqlx::query_as( " SELECT - name, password_hash, admin, deactivated, creation_ts + name, password_hash, admin, deactivated, creation_ts, is_guest FROM users - WHERE appservice_id IS NULL AND is_guest = 0 + WHERE appservice_id IS NULL ", ) .fetch(&mut *self.txn) diff --git a/crates/syn2mas/src/synapse_reader/snapshots/syn2mas__synapse_reader__test__read_users.snap b/crates/syn2mas/src/synapse_reader/snapshots/syn2mas__synapse_reader__test__read_users.snap index a1ec760f1..77fb9e347 100644 --- a/crates/syn2mas/src/synapse_reader/snapshots/syn2mas__synapse_reader__test__read_users.snap +++ b/crates/syn2mas/src/synapse_reader/snapshots/syn2mas__synapse_reader__test__read_users.snap @@ -19,5 +19,8 @@ expression: users creation_ts: SecondsTimestamp( 2018-06-30T21:26:02Z, ), + is_guest: SynapseBool( + false, + ), }, }