@@ -917,26 +917,36 @@ impl<'a> CrdbScimProviderStore<'a> {
917917 Ok ( convert_to_scim_group ( new_group, members) )
918918 }
919919
920- async fn list_groups_in_txn (
920+ async fn list_groups_with_members (
921921 & self ,
922922 conn : & async_bb8_diesel:: Connection < DbConnection > ,
923923 err : OptionalError < ProviderStoreError > ,
924924 filter : Option < FilterOp > ,
925925 ) -> Result < Vec < StoredParts < Group > > , diesel:: result:: Error > {
926- use nexus_db_schema:: schema:: silo_group:: dsl;
926+ use nexus_db_schema:: schema:: silo_group:: dsl as group_dsl;
927+ use nexus_db_schema:: schema:: silo_group_membership:: dsl as membership_dsl;
928+ use std:: collections:: HashMap ;
927929
928- let mut query = dsl:: silo_group
929- . filter ( dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
930- . filter ( dsl:: user_provision_type. eq ( model:: UserProvisionType :: Scim ) )
931- . filter ( dsl:: time_deleted. is_null ( ) )
930+ let mut query = group_dsl:: silo_group
931+ . left_join (
932+ membership_dsl:: silo_group_membership
933+ . on ( group_dsl:: id. eq ( membership_dsl:: silo_group_id) ) ,
934+ )
935+ . filter ( group_dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
936+ . filter (
937+ group_dsl:: user_provision_type
938+ . eq ( model:: UserProvisionType :: Scim ) ,
939+ )
940+ . filter ( group_dsl:: time_deleted. is_null ( ) )
932941 . into_boxed ( ) ;
933942
934943 match filter {
935944 Some ( FilterOp :: DisplayNameEq ( display_name) ) => {
936945 // displayName is defined as `"caseExact" : false` in RFC 7643,
937946 // section 8.7.1
938947 query = query. filter (
939- lower ( dsl:: display_name) . eq ( lower ( display_name. clone ( ) ) ) ,
948+ lower ( group_dsl:: display_name)
949+ . eq ( lower ( display_name. clone ( ) ) ) ,
940950 ) ;
941951 }
942952
@@ -954,20 +964,60 @@ impl<'a> CrdbScimProviderStore<'a> {
954964 }
955965 }
956966
957- let groups = query
958- . select ( model:: SiloGroup :: as_returning ( ) )
967+ // Select group fields and optional member user_id
968+ type GroupRow = ( model:: SiloGroup , Option < Uuid > ) ;
969+ type GroupsMap =
970+ HashMap < Uuid , ( Option < model:: SiloGroup > , Vec < SiloUserUuid > ) > ;
971+
972+ let rows: Vec < GroupRow > = query
973+ . select ( (
974+ model:: SiloGroup :: as_select ( ) ,
975+ membership_dsl:: silo_user_id. nullable ( ) ,
976+ ) )
959977 . load_async ( conn)
960978 . await ?;
961979
962- let mut returned_groups = Vec :: with_capacity ( groups. len ( ) ) ;
980+ // Group the results by group_id
981+ let mut groups_map: GroupsMap = HashMap :: new ( ) ;
963982
964- for group in groups {
965- let members = self
966- . get_group_members_for_group_in_txn (
967- conn,
968- group. identity . id . into ( ) ,
969- )
970- . await ?;
983+ for ( group, maybe_user_id) in rows {
984+ let group_id = group. identity . id . into_untyped_uuid ( ) ;
985+
986+ let entry = groups_map
987+ . entry ( group_id)
988+ . or_insert_with ( || ( None , Vec :: new ( ) ) ) ;
989+
990+ // Store the group on first occurrence
991+ if entry. 0 . is_none ( ) {
992+ entry. 0 = Some ( group) ;
993+ }
994+
995+ // If this row has a member, add it to the group's members
996+ if let Some ( user_id) = maybe_user_id {
997+ entry. 1 . push ( SiloUserUuid :: from_untyped_uuid ( user_id) ) ;
998+ }
999+ }
1000+
1001+ // Convert to the expected return type
1002+ let mut returned_groups = Vec :: with_capacity ( groups_map. len ( ) ) ;
1003+
1004+ for ( _group_id, ( maybe_group, members) ) in groups_map {
1005+ let group = maybe_group. expect ( "group should always be present" ) ;
1006+
1007+ let members = if members. is_empty ( ) {
1008+ None
1009+ } else {
1010+ let mut id_ord_map = IdOrdMap :: with_capacity ( members. len ( ) ) ;
1011+ for user_id in members {
1012+ id_ord_map
1013+ . insert_unique ( GroupMember {
1014+ resource_type : Some ( ResourceType :: User . to_string ( ) ) ,
1015+ value : Some ( user_id. to_string ( ) ) ,
1016+ } )
1017+ . expect ( "user_id should be unique" ) ;
1018+ }
1019+ Some ( id_ord_map)
1020+ } ;
9711021
9721022 let SiloGroup :: Scim ( group) = group. into ( ) else {
9731023 // With the user provision type filter, this should never be
@@ -1723,14 +1773,7 @@ impl<'a> ProviderStore for CrdbScimProviderStore<'a> {
17231773 let err: OptionalError < ProviderStoreError > = OptionalError :: new ( ) ;
17241774
17251775 let groups = self
1726- . datastore
1727- . transaction_retry_wrapper ( "scim_list_groups" )
1728- . transaction ( & conn, |conn| {
1729- let err = err. clone ( ) ;
1730- let filter = filter. clone ( ) ;
1731-
1732- async move { self . list_groups_in_txn ( & conn, err, filter) . await }
1733- } )
1776+ . list_groups_with_members ( & conn, err. clone ( ) , filter)
17341777 . await
17351778 . map_err ( |e| {
17361779 if let Some ( e) = err. take ( ) {
0 commit comments