Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 475a43d

Browse files
committed
admin: check password complexity in password set API
1 parent 8b5d576 commit 475a43d

File tree

2 files changed

+105
-3
lines changed

2 files changed

+105
-3
lines changed

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

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ pub enum RouteError {
3333
#[error(transparent)]
3434
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
3535

36+
#[error("Password is too weak")]
37+
PasswordTooWeak,
38+
3639
#[error("Password hashing failed")]
3740
Password(#[source] anyhow::Error),
3841

@@ -47,6 +50,7 @@ impl IntoResponse for RouteError {
4750
let error = ErrorResponse::from_error(&self);
4851
let status = match self {
4952
Self::Internal(_) | Self::Password(_) => StatusCode::INTERNAL_SERVER_ERROR,
53+
Self::PasswordTooWeak => StatusCode::BAD_REQUEST,
5054
Self::NotFound(_) => StatusCode::NOT_FOUND,
5155
};
5256
(status, Json(error)).into_response()
@@ -64,6 +68,9 @@ pub struct Request {
6468
/// The password to set for the user
6569
#[schemars(example = "password_example")]
6670
password: String,
71+
72+
/// Skip the password complexity check
73+
skip_password_check: Option<bool>,
6774
}
6875

6976
pub fn doc(operation: TransformOperation) -> TransformOperation {
@@ -72,6 +79,10 @@ pub fn doc(operation: TransformOperation) -> TransformOperation {
7279
.summary("Set the password for a user")
7380
.tag("user")
7481
.response_with::<200, StatusCode, _>(|t| t.description("Password was set"))
82+
.response_with::<400, RouteError, _>(|t| {
83+
let response = ErrorResponse::from_error(&RouteError::PasswordTooWeak);
84+
t.description("Password is too weak").example(response)
85+
})
7586
.response_with::<404, RouteError, _>(|t| {
7687
let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
7788
t.description("User was not found").example(response)
@@ -94,6 +105,16 @@ pub async fn handler(
94105
.await?
95106
.ok_or(RouteError::NotFound(*id))?;
96107

108+
let skip_password_check = params.skip_password_check.unwrap_or(false);
109+
tracing::info!(skip_password_check, "skip_password_check");
110+
if !skip_password_check
111+
&& !password_manager
112+
.is_password_complex_enough(&params.password)
113+
.unwrap_or(false)
114+
{
115+
return Err(RouteError::PasswordTooWeak);
116+
}
117+
97118
let password = Zeroizing::new(params.password.into_bytes());
98119
let (version, hashed_password) = password_manager
99120
.hash(&mut rng, password)
@@ -144,7 +165,66 @@ mod tests {
144165
let request = Request::post(format!("/api/admin/v1/users/{user_id}/set-password"))
145166
.bearer(&token)
146167
.json(serde_json::json!({
147-
"password": "hunter2",
168+
"password": "this is a good enough password",
169+
}));
170+
171+
let response = state.request(request).await;
172+
response.assert_status(StatusCode::NO_CONTENT);
173+
174+
// Check that the user now has a password
175+
let mut repo = state.repository().await.unwrap();
176+
let user_password = repo.user_password().active(&user).await.unwrap().unwrap();
177+
let password = Zeroizing::new(b"this is a good enough password".to_vec());
178+
state
179+
.password_manager
180+
.verify(
181+
user_password.version,
182+
password,
183+
user_password.hashed_password,
184+
)
185+
.await
186+
.unwrap();
187+
}
188+
189+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
190+
async fn test_weak_password(pool: PgPool) {
191+
setup();
192+
let mut state = TestState::from_pool(pool).await.unwrap();
193+
let token = state.token_with_scope("urn:mas:admin").await;
194+
195+
// Create a user
196+
let mut repo = state.repository().await.unwrap();
197+
let user = repo
198+
.user()
199+
.add(&mut state.rng(), &state.clock, "alice".to_owned())
200+
.await
201+
.unwrap();
202+
repo.save().await.unwrap();
203+
204+
let user_id = user.id;
205+
206+
// Set a weak password through the API
207+
let request = Request::post(format!("/api/admin/v1/users/{user_id}/set-password"))
208+
.bearer(&token)
209+
.json(serde_json::json!({
210+
"password": "password",
211+
}));
212+
213+
let response = state.request(request).await;
214+
response.assert_status(StatusCode::BAD_REQUEST);
215+
216+
// Check that the user still has a password
217+
let mut repo = state.repository().await.unwrap();
218+
let user_password = repo.user_password().active(&user).await.unwrap();
219+
assert!(user_password.is_none());
220+
repo.save().await.unwrap();
221+
222+
// Now try with the skip_password_check flag
223+
let request = Request::post(format!("/api/admin/v1/users/{user_id}/set-password"))
224+
.bearer(&token)
225+
.json(serde_json::json!({
226+
"password": "password",
227+
"skip_password_check": true,
148228
}));
149229

150230
let response = state.request(request).await;
@@ -153,7 +233,7 @@ mod tests {
153233
// Check that the user now has a password
154234
let mut repo = state.repository().await.unwrap();
155235
let user_password = repo.user_password().active(&user).await.unwrap().unwrap();
156-
let password = Zeroizing::new(b"hunter2".to_vec());
236+
let password = Zeroizing::new(b"password".to_vec());
157237
state
158238
.password_manager
159239
.verify(
@@ -175,7 +255,7 @@ mod tests {
175255
let request = Request::post("/api/admin/v1/users/01040G2081040G2081040G2081/set-password")
176256
.bearer(&token)
177257
.json(serde_json::json!({
178-
"password": "hunter2",
258+
"password": "this is a good enough password",
179259
}));
180260

181261
let response = state.request(request).await;

docs/api/spec.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,23 @@
350350
}
351351
}
352352
},
353+
"400": {
354+
"description": "Password is too weak",
355+
"content": {
356+
"application/json": {
357+
"schema": {
358+
"$ref": "#/components/schemas/ErrorResponse"
359+
},
360+
"example": {
361+
"errors": [
362+
{
363+
"title": "Password is too weak"
364+
}
365+
]
366+
}
367+
}
368+
}
369+
},
353370
"404": {
354371
"description": "User was not found",
355372
"content": {
@@ -963,6 +980,11 @@
963980
"hunter2"
964981
],
965982
"type": "string"
983+
},
984+
"skip_password_check": {
985+
"description": "Skip the password complexity check",
986+
"type": "boolean",
987+
"nullable": true
966988
}
967989
}
968990
},

0 commit comments

Comments
 (0)