1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-09 04:22:45 +03:00

Add a way to reactivate users on the homeserver

This commit is contained in:
Quentin Gliech
2024-07-12 14:15:58 +02:00
parent 3eab10672f
commit 0207495225
6 changed files with 153 additions and 11 deletions

View File

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

View File

@@ -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<Vec<ExternalID>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
deactivated: Option<bool>,
}
#[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,

View File

@@ -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<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
}
@@ -412,6 +428,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
}

View File

@@ -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")?;

View File

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

View File

@@ -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<ReactivateUserJob>,
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<TokioExecutor>,
@@ -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)
}