@@ -33,6 +33,9 @@ pub enum RouteError {
33
33
#[ error( transparent) ]
34
34
Internal ( Box < dyn std:: error:: Error + Send + Sync + ' static > ) ,
35
35
36
+ #[ error( "Password is too weak" ) ]
37
+ PasswordTooWeak ,
38
+
36
39
#[ error( "Password hashing failed" ) ]
37
40
Password ( #[ source] anyhow:: Error ) ,
38
41
@@ -47,6 +50,7 @@ impl IntoResponse for RouteError {
47
50
let error = ErrorResponse :: from_error ( & self ) ;
48
51
let status = match self {
49
52
Self :: Internal ( _) | Self :: Password ( _) => StatusCode :: INTERNAL_SERVER_ERROR ,
53
+ Self :: PasswordTooWeak => StatusCode :: BAD_REQUEST ,
50
54
Self :: NotFound ( _) => StatusCode :: NOT_FOUND ,
51
55
} ;
52
56
( status, Json ( error) ) . into_response ( )
@@ -64,6 +68,9 @@ pub struct Request {
64
68
/// The password to set for the user
65
69
#[ schemars( example = "password_example" ) ]
66
70
password : String ,
71
+
72
+ /// Skip the password complexity check
73
+ skip_password_check : Option < bool > ,
67
74
}
68
75
69
76
pub fn doc ( operation : TransformOperation ) -> TransformOperation {
@@ -72,6 +79,10 @@ pub fn doc(operation: TransformOperation) -> TransformOperation {
72
79
. summary ( "Set the password for a user" )
73
80
. tag ( "user" )
74
81
. 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
+ } )
75
86
. response_with :: < 404 , RouteError , _ > ( |t| {
76
87
let response = ErrorResponse :: from_error ( & RouteError :: NotFound ( Ulid :: nil ( ) ) ) ;
77
88
t. description ( "User was not found" ) . example ( response)
@@ -94,6 +105,16 @@ pub async fn handler(
94
105
. await ?
95
106
. ok_or ( RouteError :: NotFound ( * id) ) ?;
96
107
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
+
97
118
let password = Zeroizing :: new ( params. password . into_bytes ( ) ) ;
98
119
let ( version, hashed_password) = password_manager
99
120
. hash ( & mut rng, password)
@@ -144,7 +165,66 @@ mod tests {
144
165
let request = Request :: post ( format ! ( "/api/admin/v1/users/{user_id}/set-password" ) )
145
166
. bearer ( & token)
146
167
. 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 ,
148
228
} ) ) ;
149
229
150
230
let response = state. request ( request) . await ;
@@ -153,7 +233,7 @@ mod tests {
153
233
// Check that the user now has a password
154
234
let mut repo = state. repository ( ) . await . unwrap ( ) ;
155
235
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 ( ) ) ;
157
237
state
158
238
. password_manager
159
239
. verify (
@@ -175,7 +255,7 @@ mod tests {
175
255
let request = Request :: post ( "/api/admin/v1/users/01040G2081040G2081040G2081/set-password" )
176
256
. bearer ( & token)
177
257
. json ( serde_json:: json!( {
178
- "password" : "hunter2 " ,
258
+ "password" : "this is a good enough password " ,
179
259
} ) ) ;
180
260
181
261
let response = state. request ( request) . await ;
0 commit comments