@@ -298,26 +298,49 @@ impl<'a> CrdbScimProviderStore<'a> {
298298 Ok ( convert_to_scim_user ( new_user, None ) )
299299 }
300300
301- async fn list_users_in_txn (
301+ /// Optimized version of list_users_in_txn that uses a single query with
302+ /// joins instead of N+1 queries.
303+ async fn list_users_with_groups_in_txn (
302304 & self ,
303305 conn : & async_bb8_diesel:: Connection < DbConnection > ,
304306 err : OptionalError < ProviderStoreError > ,
305307 filter : Option < FilterOp > ,
306308 ) -> Result < Vec < StoredParts < User > > , diesel:: result:: Error > {
307- use nexus_db_schema:: schema:: silo_user:: dsl;
308-
309- let mut query = dsl:: silo_user
310- . filter ( dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
311- . filter ( dsl:: user_provision_type. eq ( model:: UserProvisionType :: Scim ) )
312- . filter ( dsl:: time_deleted. is_null ( ) )
309+ use nexus_db_schema:: schema:: silo_group:: dsl as group_dsl;
310+ use nexus_db_schema:: schema:: silo_group_membership:: dsl as membership_dsl;
311+ use nexus_db_schema:: schema:: silo_user:: dsl as user_dsl;
312+ use std:: collections:: HashMap ;
313+
314+ let mut query = user_dsl:: silo_user
315+ . left_join (
316+ membership_dsl:: silo_group_membership
317+ . on ( user_dsl:: id. eq ( membership_dsl:: silo_user_id) ) ,
318+ )
319+ . left_join (
320+ group_dsl:: silo_group. on ( membership_dsl:: silo_group_id
321+ . eq ( group_dsl:: id)
322+ . and ( group_dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
323+ . and (
324+ group_dsl:: user_provision_type
325+ . eq ( model:: UserProvisionType :: Scim ) ,
326+ )
327+ . and ( group_dsl:: time_deleted. is_null ( ) ) ) ,
328+ )
329+ . filter ( user_dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
330+ . filter (
331+ user_dsl:: user_provision_type
332+ . eq ( model:: UserProvisionType :: Scim ) ,
333+ )
334+ . filter ( user_dsl:: time_deleted. is_null ( ) )
313335 . into_boxed ( ) ;
314336
315337 match filter {
316338 Some ( FilterOp :: UserNameEq ( username) ) => {
317339 // userName is defined as `"caseExact" : false` in RFC 7643,
318340 // section 8.7.1
319- query = query
320- . filter ( lower ( dsl:: user_name) . eq ( lower ( username. clone ( ) ) ) ) ;
341+ query = query. filter (
342+ lower ( user_dsl:: user_name) . eq ( lower ( username. clone ( ) ) ) ,
343+ ) ;
321344 }
322345
323346 None => {
@@ -334,17 +357,72 @@ impl<'a> CrdbScimProviderStore<'a> {
334357 }
335358 }
336359
337- let users = query
338- . select ( model:: SiloUser :: as_returning ( ) )
339- . load_async ( conn)
340- . await ?;
360+ // Select user fields and optional group fields
361+ // Note: We need to explicitly select the group columns to work with the
362+ // boxed query and LEFT JOIN
363+ let rows: Vec < ( model:: SiloUser , Option < ( Uuid , Option < String > ) > ) > =
364+ query
365+ . select ( (
366+ model:: SiloUser :: as_select ( ) ,
367+ ( group_dsl:: id, group_dsl:: display_name) . nullable ( ) ,
368+ ) )
369+ . load_async ( conn)
370+ . await ?;
341371
342- let mut returned_users = Vec :: with_capacity ( users. len ( ) ) ;
372+ // Group the results by user_id
373+ let mut users_map: HashMap <
374+ Uuid ,
375+ ( Option < model:: SiloUser > , Vec < ( SiloGroupUuid , String ) > ) ,
376+ > = HashMap :: new ( ) ;
343377
344- for user in users {
345- let groups = self
346- . get_user_groups_for_user_in_txn ( conn, user. identity . id . into ( ) )
347- . await ?;
378+ for ( user, maybe_group_info) in rows {
379+ let user_id = user. identity . id . into_untyped_uuid ( ) ;
380+
381+ let entry =
382+ users_map. entry ( user_id) . or_insert_with ( || ( None , Vec :: new ( ) ) ) ;
383+
384+ // Store the user on first occurrence
385+ if entry. 0 . is_none ( ) {
386+ entry. 0 = Some ( user) ;
387+ }
388+
389+ // If this row has a group, add it to the user's groups
390+ if let Some ( ( group_id, maybe_display_name) ) = maybe_group_info {
391+ let display_name = maybe_display_name. expect (
392+ "the constraint `display_name_consistency` prevents a \
393+ group with provision type 'scim' from having a null \
394+ display_name",
395+ ) ;
396+
397+ entry. 1 . push ( (
398+ SiloGroupUuid :: from_untyped_uuid ( group_id) ,
399+ display_name,
400+ ) ) ;
401+ }
402+ }
403+
404+ // Convert to the expected return type
405+ let mut returned_users = Vec :: with_capacity ( users_map. len ( ) ) ;
406+
407+ for ( _user_id, ( maybe_user, groups) ) in users_map {
408+ let user = maybe_user. expect ( "user should always be present" ) ;
409+
410+ let groups = if groups. is_empty ( ) {
411+ None
412+ } else {
413+ Some (
414+ groups
415+ . into_iter ( )
416+ . map ( |( group_id, display_name) | UserGroup {
417+ // Note neither the scim2-rs crate or Nexus supports
418+ // nested groups
419+ member_type : Some ( UserGroupType :: Direct ) ,
420+ value : Some ( group_id. to_string ( ) ) ,
421+ display : Some ( display_name) ,
422+ } )
423+ . collect ( ) ,
424+ )
425+ } ;
348426
349427 let SiloUser :: Scim ( user) = user. into ( ) else {
350428 // With the user provision type filter, this should never be
@@ -1392,7 +1470,9 @@ impl<'a> ProviderStore for CrdbScimProviderStore<'a> {
13921470 let err = err. clone ( ) ;
13931471 let filter = filter. clone ( ) ;
13941472
1395- async move { self . list_users_in_txn ( & conn, err, filter) . await }
1473+ async move {
1474+ self . list_users_with_groups_in_txn ( & conn, err, filter) . await
1475+ }
13961476 } )
13971477 . await
13981478 . map_err ( |e| {
0 commit comments