Skip to content

Commit 62695c3

Browse files
authored
Fix setting default MFA (#2626)
* fix default mfa * format * Update templates.rs
1 parent 2e81475 commit 62695c3

13 files changed

Lines changed: 355 additions & 48 deletions

File tree

crates/defguard_common/src/db/models/user.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub enum UserError {
5151
EmailMfaError(String),
5252
}
5353

54-
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, ToSchema, Type)]
54+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, ToSchema, Type, Copy)]
5555
#[sqlx(type_name = "mfa_method", rename_all = "snake_case")]
5656
pub enum MFAMethod {
5757
None,
@@ -600,7 +600,7 @@ impl User<Id> {
600600
let res = users
601601
.iter()
602602
.map(|u| UserDiagnostic {
603-
mfa_method: u.mfa_method.clone(),
603+
mfa_method: u.mfa_method,
604604
totp_enabled: u.totp_enabled,
605605
email_mfa_enabled: u.email_mfa_enabled,
606606
mfa_enabled: u.mfa_enabled,

crates/defguard_common/src/types/user_info.rs

Lines changed: 190 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,23 +133,26 @@ impl UserInfo {
133133
Ok(group_diff)
134134
}
135135

136-
/// Copy fields to [`User`]. This function is safe to call by a non-admin user.
137-
pub fn into_user_safe_fields(self, user: &mut User<Id>) -> sqlx::Result<()> {
138-
user.phone = self.phone;
139-
user.mfa_method = self.mfa_method;
136+
/// Copy fields over to the given [`User`].
137+
/// Additional flags control which fields are copied over.
138+
pub fn handle_update_user_fields(
139+
self,
140+
user: &mut User<Id>,
141+
is_admin: bool,
142+
is_updating_self: bool,
143+
) {
144+
if is_admin {
145+
user.username = self.username;
146+
user.last_name = self.last_name;
147+
user.first_name = self.first_name;
148+
user.email = self.email;
149+
}
140150

141-
Ok(())
142-
}
151+
if is_updating_self {
152+
user.mfa_method = self.mfa_method;
153+
}
143154

144-
/// Copy fields to [`User`]. This function should be used by administrators.
145-
pub fn into_user_all_fields(self, user: &mut User<Id>) -> sqlx::Result<()> {
146155
user.phone = self.phone;
147-
user.username = self.username;
148-
user.last_name = self.last_name;
149-
user.first_name = self.first_name;
150-
user.email = self.email;
151-
152-
Ok(())
153156
}
154157
}
155158

@@ -160,6 +163,18 @@ mod test {
160163
use super::*;
161164
use crate::db::setup_pool;
162165

166+
/// Build a minimal `UserInfo` from an existing saved `User<Id>`.
167+
/// Only the fields exercised by `handle_update_user_fields` need to be set
168+
/// here; the rest are left at their DB-loaded defaults.
169+
async fn user_info_from_db(pool: &PgPool, username: &str) -> (UserInfo, User<Id>) {
170+
let user = User::find_by_username(pool, username)
171+
.await
172+
.unwrap()
173+
.unwrap();
174+
let info = UserInfo::from_user(pool, user.clone()).await.unwrap();
175+
(info, user)
176+
}
177+
163178
#[sqlx::test]
164179
async fn test_user_info(_: PgPoolOptions, options: PgConnectOptions) {
165180
let pool = setup_pool(options).await;
@@ -198,12 +213,172 @@ mod test {
198213
.handle_user_groups(&mut transaction, &mut user)
199214
.await
200215
.unwrap();
201-
user_info.into_user_all_fields(&mut user).unwrap();
216+
// admin updating their own account: is_admin=true, is_updating_self=true
217+
user_info.handle_update_user_fields(&mut user, true, true);
202218
transaction.commit().await.unwrap();
203219

204220
assert_eq!(group1.member_usernames(&pool).await.unwrap(), ["hpotter"]);
205221
assert_eq!(group3.member_usernames(&pool).await.unwrap(), ["hpotter"]);
206222
assert!(group2.member_usernames(&pool).await.unwrap().is_empty());
207223
assert!(group4.member_usernames(&pool).await.unwrap().is_empty());
208224
}
225+
226+
// Admin updating another user must be able to change all profile
227+
// fields (username, first/last name, email) and phone, but NOT mfa_method.
228+
#[sqlx::test]
229+
async fn test_handle_update_admin_updating_other_user(
230+
_: PgPoolOptions,
231+
options: PgConnectOptions,
232+
) {
233+
let pool = setup_pool(options).await;
234+
let mut user = User::new(
235+
"hpotter",
236+
Some("pass123"),
237+
"Potter",
238+
"Harry",
239+
"h.potter@hogwart.edu.uk",
240+
Some("+48100200300".to_string()),
241+
)
242+
.save(&pool)
243+
.await
244+
.unwrap();
245+
246+
let (mut info, _) = user_info_from_db(&pool, "hpotter").await;
247+
info.username = "h_potter_new".into();
248+
info.first_name = "UpdatedFirst".into();
249+
info.last_name = "Pot".into();
250+
info.email = "updated@hogwart.edu.uk".into();
251+
info.phone = Some("+48999888777".into());
252+
info.mfa_method = MFAMethod::OneTimePassword;
253+
254+
// is_admin=true, is_updating_self=false (admin editing someone else)
255+
info.handle_update_user_fields(&mut user, true, false);
256+
257+
assert_eq!(user.username, "h_potter_new");
258+
assert_eq!(user.first_name, "UpdatedFirst");
259+
assert_eq!(user.last_name, "Pot");
260+
assert_eq!(user.email, "updated@hogwart.edu.uk");
261+
assert_eq!(user.phone, Some("+48999888777".into()));
262+
// mfa_method must NOT change because is_updating_self=false
263+
assert_eq!(user.mfa_method, MFAMethod::None);
264+
}
265+
266+
// A regular user updating themselves may only change phone and
267+
// mfa_method; name/email fields must be left untouched.
268+
#[sqlx::test]
269+
async fn test_handle_update_non_admin_updating_self(
270+
_: PgPoolOptions,
271+
options: PgConnectOptions,
272+
) {
273+
let pool = setup_pool(options).await;
274+
let mut user = User::new(
275+
"hpotter",
276+
Some("pass123"),
277+
"Potter",
278+
"Harry",
279+
"h.potter@hogwart.edu.uk",
280+
None,
281+
)
282+
.save(&pool)
283+
.await
284+
.unwrap();
285+
286+
let (mut info, _) = user_info_from_db(&pool, "hpotter").await;
287+
info.username = "changed_username".into();
288+
info.first_name = "UpdatedFirst".into();
289+
info.last_name = "UpdatedLast".into();
290+
info.email = "updated@example.com".into();
291+
info.phone = Some("+48111222333".into());
292+
info.mfa_method = MFAMethod::OneTimePassword;
293+
294+
// is_admin=false, is_updating_self=true
295+
info.handle_update_user_fields(&mut user, false, true);
296+
297+
// profile fields must remain unchanged
298+
assert_eq!(user.username, "hpotter");
299+
assert_eq!(user.first_name, "Harry");
300+
assert_eq!(user.last_name, "Potter");
301+
assert_eq!(user.email, "h.potter@hogwart.edu.uk");
302+
// phone and mfa_method are always allowed
303+
assert_eq!(user.phone, Some("+48111222333".into()));
304+
assert_eq!(user.mfa_method, MFAMethod::OneTimePassword);
305+
}
306+
307+
// A non-admin modifying ANOTHER user must not be able to change
308+
// any protected field, and mfa_method must also stay unchanged because
309+
// is_updating_self=false.
310+
#[sqlx::test]
311+
async fn test_handle_update_non_admin_updating_other_user(
312+
_: PgPoolOptions,
313+
options: PgConnectOptions,
314+
) {
315+
let pool = setup_pool(options).await;
316+
let mut user = User::new(
317+
"hpotter",
318+
Some("pass123"),
319+
"Potter",
320+
"Harry",
321+
"h.potter@hogwart.edu.uk",
322+
Some("+48100200300".to_string()),
323+
)
324+
.save(&pool)
325+
.await
326+
.unwrap();
327+
let original_mfa = user.mfa_method;
328+
329+
let (mut info, _) = user_info_from_db(&pool, "hpotter").await;
330+
info.username = "changed_username".into();
331+
info.first_name = "UpdatedFirst".into();
332+
info.last_name = "UpdatedLast".into();
333+
info.email = "updated@example.com".into();
334+
info.phone = Some("+48000000000".into());
335+
info.mfa_method = MFAMethod::OneTimePassword;
336+
337+
// is_admin=false, is_updating_self=false
338+
info.handle_update_user_fields(&mut user, false, false);
339+
340+
// only phone changes; everything else stays the same
341+
assert_eq!(user.username, "hpotter");
342+
assert_eq!(user.first_name, "Harry");
343+
assert_eq!(user.last_name, "Potter");
344+
assert_eq!(user.email, "h.potter@hogwart.edu.uk");
345+
assert_eq!(user.phone, Some("+48000000000".into()));
346+
assert_eq!(user.mfa_method, original_mfa);
347+
}
348+
349+
// Admin updating their own account can change all fields
350+
// including mfa_method.
351+
#[sqlx::test]
352+
async fn test_handle_update_admin_updating_self(_: PgPoolOptions, options: PgConnectOptions) {
353+
let pool = setup_pool(options).await;
354+
let mut user = User::new(
355+
"admin",
356+
Some("pass123"),
357+
"Admin",
358+
"Super",
359+
"admin@defguard",
360+
None,
361+
)
362+
.save(&pool)
363+
.await
364+
.unwrap();
365+
366+
let (mut info, _) = user_info_from_db(&pool, "admin").await;
367+
info.username = "admin_renamed".into();
368+
info.first_name = "NewFirst".into();
369+
info.last_name = "NewLast".into();
370+
info.email = "new@defguard".into();
371+
info.phone = Some("+48777888999".into());
372+
info.mfa_method = MFAMethod::OneTimePassword;
373+
374+
// is_admin=true, is_updating_self=true
375+
info.handle_update_user_fields(&mut user, true, true);
376+
377+
assert_eq!(user.username, "admin_renamed");
378+
assert_eq!(user.first_name, "NewFirst");
379+
assert_eq!(user.last_name, "NewLast");
380+
assert_eq!(user.email, "new@defguard");
381+
assert_eq!(user.phone, Some("+48777888999".into()));
382+
assert_eq!(user.mfa_method, MFAMethod::OneTimePassword);
383+
}
209384
}

crates/defguard_core/src/handlers/user.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -785,11 +785,11 @@ pub(crate) async fn modify_user(
785785
token.delete(&mut *transaction).await?;
786786
}
787787
}
788-
789-
user_info.into_user_all_fields(&mut user)?;
790-
} else {
791-
user_info.into_user_safe_fields(&mut user)?;
792788
}
789+
790+
let updating_self = session.user.username == user.username;
791+
user_info.handle_update_user_fields(&mut user, session.is_admin, updating_self);
792+
793793
user.save(&mut *transaction).await?;
794794
transaction.commit().await?;
795795
let user_info = UserInfo::from_user(&appstate.pool, user.clone()).await?;

0 commit comments

Comments
 (0)