Skip to content

Commit d807975

Browse files
committed
Decouple (un)locking from (re/de)activation
Unify the admin API, CLI, and GraphQL API in not having the unlock command also reactivate, or the deactivate command also lock. Still let the unlock command of the CLI and GraphQL API to also reactivate the target user, albeit as a non-default option.
1 parent df80326 commit d807975

File tree

14 files changed

+99
-270
lines changed

14 files changed

+99
-270
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
@@ -527,8 +531,12 @@ impl Options {
527531
Ok(ExitCode::SUCCESS)
528532
}
529533

530-
SC::UnlockUser { username } => {
531-
let _span = info_span!("cli.manage.lock_user", user.username = username).entered();
534+
SC::UnlockUser {
535+
username,
536+
reactivate,
537+
} => {
538+
let _span =
539+
info_span!("cli.manage.unlock_user", user.username = username).entered();
532540
let config = DatabaseConfig::extract_or_default(figment)?;
533541
let mut conn = database_connection_from_config(&config).await?;
534542
let txn = conn.begin().await?;
@@ -540,10 +548,14 @@ impl Options {
540548
.await?
541549
.context("User not found")?;
542550

543-
warn!(%user.id, "User scheduling user reactivation");
544-
repo.queue_job()
545-
.schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user))
546-
.await?;
551+
if reactivate {
552+
warn!(%user.id, "Scheduling user reactivation");
553+
repo.queue_job()
554+
.schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user))
555+
.await?;
556+
} else {
557+
repo.user().unlock(user).await?;
558+
}
547559

548560
repo.into_inner().commit().await?;
549561

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

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

