@@ -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}
0 commit comments