Support reading and writing guests

This commit is contained in:
Olivier 'reivilibre
2025-01-30 17:17:32 +00:00
committed by Quentin Gliech
parent dc305ddc40
commit ac58b4f326
15 changed files with 118 additions and 67 deletions

View File

@@ -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;

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -199,6 +199,10 @@ pub struct MasNewUser {
pub created_at: DateTime<Utc>,
pub locked_at: Option<DateTime<Utc>>,
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<MasNewUser>) -> 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 <https://github.com/launchbadge/sqlx/blob/main/FAQ.md#how-can-i-bind-an-array-to-a-values-clause-how-can-i-do-bulk-inserts>.
// 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 <https://github.com/launchbadge/sqlx/blob/main/FAQ.md#how-can-i-bind-an-array-to-a-values-clause-how-can-i-do-bulk-inserts>.
// 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<Uuid> = Vec::with_capacity(users.len());
let mut usernames: Vec<String> = Vec::with_capacity(users.len());
let mut created_ats: Vec<DateTime<Utc>> = Vec::with_capacity(users.len());
let mut locked_ats: Vec<Option<DateTime<Utc>>> =
Vec::with_capacity(users.len());
let mut can_request_admins: Vec<bool> = Vec::with_capacity(users.len());
let mut is_guests: Vec<bool> = 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<Uuid> = Vec::with_capacity(users.len());
let mut usernames: Vec<String> = Vec::with_capacity(users.len());
let mut created_ats: Vec<DateTime<Utc>> = Vec::with_capacity(users.len());
let mut locked_ats: Vec<Option<DateTime<Utc>>> = Vec::with_capacity(users.len());
let mut can_request_admins: Vec<bool> = 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<DateTime<Utc>>],
&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<DateTime<Utc>>],
&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");

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -19,5 +19,8 @@ expression: users
creation_ts: SecondsTimestamp(
2018-06-30T21:26:02Z,
),
is_guest: SynapseBool(
false,
),
},
}