@@ -13,13 +13,15 @@ use chrono::{DateTime, Duration, Utc};
1313use dropshot:: HttpError ;
1414use http:: HeaderValue ;
1515use nexus_types:: authn:: cookies:: parse_cookies;
16+ use omicron_uuid_kinds:: ConsoleSessionUuid ;
1617use slog:: debug;
1718use uuid:: Uuid ;
1819
1920// many parts of the implementation will reference this OWASP guide
2021// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
2122
2223pub trait Session {
24+ fn id ( & self ) -> ConsoleSessionUuid ;
2325 fn silo_user_id ( & self ) -> Uuid ;
2426 fn silo_id ( & self ) -> Uuid ;
2527 fn time_last_used ( & self ) -> DateTime < Utc > ;
@@ -39,11 +41,11 @@ pub trait SessionStore {
3941 /// Extend session by updating time_last_used to now
4042 async fn session_update_last_used (
4143 & self ,
42- token : String ,
44+ id : ConsoleSessionUuid ,
4345 ) -> Option < Self :: SessionModel > ;
4446
4547 /// Mark session expired
46- async fn session_expire ( & self , token : String ) -> Option < ( ) > ;
48+ async fn session_expire ( & self , id : ConsoleSessionUuid ) -> Option < ( ) > ;
4749
4850 /// Maximum time session can remain idle before expiring
4951 fn session_idle_timeout ( & self ) -> Duration ;
@@ -131,7 +133,7 @@ where
131133 // expired
132134 let now = Utc :: now ( ) ;
133135 if session. time_last_used ( ) + ctx. session_idle_timeout ( ) < now {
134- let expired_session = ctx. session_expire ( token . clone ( ) ) . await ;
136+ let expired_session = ctx. session_expire ( session . id ( ) ) . await ;
135137 if expired_session. is_none ( ) {
136138 debug ! ( log, "failed to expire session" )
137139 }
@@ -151,7 +153,7 @@ where
151153 // existed longer than absolute_timeout, it is expired and we can no
152154 // longer extend the session
153155 if session. time_created ( ) + ctx. session_absolute_timeout ( ) < now {
154- let expired_session = ctx. session_expire ( token . clone ( ) ) . await ;
156+ let expired_session = ctx. session_expire ( session . id ( ) ) . await ;
155157 if expired_session. is_none ( ) {
156158 debug ! ( log, "failed to expire session" )
157159 }
@@ -172,7 +174,7 @@ where
172174 // authenticated for this request at this point. The next request might
173175 // be wrongly considered idle, but that's a problem for the next
174176 // request.
175- let updated_session = ctx. session_update_last_used ( token ) . await ;
177+ let updated_session = ctx. session_update_last_used ( session . id ( ) ) . await ;
176178 if updated_session. is_none ( ) {
177179 debug ! ( log, "failed to extend session" )
178180 }
@@ -199,26 +201,31 @@ mod test {
199201 use async_trait:: async_trait;
200202 use chrono:: { DateTime , Duration , Utc } ;
201203 use http;
204+ use omicron_uuid_kinds:: ConsoleSessionUuid ;
202205 use slog;
203- use std:: collections:: HashMap ;
204206 use std:: sync:: Mutex ;
205207 use uuid:: Uuid ;
206208
207209 // the mutex is annoying, but we need it in order to mutate the hashmap
208210 // without passing TestServerContext around as mutable
209211 struct TestServerContext {
210- sessions : Mutex < HashMap < String , FakeSession > > ,
212+ sessions : Mutex < Vec < FakeSession > > ,
211213 }
212214
213- #[ derive( Clone , Copy ) ]
215+ #[ derive( Clone ) ]
214216 struct FakeSession {
217+ id : ConsoleSessionUuid ,
218+ token : String ,
215219 silo_user_id : Uuid ,
216220 silo_id : Uuid ,
217221 time_created : DateTime < Utc > ,
218222 time_last_used : DateTime < Utc > ,
219223 }
220224
221225 impl Session for FakeSession {
226+ fn id ( & self ) -> ConsoleSessionUuid {
227+ self . id
228+ }
222229 fn silo_user_id ( & self ) -> Uuid {
223230 self . silo_user_id
224231 }
@@ -241,23 +248,34 @@ mod test {
241248 & self ,
242249 token : String ,
243250 ) -> Option < Self :: SessionModel > {
244- self . sessions . lock ( ) . unwrap ( ) . get ( & token) . map ( |s| * s)
251+ self . sessions
252+ . lock ( )
253+ . unwrap ( )
254+ . iter ( )
255+ . find ( |s| s. token == token)
256+ . map ( |s| s. clone ( ) )
245257 }
246258
247259 async fn session_update_last_used (
248260 & self ,
249- token : String ,
261+ id : ConsoleSessionUuid ,
250262 ) -> Option < Self :: SessionModel > {
251263 let mut sessions = self . sessions . lock ( ) . unwrap ( ) ;
252- let session = * sessions. get ( & token) . unwrap ( ) ;
253- let new_session =
254- FakeSession { time_last_used : Utc :: now ( ) , ..session } ;
255- ( * sessions) . insert ( token, new_session)
264+ if let Some ( pos) = sessions. iter ( ) . position ( |s| s. id == id) {
265+ let new_session = FakeSession {
266+ time_last_used : Utc :: now ( ) ,
267+ ..sessions[ pos] . clone ( )
268+ } ;
269+ sessions[ pos] = new_session. clone ( ) ;
270+ Some ( new_session)
271+ } else {
272+ None
273+ }
256274 }
257275
258- async fn session_expire ( & self , token : String ) -> Option < ( ) > {
276+ async fn session_expire ( & self , id : ConsoleSessionUuid ) -> Option < ( ) > {
259277 let mut sessions = self . sessions . lock ( ) . unwrap ( ) ;
260- ( * sessions) . remove ( & token ) ;
278+ sessions. retain ( |s| s . id != id ) ;
261279 Some ( ( ) )
262280 }
263281
@@ -295,32 +313,29 @@ mod test {
295313
296314 #[ tokio:: test]
297315 async fn test_missing_cookie ( ) {
298- let context =
299- TestServerContext { sessions : Mutex :: new ( HashMap :: new ( ) ) } ;
316+ let context = TestServerContext { sessions : Mutex :: new ( Vec :: new ( ) ) } ;
300317 let result = authn_with_cookie ( & context, None ) . await ;
301318 assert ! ( matches!( result, SchemeResult :: NotRequested ) ) ;
302319 }
303320
304321 #[ tokio:: test]
305322 async fn test_other_cookie ( ) {
306- let context =
307- TestServerContext { sessions : Mutex :: new ( HashMap :: new ( ) ) } ;
323+ let context = TestServerContext { sessions : Mutex :: new ( Vec :: new ( ) ) } ;
308324 let result = authn_with_cookie ( & context, Some ( "other=def" ) ) . await ;
309325 assert ! ( matches!( result, SchemeResult :: NotRequested ) ) ;
310326 }
311327
312328 #[ tokio:: test]
313329 async fn test_expired_cookie_idle ( ) {
314330 let context = TestServerContext {
315- sessions : Mutex :: new ( HashMap :: from ( [ (
316- "abc" . to_string ( ) ,
317- FakeSession {
318- silo_user_id : Uuid :: new_v4 ( ) ,
319- silo_id : Uuid :: new_v4 ( ) ,
320- time_last_used : Utc :: now ( ) - Duration :: hours ( 2 ) ,
321- time_created : Utc :: now ( ) - Duration :: hours ( 2 ) ,
322- } ,
323- ) ] ) ) ,
331+ sessions : Mutex :: new ( vec ! [ FakeSession {
332+ id: ConsoleSessionUuid :: new_v4( ) ,
333+ token: "abc" . to_string( ) ,
334+ silo_user_id: Uuid :: new_v4( ) ,
335+ silo_id: Uuid :: new_v4( ) ,
336+ time_last_used: Utc :: now( ) - Duration :: hours( 2 ) ,
337+ time_created: Utc :: now( ) - Duration :: hours( 2 ) ,
338+ } ] ) ,
324339 } ;
325340 let result = authn_with_cookie ( & context, Some ( "session=abc" ) ) . await ;
326341 assert ! ( matches!(
@@ -332,21 +347,21 @@ mod test {
332347 ) ) ;
333348
334349 // key should be removed from sessions dict, i.e., session deleted
335- assert ! ( !context. sessions. lock( ) . unwrap( ) . contains_key( "abc" ) )
350+ let sessions = context. sessions . lock ( ) . unwrap ( ) ;
351+ assert ! ( !sessions. iter( ) . any( |s| s. token == "abc" ) )
336352 }
337353
338354 #[ tokio:: test]
339355 async fn test_expired_cookie_absolute ( ) {
340356 let context = TestServerContext {
341- sessions : Mutex :: new ( HashMap :: from ( [ (
342- "abc" . to_string ( ) ,
343- FakeSession {
344- silo_user_id : Uuid :: new_v4 ( ) ,
345- silo_id : Uuid :: new_v4 ( ) ,
346- time_last_used : Utc :: now ( ) ,
347- time_created : Utc :: now ( ) - Duration :: hours ( 20 ) ,
348- } ,
349- ) ] ) ) ,
357+ sessions : Mutex :: new ( vec ! [ FakeSession {
358+ id: ConsoleSessionUuid :: new_v4( ) ,
359+ token: "abc" . to_string( ) ,
360+ silo_user_id: Uuid :: new_v4( ) ,
361+ silo_id: Uuid :: new_v4( ) ,
362+ time_last_used: Utc :: now( ) ,
363+ time_created: Utc :: now( ) - Duration :: hours( 20 ) ,
364+ } ] ) ,
350365 } ;
351366 let result = authn_with_cookie ( & context, Some ( "session=abc" ) ) . await ;
352367 assert ! ( matches!(
@@ -359,22 +374,21 @@ mod test {
359374
360375 // key should be removed from sessions dict, i.e., session deleted
361376 let sessions = context. sessions . lock ( ) . unwrap ( ) ;
362- assert ! ( !sessions. contains_key ( "abc" ) )
377+ assert ! ( !sessions. iter ( ) . any ( |s| s . token == "abc" ) )
363378 }
364379
365380 #[ tokio:: test]
366381 async fn test_valid_cookie ( ) {
367382 let time_last_used = Utc :: now ( ) - Duration :: seconds ( 5 ) ;
368383 let context = TestServerContext {
369- sessions : Mutex :: new ( HashMap :: from ( [ (
370- "abc" . to_string ( ) ,
371- FakeSession {
372- silo_user_id : Uuid :: new_v4 ( ) ,
373- silo_id : Uuid :: new_v4 ( ) ,
374- time_last_used,
375- time_created : Utc :: now ( ) ,
376- } ,
377- ) ] ) ) ,
384+ sessions : Mutex :: new ( vec ! [ FakeSession {
385+ id: ConsoleSessionUuid :: new_v4( ) ,
386+ token: "abc" . to_string( ) ,
387+ silo_user_id: Uuid :: new_v4( ) ,
388+ silo_id: Uuid :: new_v4( ) ,
389+ time_last_used,
390+ time_created: Utc :: now( ) ,
391+ } ] ) ,
378392 } ;
379393 let result = authn_with_cookie ( & context, Some ( "session=abc" ) ) . await ;
380394 assert ! ( matches!(
@@ -384,13 +398,13 @@ mod test {
384398
385399 // valid cookie should have updated time_last_used
386400 let sessions = context. sessions . lock ( ) . unwrap ( ) ;
387- assert ! ( sessions. get( "abc" ) . unwrap( ) . time_last_used > time_last_used)
401+ let session = sessions. iter ( ) . find ( |s| s. token == "abc" ) . unwrap ( ) ;
402+ assert ! ( session. time_last_used > time_last_used)
388403 }
389404
390405 #[ tokio:: test]
391406 async fn test_garbage_cookie ( ) {
392- let context =
393- TestServerContext { sessions : Mutex :: new ( HashMap :: new ( ) ) } ;
407+ let context = TestServerContext { sessions : Mutex :: new ( Vec :: new ( ) ) } ;
394408 let result =
395409 authn_with_cookie ( & context, Some ( "unparsable garbage!!!!!1" ) ) . await ;
396410 assert ! ( matches!( result, SchemeResult :: NotRequested ) ) ;
0 commit comments