Skip to content
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
77 changes: 66 additions & 11 deletions crates/handlers/src/admin/v1/users/deactivate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use mas_storage::{
BoxRng,
queue::{DeactivateUserJob, QueueJobRepositoryExt as _},
};
use schemars::JsonSchema;
use serde::Deserialize;
use tracing::info;
use ulid::Ulid;

Expand Down Expand Up @@ -49,7 +51,26 @@ impl IntoResponse for RouteError {
}
}

pub fn doc(operation: TransformOperation) -> TransformOperation {
/// # JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint
#[derive(Default, Deserialize, JsonSchema)]
#[serde(rename = "DeactivateUserRequest")]
pub struct Request {
/// Whether to skip requesting the homeserver to GDPR-erase the user upon
/// deactivation.
#[serde(default)]
skip_erase: bool,
}

pub fn doc(mut operation: TransformOperation) -> TransformOperation {
operation
.inner_mut()
.request_body
.as_mut()
.unwrap()
.as_item_mut()
.unwrap()
.required = false;

operation
.id("deactivateUser")
.summary("Deactivate a user")
Expand All @@ -76,7 +97,9 @@ pub async fn handler(
}: CallContext,
NoApi(mut rng): NoApi<BoxRng>,
id: UlidPathParam,
body: Option<Json<Request>>,
) -> Result<Json<SingleResponse<User>>, RouteError> {
let Json(params) = body.unwrap_or_default();
let id = *id;
let mut user = repo
.user()
Expand All @@ -90,7 +113,11 @@ pub async fn handler(

info!(%user.id, "Scheduling deactivation of user");
repo.queue_job()
.schedule_job(&mut rng, &clock, DeactivateUserJob::new(&user, true))
.schedule_job(
&mut rng,
&clock,
DeactivateUserJob::new(&user, !params.skip_erase),
)
.await?;

repo.save().await?;
Expand All @@ -105,14 +132,13 @@ pub async fn handler(
mod tests {
use chrono::Duration;
use hyper::{Request, StatusCode};
use insta::assert_json_snapshot;
use insta::{allow_duplicates, assert_json_snapshot};
use mas_storage::{Clock, RepositoryAccess, user::UserRepository};
use sqlx::PgPool;
use sqlx::{PgPool, types::Json};

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

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_deactivate_user(pool: PgPool) {
async fn test_deactivate_user_helper(pool: PgPool, skip_erase: Option<bool>) {
setup();
let mut state = TestState::from_pool(pool.clone()).await.unwrap();
let token = state.token_with_scope("urn:mas:admin").await;
Expand All @@ -125,9 +151,14 @@ mod tests {
.unwrap();
repo.save().await.unwrap();

let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id))
.bearer(&token)
.empty();
let request =
Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)).bearer(&token);
let request = match skip_erase {
None => request.empty(),
Some(skip_erase) => request.json(serde_json::json!({
"skip_erase": skip_erase,
})),
};
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let body: serde_json::Value = response.json();
Expand All @@ -138,6 +169,20 @@ mod tests {
serde_json::json!(state.clock.now())
);

// It should have scheduled a deactivation job for the user
// XXX: we don't have a good way to look for the deactivation job
let job: Json<serde_json::Value> = sqlx::query_scalar(
"SELECT payload FROM queue_jobs WHERE queue_name = 'deactivate-user'",
)
.fetch_one(&pool)
.await
.expect("Deactivation job to be scheduled");
assert_eq!(job["user_id"], serde_json::json!(user.id));
assert_eq!(
job["hs_erase"],
serde_json::json!(!skip_erase.unwrap_or(false))
);

// Make sure to run the jobs in the queue
state.run_jobs_in_queue().await;

Expand All @@ -148,7 +193,7 @@ mod tests {
response.assert_status(StatusCode::OK);
let body: serde_json::Value = response.json();

assert_json_snapshot!(body, @r#"
allow_duplicates!(assert_json_snapshot!(body, @r#"
{
"data": {
"type": "user",
Expand All @@ -168,7 +213,17 @@ mod tests {
"self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E"
}
}
"#);
"#));
}

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_deactivate_user(pool: PgPool) {
test_deactivate_user_helper(pool, Option::None).await;
}

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_deactivate_user_skip_erase(pool: PgPool) {
test_deactivate_user_helper(pool, Option::Some(true)).await;
}

#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
Expand Down
20 changes: 20 additions & 0 deletions docs/api/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,15 @@
"style": "simple"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeactivateUserRequest"
}
}
}
},
"responses": {
"200": {
"description": "User was deactivated",
Expand Down Expand Up @@ -3872,6 +3881,17 @@
}
}
},
"DeactivateUserRequest": {
"title": "JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint",
"type": "object",
"properties": {
"skip_erase": {
"description": "Whether to skip requesting the homeserver to GDPR-erase the user upon deactivation.",
"default": false,
"type": "boolean"
}
}
},
"UserEmailFilter": {
"type": "object",
"properties": {
Expand Down
Loading