Skip to content
This repository has been archived by the owner on Sep 10, 2024. It is now read-only.

Allow reactivating users on the homeserver #2970

Merged
merged 3 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions crates/cli/src/commands/manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion crates/handlers/src/graphql/model/matrix.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -25,6 +25,9 @@ pub struct MatrixUser {

/// The avatar URL of the user, if any.
avatar_url: Option<String>,

/// Whether the user is deactivated on the homeserver.
deactivated: bool,
}

impl MatrixUser {
Expand All @@ -40,6 +43,7 @@ impl MatrixUser {
mxid,
display_name: info.displayname,
avatar_url: info.avatar_url,
deactivated: info.deactivated,
})
}
}
80 changes: 80 additions & 0 deletions crates/handlers/src/graphql/mutations/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,52 @@ impl LockUserPayload {
}
}

/// The input for the `unlockUser` mutation.
#[derive(InputObject)]
struct UnlockUserInput {
/// The ID of the user to unlock
user_id: ID,
}

/// The status of the `unlockUser` mutation.
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum UnlockUserStatus {
/// The user was unlocked.
Unlocked,

/// The user was not found.
NotFound,
}

/// The payload for the `unlockUser` mutation.
#[derive(Description)]
enum UnlockUserPayload {
/// The user was unlocked.
Unlocked(mas_data_model::User),

/// The user was not found.
NotFound,
}

#[Object(use_type_description)]
impl UnlockUserPayload {
/// Status of the operation
async fn status(&self) -> UnlockUserStatus {
match self {
Self::Unlocked(_) => UnlockUserStatus::Unlocked,
Self::NotFound => UnlockUserStatus::NotFound,
}
}

/// The user that was unlocked.
async fn user(&self) -> Option<User> {
match self {
Self::Unlocked(user) => Some(User(user.clone())),
Self::NotFound => None,
}
}
}

/// The input for the `setCanRequestAdmin` mutation.
#[derive(InputObject)]
struct SetCanRequestAdminInput {
Expand Down Expand Up @@ -382,6 +428,40 @@ impl UserMutations {
Ok(LockUserPayload::Locked(user))
}

/// Unlock a user. This is only available to administrators.
async fn unlock_user(
&self,
ctx: &Context<'_>,
input: UnlockUserInput,
) -> Result<UnlockUserPayload, async_graphql::Error> {
let state = ctx.state();
let requester = ctx.requester();
let matrix = state.homeserver_connection();

if !requester.is_admin() {
return Err(async_graphql::Error::new("Unauthorized"));
}

let mut repo = state.repository().await?;
let user_id = NodeType::User.extract_ulid(&input.user_id)?;
let user = repo.user().lookup(user_id).await?;

let Some(user) = user else {
return Ok(UnlockUserPayload::NotFound);
};

// Call the homeserver synchronously to unlock the user
let mxid = matrix.mxid(&user.username);
matrix.reactivate_user(&mxid).await?;

// Now unlock the user in our database
let user = repo.user().unlock(user).await?;

repo.save().await?;

Ok(UnlockUserPayload::Unlocked(user))
}

/// Set whether a user can request admin. This is only available to
/// administrators.
async fn set_can_request_admin(
Expand Down
50 changes: 49 additions & 1 deletion crates/matrix-synapse/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -131,6 +131,9 @@ struct SynapseUser {

#[serde(default, skip_serializing_if = "Option::is_none")]
external_ids: Option<Vec<ExternalID>>,

#[serde(default, skip_serializing_if = "Option::is_none")]
deactivated: Option<bool>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -214,6 +217,7 @@ impl HomeserverConnection for SynapseConnection {
Ok(MatrixUser {
displayname: body.display_name,
avatar_url: body.avatar_url,
deactivated: body.deactivated.unwrap_or(false),
})
}

Expand Down Expand Up @@ -539,6 +543,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,
Expand Down
23 changes: 22 additions & 1 deletion crates/matrix/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -26,6 +26,7 @@ pub type BoxHomeserverConnection<Error = anyhow::Error> =
pub struct MatrixUser {
pub displayname: Option<String>,
pub avatar_url: Option<String>,
pub deactivated: bool,
}

#[derive(Debug, Default)]
Expand Down Expand Up @@ -288,6 +289,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
Expand Down Expand Up @@ -362,6 +375,10 @@ impl<T: HomeserverConnection + Send + Sync + ?Sized> 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
}
Expand Down Expand Up @@ -412,6 +429,10 @@ impl<T: HomeserverConnection + ?Sized> HomeserverConnection for Arc<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
}
Expand Down
14 changes: 13 additions & 1 deletion crates/matrix/src/mock.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -27,6 +27,7 @@ struct MockUser {
devices: HashSet<String>,
emails: Option<Vec<String>>,
cross_signing_reset_allowed: bool,
deactivated: bool,
}

/// A mock implementation of a [`HomeserverConnection`], which never fails and
Expand Down Expand Up @@ -69,6 +70,7 @@ impl crate::HomeserverConnection for HomeserverConnection {
Ok(MatrixUser {
displayname: user.displayname.clone(),
avatar_url: user.avatar_url.clone(),
deactivated: user.deactivated,
})
}

Expand All @@ -82,6 +84,7 @@ impl crate::HomeserverConnection for HomeserverConnection {
devices: HashSet::new(),
emails: None,
cross_signing_reset_allowed: false,
deactivated: false,
});

anyhow::ensure!(
Expand Down Expand Up @@ -140,6 +143,7 @@ impl crate::HomeserverConnection for HomeserverConnection {
let user = users.get_mut(mxid).context("User not found")?;
user.devices.clear();
user.emails = None;
user.deactivated = true;
if erase {
user.avatar_url = None;
user.displayname = None;
Expand All @@ -148,6 +152,14 @@ impl crate::HomeserverConnection for HomeserverConnection {
Ok(())
}

async fn reactivate_user(&self, mxid: &str) -> Result<(), Self::Error> {
let mut users = self.users.write().await;
let user = users.get_mut(mxid).context("User not found")?;
user.deactivated = false;

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")?;
Expand Down
30 changes: 29 additions & 1 deletion crates/storage/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -489,6 +517,6 @@ mod jobs {
}

pub use self::jobs::{
DeactivateUserJob, DeleteDeviceJob, ProvisionDeviceJob, ProvisionUserJob,
DeactivateUserJob, DeleteDeviceJob, ProvisionDeviceJob, ProvisionUserJob, ReactivateUserJob,
SendAccountRecoveryEmailsJob, SyncDevicesJob, VerifyEmailJob,
};
Loading
Loading