Skip to content

Commit 23797ee

Browse files
authored
Merge branch 'main' into quenting/rust-1.87
2 parents aee585c + b8480b1 commit 23797ee

File tree

17 files changed

+711
-99
lines changed

17 files changed

+711
-99
lines changed

crates/cli/src/commands/manage.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ enum Subcommand {
149149
UnlockUser {
150150
/// User to unlock
151151
username: String,
152+
153+
/// Whether to reactivate the user if it had been deactivated
154+
#[arg(long)]
155+
reactivate: bool,
152156
},
153157

154158
/// Register a user
@@ -535,8 +539,12 @@ impl Options {
535539
Ok(ExitCode::SUCCESS)
536540
}
537541

538-
SC::UnlockUser { username } => {
539-
let _span = info_span!("cli.manage.lock_user", user.username = username).entered();
542+
SC::UnlockUser {
543+
username,
544+
reactivate,
545+
} => {
546+
let _span =
547+
info_span!("cli.manage.unlock_user", user.username = username).entered();
540548
let config = DatabaseConfig::extract_or_default(figment)
541549
.map_err(anyhow::Error::from_boxed)?;
542550
let mut conn = database_connection_from_config(&config).await?;
@@ -549,10 +557,14 @@ impl Options {
549557
.await?
550558
.context("User not found")?;
551559

552-
warn!(%user.id, "User scheduling user reactivation");
553-
repo.queue_job()
554-
.schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user))
555-
.await?;
560+
if reactivate {
561+
warn!(%user.id, "Scheduling user reactivation");
562+
repo.queue_job()
563+
.schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user))
564+
.await?;
565+
} else {
566+
repo.user().unlock(user).await?;
567+
}
556568

557569
repo.into_inner().commit().await?;
558570

crates/handlers/src/admin/v1/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ where
9494
"/users/{id}/deactivate",
9595
post_with(self::users::deactivate, self::users::deactivate_doc),
9696
)
97+
.api_route(
98+
"/users/{id}/reactivate",
99+
post_with(self::users::reactivate, self::users::reactivate_doc),
100+
)
97101
.api_route(
98102
"/users/{id}/lock",
99103
post_with(self::users::lock, self::users::lock_doc),

crates/handlers/src/admin/v1/users/add.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ use aide::{NoApi, OperationIo, transform::TransformOperation};
1010
use axum::{Json, extract::State, response::IntoResponse};
1111
use hyper::StatusCode;
1212
use mas_axum_utils::record_error;
13-
use mas_matrix::HomeserverConnection;
14-
use mas_storage::{
15-
BoxRng,
16-
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
17-
};
13+
use mas_matrix::{HomeserverConnection, ProvisionRequest};
14+
use mas_storage::BoxRng;
1815
use schemars::JsonSchema;
1916
use serde::Deserialize;
2017
use tracing::warn;
@@ -168,9 +165,13 @@ pub async fn handler(
168165

169166
let user = repo.user().add(&mut rng, &clock, params.username).await?;
170167

171-
repo.queue_job()
172-
.schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user))
173-
.await?;
168+
homeserver
169+
.provision_user(&ProvisionRequest::new(
170+
homeserver.mxid(&user.username),
171+
&user.sub,
172+
))
173+
.await
174+
.map_err(RouteError::Homeserver)?;
174175

175176
repo.save().await?;
176177

@@ -183,6 +184,7 @@ pub async fn handler(
183184
#[cfg(test)]
184185
mod tests {
185186
use hyper::{Request, StatusCode};
187+
use mas_matrix::HomeserverConnection;
186188
use mas_storage::{RepositoryAccess, user::UserRepository};
187189
use sqlx::PgPool;
188190

@@ -218,6 +220,11 @@ mod tests {
218220
.unwrap();
219221

220222
assert_eq!(user.username, "alice");
223+
224+
// Check that the user was created on the homeserver
225+
let mxid = state.homeserver_connection.mxid("alice");
226+
let result = state.homeserver_connection.query_user(&mxid).await;
227+
assert!(result.is_ok());
221228
}
222229

223230
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]

crates/handlers/src/admin/v1/users/deactivate.rs

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use mas_storage::{
1212
BoxRng,
1313
queue::{DeactivateUserJob, QueueJobRepositoryExt as _},
1414
};
15+
use schemars::JsonSchema;
16+
use serde::Deserialize;
1517
use tracing::info;
1618
use ulid::Ulid;
1719

@@ -49,18 +51,40 @@ impl IntoResponse for RouteError {
4951
}
5052
}
5153

