diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 50aa7250..b8d6c6a5 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -27,7 +27,9 @@ use mas_matrix::HomeserverConnection; use mas_matrix_synapse::SynapseConnection; use mas_storage::{ compat::{CompatAccessTokenRepository, CompatSessionRepository}, - job::{DeactivateUserJob, JobRepositoryExt, ProvisionUserJob, SyncDevicesJob}, + job::{ + DeactivateUserJob, JobRepositoryExt, ProvisionUserJob, ReactivateUserJob, SyncDevicesJob, + }, user::{UserEmailRepository, UserPasswordRepository, UserRepository}, Clock, RepositoryAccess, SystemClock, }; @@ -488,9 +490,11 @@ impl Options { .await? .context("User not found")?; - info!(%user.id, "Unlocking user"); + warn!(%user.id, "User scheduling user reactivation"); + repo.job() + .schedule_job(ReactivateUserJob::new(&user)) + .await?; - repo.user().unlock(user).await?; repo.into_inner().commit().await?; Ok(ExitCode::SUCCESS) diff --git a/crates/matrix-synapse/src/lib.rs b/crates/matrix-synapse/src/lib.rs index 77a3c711..626e6a38 100644 --- a/crates/matrix-synapse/src/lib.rs +++ b/crates/matrix-synapse/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. +// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -131,6 +131,9 @@ struct SynapseUser { #[serde(default, skip_serializing_if = "Option::is_none")] external_ids: Option>, + + #[serde(default, skip_serializing_if = "Option::is_none")] + deactivated: Option, } #[derive(Deserialize)] @@ -539,6 +542,50 @@ impl HomeserverConnection for SynapseConnection { Ok(()) } + #[tracing::instrument( + name = "homeserver.reactivate_user", + skip_all, + fields( + matrix.homeserver = self.homeserver, + matrix.mxid = mxid, + ), + err(Debug), + )] + async fn reactivate_user(&self, mxid: &str) -> Result<(), anyhow::Error> { + let body = SynapseUser { + deactivated: Some(false), + ..SynapseUser::default() + }; + + let mut client = self + .http_client_factory + .client("homeserver.reactivate_user") + .request_bytes_to_body() + .json_request() + .response_body_to_bytes() + .catch_http_errors(catch_homeserver_error); + + let mxid = urlencoding::encode(mxid); + let request = self + .put(&format!("_synapse/admin/v2/users/{mxid}")) + .body(body)?; + + let response = client + .ready() + .await? + .call(request) + .await + .context("Failed to provision user in Synapse")?; + + match response.status() { + StatusCode::CREATED | StatusCode::OK => Ok(()), + code => Err(anyhow::anyhow!( + "Failed to provision user in Synapse: {}", + code + )), + } + } + #[tracing::instrument( name = "homeserver.set_displayname", skip_all, diff --git a/crates/matrix/src/lib.rs b/crates/matrix/src/lib.rs index 6b416092..7ae7538b 100644 --- a/crates/matrix/src/lib.rs +++ b/crates/matrix/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. +// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -288,6 +288,18 @@ pub trait HomeserverConnection: Send + Sync { /// be deleted. async fn delete_user(&self, mxid: &str, erase: bool) -> Result<(), Self::Error>; + /// Reactivate a user on the homeserver. + /// + /// # Parameters + /// + /// * `mxid` - The Matrix ID of the user to reactivate. + /// + /// # Errors + /// + /// Returns an error if the homeserver is unreachable or the user could not + /// be reactivated. + async fn reactivate_user(&self, mxid: &str) -> Result<(), Self::Error>; + /// Set the displayname of a user on the homeserver. /// /// # Parameters @@ -362,6 +374,10 @@ impl HomeserverConnection for &T (**self).delete_user(mxid, erase).await } + async fn reactivate_user(&self, mxid: &str) -> Result<(), Self::Error> { + (**self).reactivate_user(mxid).await + } + async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error> { (**self).set_displayname(mxid, displayname).await } @@ -412,6 +428,10 @@ impl HomeserverConnection for Arc { (**self).delete_user(mxid, erase).await } + async fn reactivate_user(&self, mxid: &str) -> Result<(), Self::Error> { + (**self).reactivate_user(mxid).await + } + async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error> { (**self).set_displayname(mxid, displayname).await } diff --git a/crates/matrix/src/mock.rs b/crates/matrix/src/mock.rs index d7f0421e..c895c722 100644 --- a/crates/matrix/src/mock.rs +++ b/crates/matrix/src/mock.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. +// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -148,6 +148,10 @@ impl crate::HomeserverConnection for HomeserverConnection { Ok(()) } + async fn reactivate_user(&self, _mxid: &str) -> Result<(), Self::Error> { + Ok(()) + } + async fn set_displayname(&self, mxid: &str, displayname: &str) -> Result<(), Self::Error> { let mut users = self.users.write().await; let user = users.get_mut(mxid).context("User not found")?; diff --git a/crates/storage/src/job.rs b/crates/storage/src/job.rs index 962dfe0f..1ac66fb9 100644 --- a/crates/storage/src/job.rs +++ b/crates/storage/src/job.rs @@ -455,6 +455,34 @@ mod jobs { const NAME: &'static str = "deactivate-user"; } + /// A job to reactivate a user + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct ReactivateUserJob { + user_id: Ulid, + } + + impl ReactivateUserJob { + /// Create a new job to reactivate a user + /// + /// # Parameters + /// + /// * `user` - The user to reactivate + #[must_use] + pub fn new(user: &User) -> Self { + Self { user_id: user.id } + } + + /// The ID of the user to reactivate + #[must_use] + pub fn user_id(&self) -> Ulid { + self.user_id + } + } + + impl Job for ReactivateUserJob { + const NAME: &'static str = "reactivate-user"; + } + /// Send account recovery emails #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SendAccountRecoveryEmailsJob { @@ -489,6 +517,6 @@ mod jobs { } pub use self::jobs::{ - DeactivateUserJob, DeleteDeviceJob, ProvisionDeviceJob, ProvisionUserJob, + DeactivateUserJob, DeleteDeviceJob, ProvisionDeviceJob, ProvisionUserJob, ReactivateUserJob, SendAccountRecoveryEmailsJob, SyncDevicesJob, VerifyEmailJob, }; diff --git a/crates/tasks/src/user.rs b/crates/tasks/src/user.rs index 2eb2d3a2..fe64b6c1 100644 --- a/crates/tasks/src/user.rs +++ b/crates/tasks/src/user.rs @@ -1,4 +1,4 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. +// Copyright 2023, 2024 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ use anyhow::Context; use apalis_core::{context::JobContext, executor::TokioExecutor, monitor::Monitor}; use mas_storage::{ - job::{DeactivateUserJob, JobWithSpanContext}, + job::{DeactivateUserJob, JobWithSpanContext, ReactivateUserJob}, user::UserRepository, RepositoryAccess, }; @@ -54,7 +54,8 @@ async fn deactivate_user( // TODO: delete the sessions & access tokens - // Before calling back to the homeserver, commit the changes to the database + // Before calling back to the homeserver, commit the changes to the database, as + // we want the user to be locked out as soon as possible repo.save().await?; let mxid = matrix.mxid(&user.username); @@ -64,6 +65,39 @@ async fn deactivate_user( Ok(()) } +/// Job to reactivate a user, both locally and on the Matrix homeserver. +#[tracing::instrument( + name = "job.reactivate_user", + fields(user.id = %job.user_id()), + skip_all, + err(Debug), +)] +pub async fn reactivate_user( + job: JobWithSpanContext, + ctx: JobContext, +) -> Result<(), anyhow::Error> { + let state = ctx.state(); + let matrix = state.matrix_connection(); + let mut repo = state.repository().await?; + + let user = repo + .user() + .lookup(job.user_id()) + .await? + .context("User not found")?; + + let mxid = matrix.mxid(&user.username); + info!("Reactivating user {} on homeserver", mxid); + matrix.reactivate_user(&mxid).await?; + + // We want to unlock the user from our side only once it has been reactivated on + // the homeserver + let _user = repo.user().unlock(user).await?; + repo.save().await?; + + Ok(()) +} + pub(crate) fn register( suffix: &str, monitor: Monitor, @@ -73,5 +107,10 @@ pub(crate) fn register( let deactivate_user_worker = crate::build!(DeactivateUserJob => deactivate_user, suffix, state, storage_factory); - monitor.register(deactivate_user_worker) + let reactivate_user_worker = + crate::build!(ReactivateUserJob => reactivate_user, suffix, state, storage_factory); + + monitor + .register(deactivate_user_worker) + .register(reactivate_user_worker) }