@@ -51,36 +49,21 @@ impl IntoResponse for RouteError {
5149
}
5250
}
5351

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 locking the user before deactivation.
59-
#[serde(default)]
60-
skip_lock: bool,
61-
}
62-
63-
pub fn doc(mut operation: TransformOperation) -> TransformOperation {
64-
operation
65-
.inner_mut()
66-
.request_body
67-
.as_mut()
68-
.unwrap()
69-
.as_item_mut()
70-
.unwrap()
71-
.required = false;
72-
52+
pub fn doc(operation: TransformOperation) -> TransformOperation {
7353
operation
7454
.id("deactivateUser")
7555
.summary("Deactivate a user")
76-
.description("Calling this endpoint will lock and deactivate the user, preventing them from doing any action.
77-
This invalidates any existing session, and will ask the homeserver to make them leave all rooms.")
56+
.description(
57+
"Calling this endpoint will deactivate the user, preventing them from doing any action.
58+
This invalidates any existing session, and will ask the homeserver to make them leave all rooms.",
59+
)
7860
.tag("user")
7961
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
8062
// In the samples, the third user is the one locked
8163
let [_alice, _bob, charlie, ..] = User::samples();
8264
let id = charlie.id();
83-
let response = SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate"));
65+
let response =
66+
SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate"));
8467
t.description("User was deactivated").example(response)
8568
})
8669
.response_with::<404, RouteError, _>(|t| {
@@ -96,19 +79,15 @@ pub async fn handler(
9679
}: CallContext,
9780
NoApi(mut rng): NoApi<BoxRng>,
9881
id: UlidPathParam,
99-
body: Option<Json<Request>>,
10082
) -> Result<Json<SingleResponse<User>>, RouteError> {
101-
let Json(params) = body.unwrap_or_default();
10283
let id = *id;
103-
let mut user = repo
84+
let user = repo
10485
.user()
10586
.lookup(id)
10687
.await?
10788
.ok_or(RouteError::NotFound(id))?;
10889

109-
if !params.skip_lock && user.locked_at.is_none() {
110-
user = repo.user().lock(&clock, user).await?;
111-
}
90+
let user = repo.user().deactivate(&clock, user).await?;
11291

11392
info!(%user.id, "Scheduling deactivation of user");
11493
repo.queue_job()
@@ -127,13 +106,14 @@ pub async fn handler(
127106
mod tests {
128107
use chrono::Duration;
129108
use hyper::{Request, StatusCode};
130-
use insta::{allow_duplicates, assert_json_snapshot};
109+
use insta::assert_json_snapshot;
131110
use mas_storage::{Clock, RepositoryAccess, user::UserRepository};
132111
use sqlx::PgPool;
133112

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

136-
async fn test_deactivate_user_helper(pool: PgPool, skip_lock: Option<bool>) {
115+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
116+
async fn test_deactivate_user(pool: PgPool) {
137117
setup();
138118
let mut state = TestState::from_pool(pool.clone()).await.unwrap();
139119
let token = state.token_with_scope("urn:mas:admin").await;
@@ -146,27 +126,23 @@ mod tests {
146126
.unwrap();
147127
repo.save().await.unwrap();
148128

149-
let request =
150-
Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)).bearer(&token);
151-
let request = match skip_lock {
152-
None => request.empty(),
153-
Some(skip_lock) => request.json(serde_json::json!({
154-
"skip_lock": skip_lock,
155-
})),
156-
};
129+
let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id))
130+
.bearer(&token)
131+
.empty();
157132
let response = state.request(request).await;
158133
response.assert_status(StatusCode::OK);
159134
let body: serde_json::Value = response.json();
160135

161-
// The locked_at timestamp should be the same as the current time, or null if
162-
// not locked
136+
// The deactivated_at timestamp should be the same as the current time
137+
assert_eq!(
138+
body["data"]["attributes"]["deactivated_at"],
139+
serde_json::json!(state.clock.now())
140+
);
141+
142+
// Deactivating the user should not lock it
163143
assert_eq!(
164144
body["data"]["attributes"]["locked_at"],
165-
if skip_lock.unwrap_or(false) {
166-
serde_json::Value::Null
167-
} else {
168-
serde_json::json!(state.clock.now())
169-
}
145+
serde_json::Value::Null
170146
);
171147

172148
// Make sure to run the jobs in the queue
@@ -179,15 +155,15 @@ mod tests {
179155
response.assert_status(StatusCode::OK);
180156
let body: serde_json::Value = response.json();
181157

182-
allow_duplicates!(assert_json_snapshot!(body, @r#"
158+
assert_json_snapshot!(body, @r#"
183159
{
184160
"data": {
185161
"type": "user",
186162
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
187163
"attributes": {
188164
"username": "alice",
189165
"created_at": "2022-01-16T14:40:00Z",
190-
"locked_at": "2022-01-16T14:40:00Z",
166+
"locked_at": null,
191167
"deactivated_at": "2022-01-16T14:40:00Z",
192168
"admin": false
193169
},
@@ -199,17 +175,7 @@ mod tests {
199175
"self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E"
200176
}
201177
}
202-
"#));
203-
}
204-
205-
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
206-
async fn test_deactivate_user(pool: PgPool) {
207-
test_deactivate_user_helper(pool, Option::None).await;
208-
}
209-
210-
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
211-
async fn test_deactivate_user_skip_lock(pool: PgPool) {
212-
test_deactivate_user_helper(pool, Option::Some(true)).await;
178+
"#);
213179
}
214180

215181
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
@@ -237,14 +203,16 @@ mod tests {
237203
response.assert_status(StatusCode::OK);
238204
let body: serde_json::Value = response.json();
239205

240-
// The locked_at timestamp should be different from the current time
241-
assert_ne!(
242-
body["data"]["attributes"]["locked_at"],
206+
// The deactivated_at timestamp should be the same as the current time
207+
assert_eq!(
208+
body["data"]["attributes"]["deactivated_at"],
243209
serde_json::json!(state.clock.now())
244210
);
211+
212+
// The deactivated_at timestamp should be different from the locked_at timestamp
245213
assert_ne!(
214+
body["data"]["attributes"]["deactivated_at"],
246215
body["data"]["attributes"]["locked_at"],
247-
serde_json::Value::Null
248216
);
249217

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

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

Lines changed: 2 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

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ pub fn doc(operation: TransformOperation) -> TransformOperation {
5353
operation
5454
.id("reactivateUser")
5555
.summary("Reactivate a user")
56-
.description("Calling this endpoint will reactivate a deactivated user, both locally and on the Matrix homeserver.")
56+
.description("Calling this endpoint will reactivate a deactivated user.
57+
This DOES NOT unlock a locked user, which is still prevented from doing any action until it is explicitly unlocked.")
5758
.tag("user")
5859
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
5960
// In the samples, the third user is the one locked

0 commit comments

Comments
 (0)