Skip to content

Commit b83c747

Browse files
authored
Allow skipping GDPR-erasure when deactivating a user through the admin API (#4744)
2 parents 176c059 + 49f2dae commit b83c747

File tree

2 files changed

+86
-11
lines changed

2 files changed

+86
-11
lines changed

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

Lines changed: 66 additions & 11 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,7 +51,26 @@ 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")
@@ -79,7 +100,9 @@ pub async fn handler(
79100
}: CallContext,
80101
NoApi(mut rng): NoApi<BoxRng>,
81102
id: UlidPathParam,
103+
body: Option<Json<Request>>,
82104
) -> Result<Json<SingleResponse<User>>, RouteError> {
105+
let Json(params) = body.unwrap_or_default();
83106
let id = *id;
84107
let user = repo
85108
.user()
@@ -91,7 +114,11 @@ pub async fn handler(
91114

92115
info!(%user.id, "Scheduling deactivation of user");
93116
repo.queue_job()
94-
.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+
)
95122
.await?;
96123

97124
repo.save().await?;
@@ -106,14 +133,13 @@ pub async fn handler(
106133
mod tests {
107134
use chrono::Duration;
108135
use hyper::{Request, StatusCode};
109-
use insta::assert_json_snapshot;
136+
use insta::{allow_duplicates, assert_json_snapshot};
110137
use mas_storage::{Clock, RepositoryAccess, user::UserRepository};
111-
use sqlx::PgPool;
138+
use sqlx::{PgPool, types::Json};
112139

113140
use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
114141

115-
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
116-
async fn test_deactivate_user(pool: PgPool) {
142+
async fn test_deactivate_user_helper(pool: PgPool, skip_erase: Option<bool>) {
117143
setup();
118144
let mut state = TestState::from_pool(pool.clone()).await.unwrap();
119145
let token = state.token_with_scope("urn:mas:admin").await;
@@ -126,9 +152,14 @@ mod tests {
126152
.unwrap();
127153
repo.save().await.unwrap();
128154

129-
let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id))
130-
.bearer(&token)
131-
.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+
};
132163
let response = state.request(request).await;
133164
response.assert_status(StatusCode::OK);
134165
let body: serde_json::Value = response.json();
@@ -145,6 +176,20 @@ mod tests {
145176
serde_json::Value::Null
146177
);
147178

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+
148193
// Make sure to run the jobs in the queue
149194
state.run_jobs_in_queue().await;
150195

@@ -155,7 +200,7 @@ mod tests {
155200
response.assert_status(StatusCode::OK);
156201
let body: serde_json::Value = response.json();
157202

158-
assert_json_snapshot!(body, @r#"
203+
allow_duplicates!(assert_json_snapshot!(body, @r#"
159204
{
160205
"data": {
161206
"type": "user",
@@ -175,7 +220,17 @@ mod tests {
175220
"self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E"
176221
}
177222
}
178-
"#);
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;
179234
}
180235

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

docs/api/spec.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,15 @@
13591359
"style": "simple"
13601360
}
13611361
],
1362+
"requestBody": {
1363+
"content": {
1364+
"application/json": {
1365+
"schema": {
1366+
"$ref": "#/components/schemas/DeactivateUserRequest"
1367+
}
1368+
}
1369+
}
1370+
},
13621371
"responses": {
13631372
"200": {
13641373
"description": "User was deactivated",
@@ -3943,6 +3952,17 @@
39433952
}
39443953
}
39453954
},
3955+
"DeactivateUserRequest": {
3956+
"title": "JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint",
3957+
"type": "object",
3958+
"properties": {
3959+
"skip_erase": {
3960+
"description": "Whether to skip requesting the homeserver to GDPR-erase the user upon deactivation.",
3961+
"default": false,
3962+
"type": "boolean"
3963+
}
3964+
}
3965+
},
39463966
"UserEmailFilter": {
39473967
"type": "object",
39483968
"properties": {

0 commit comments

Comments
 (0)