52-
pub fn doc(operation: TransformOperation) -> TransformOperation {
54+
/// # JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint
55+
#[derive(Default, Deserialize, JsonSchema)]
56+
#[serde(rename = "DeactivateUserRequest")]
57+
pub struct Request {
58+
/// Whether to skip requesting the homeserver to GDPR-erase the user upon
59+
/// deactivation.
60+
#[serde(default)]
61+
skip_erase: bool,
62+
}
63+
64+
pub fn doc(mut operation: TransformOperation) -> TransformOperation {
65+
operation
66+
.inner_mut()
67+
.request_body
68+
.as_mut()
69+
.unwrap()
70+
.as_item_mut()
71+
.unwrap()
72+
.required = false;
73+
5374
operation
5475
.id("deactivateUser")
5576
.summary("Deactivate a user")
56-
.description("Calling this endpoint will lock and deactivate the user, preventing them from doing any action.
57-
This invalidates any existing session, and will ask the homeserver to make them leave all rooms.")
77+
.description(
78+
"Calling this endpoint will deactivate the user, preventing them from doing any action.
79+
This invalidates any existing session, and will ask the homeserver to make them leave all rooms.",
80+
)
5881
.tag("user")
5982
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
6083
// In the samples, the third user is the one locked
6184
let [_alice, _bob, charlie, ..] = User::samples();
6285
let id = charlie.id();
63-
let response = SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate"));
86+
let response =
87+
SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate"));
6488
t.description("User was deactivated").example(response)
6589
})
6690
.response_with::<404, RouteError, _>(|t| {
@@ -76,21 +100,25 @@ pub async fn handler(
76100
}: CallContext,
77101
NoApi(mut rng): NoApi<BoxRng>,
78102
id: UlidPathParam,
103+
body: Option<Json<Request>>,
79104
) -> Result<Json<SingleResponse<User>>, RouteError> {
105+
let Json(params) = body.unwrap_or_default();
80106
let id = *id;
81-
let mut user = repo
107+
let user = repo
82108
.user()
83109
.lookup(id)
84110
.await?
85111
.ok_or(RouteError::NotFound(id))?;
86112

87-
if user.locked_at.is_none() {
88-
user = repo.user().lock(&clock, user).await?;
89-
}
113+
let user = repo.user().deactivate(&clock, user).await?;
90114

91115
info!(%user.id, "Scheduling deactivation of user");
92116
repo.queue_job()
93-
.schedule_job(&mut rng, &clock, DeactivateUserJob::new(&user, true))
117+
.schedule_job(
118+
&mut rng,
119+
&clock,
120+
DeactivateUserJob::new(&user, !params.skip_erase),
121+
)
94122
.await?;
95123

96124
repo.save().await?;
@@ -105,14 +133,13 @@ pub async fn handler(
105133
mod tests {
106134
use chrono::Duration;
107135
use hyper::{Request, StatusCode};
108-
use insta::assert_json_snapshot;
136+
use insta::{allow_duplicates, assert_json_snapshot};
109137
use mas_storage::{Clock, RepositoryAccess, user::UserRepository};
110-
use sqlx::PgPool;
138+
use sqlx::{PgPool, types::Json};
111139

112140
use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
113141

114-
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
115-
async fn test_deactivate_user(pool: PgPool) {
142+
async fn test_deactivate_user_helper(pool: PgPool, skip_erase: Option<bool>) {
116143
setup();
117144
let mut state = TestState::from_pool(pool.clone()).await.unwrap();
118145
let token = state.token_with_scope("urn:mas:admin").await;
@@ -125,19 +152,44 @@ mod tests {
125152
.unwrap();
126153
repo.save().await.unwrap();
127154

128-
let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id))
129-
.bearer(&token)
130-
.empty();
155+
let request =
156+
Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)).bearer(&token);
157+
let request = match skip_erase {
158+
None => request.empty(),
159+
Some(skip_erase) => request.json(serde_json::json!({
160+
"skip_erase": skip_erase,
161+
})),
162+
};
131163
let response = state.request(request).await;
132164
response.assert_status(StatusCode::OK);
133165
let body: serde_json::Value = response.json();
134166

135-
// The locked_at timestamp should be the same as the current time
167+
// The deactivated_at timestamp should be the same as the current time
136168
assert_eq!(
137-
body["data"]["attributes"]["locked_at"],
169+
body["data"]["attributes"]["deactivated_at"],
138170
serde_json::json!(state.clock.now())
139171
);
140172

173+
// Deactivating the user should not lock it
174+
assert_eq!(
175+
body["data"]["attributes"]["locked_at"],
176+
serde_json::Value::Null
177+
);
178+
179+
// It should have scheduled a deactivation job for the user
180+
// XXX: we don't have a good way to look for the deactivation job
181+
let job: Json<serde_json::Value> = sqlx::query_scalar(
182+
"SELECT payload FROM queue_jobs WHERE queue_name = 'deactivate-user'",
183+
)
184+
.fetch_one(&pool)
185+
.await
186+
.expect("Deactivation job to be scheduled");
187+
assert_eq!(job["user_id"], serde_json::json!(user.id));
188+
assert_eq!(
189+
job["hs_erase"],
190+
serde_json::json!(!skip_erase.unwrap_or(false))
191+
);
192+
141193
// Make sure to run the jobs in the queue
142194
state.run_jobs_in_queue().await;
143195

@@ -148,15 +200,15 @@ mod tests {
148200
response.assert_status(StatusCode::OK);
149201
let body: serde_json::Value = response.json();
150202

151-
assert_json_snapshot!(body, @r#"
203+
allow_duplicates!(assert_json_snapshot!(body, @r#"
152204
{
153205
"data": {
154206
"type": "user",
155207
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
156208
"attributes": {
157209
"username": "alice",
158210
"created_at": "2022-01-16T14:40:00Z",
159-
"locked_at": "2022-01-16T14:40:00Z",
211+
"locked_at": null,
160212
"deactivated_at": "2022-01-16T14:40:00Z",
161213
"admin": false
162214
},
@@ -168,7 +220,17 @@ mod tests {
168220
"self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E"
169221
}
170222
}
171-
"#);
223+
"#));
224+
}
225+
226+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
227+
async fn test_deactivate_user(pool: PgPool) {
228+
test_deactivate_user_helper(pool, Option::None).await;
229+
}
230+
231+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
232+
async fn test_deactivate_user_skip_erase(pool: PgPool) {
233+
test_deactivate_user_helper(pool, Option::Some(true)).await;
172234
}
173235

174236
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
@@ -196,10 +258,16 @@ mod tests {
196258
response.assert_status(StatusCode::OK);
197259
let body: serde_json::Value = response.json();
198260

199-
// The locked_at timestamp should be different from the current time
261+
// The deactivated_at timestamp should be the same as the current time
262+
assert_eq!(
263+
body["data"]["attributes"]["deactivated_at"],
264+
serde_json::json!(state.clock.now())
265+
);
266+
267+
// The deactivated_at timestamp should be different from the locked_at timestamp
200268
assert_ne!(
269+
body["data"]["attributes"]["deactivated_at"],
201270
body["data"]["attributes"]["locked_at"],
202-
serde_json::json!(state.clock.now())
203271
);
204272

205273
// Make sure to run the jobs in the queue

crates/handlers/src/admin/v1/users/lock.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,13 @@ pub async fn handler(
7272
id: UlidPathParam,
7373
) -> Result<Json<SingleResponse<User>>, RouteError> {
7474
let id = *id;
75-
let mut user = repo
75+
let user = repo
7676
.user()
7777
.lookup(id)
7878
.await?
7979
.ok_or(RouteError::NotFound(id))?;
8080

81-
if user.locked_at.is_none() {
82-
user = repo.user().lock(&clock, user).await?;
83-
}
81+
let user = repo.user().lock(&clock, user).await?;
8482

8583
repo.save().await?;
8684

@@ -157,6 +155,10 @@ mod tests {
157155
body["data"]["attributes"]["locked_at"],
158156
serde_json::json!(state.clock.now())
159157
);
158+
assert_ne!(
159+
body["data"]["attributes"]["locked_at"],
160+
serde_json::Value::Null
161+
);
160162
}
161163

162164
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]

crates/handlers/src/admin/v1/users/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod deactivate;
1010
mod get;
1111
mod list;
1212
mod lock;
13+
mod reactivate;
1314
mod set_admin;
1415
mod set_password;
1516
mod unlock;
@@ -21,6 +22,7 @@ pub use self::{
2122
get::{doc as get_doc, handler as get},
2223
list::{doc as list_doc, handler as list},
2324
lock::{doc as lock_doc, handler as lock},
25+
reactivate::{doc as reactivate_doc, handler as reactivate},
2426
set_admin::{doc as set_admin_doc, handler as set_admin},
2527
set_password::{doc as set_password_doc, handler as set_password},
2628
unlock::{doc as unlock_doc, handler as unlock},

0 commit comments

Comments
 (0)