@@ -922,26 +922,36 @@ impl<'a> CrdbScimProviderStore<'a> {
922922 Ok ( convert_to_scim_group ( new_group, members) )
923923 }
924924
925- async fn list_groups_in_txn (
925+ async fn list_groups_with_members (
926926 & self ,
927927 conn : & async_bb8_diesel:: Connection < DbConnection > ,
928928 err : OptionalError < ProviderStoreError > ,
929929 filter : Option < FilterOp > ,
930930 ) -> Result < Vec < StoredParts < Group > > , diesel:: result:: Error > {
931- use nexus_db_schema:: schema:: silo_group:: dsl;
931+ use nexus_db_schema:: schema:: silo_group:: dsl as group_dsl;
932+ use nexus_db_schema:: schema:: silo_group_membership:: dsl as membership_dsl;
933+ use std:: collections:: HashMap ;
932934
933- let mut query = dsl:: silo_group
934- . filter ( dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
935- . filter ( dsl:: user_provision_type. eq ( model:: UserProvisionType :: Scim ) )
936- . filter ( dsl:: time_deleted. is_null ( ) )
935+ let mut query = group_dsl:: silo_group
936+ . left_join (
937+ membership_dsl:: silo_group_membership
938+ . on ( group_dsl:: id. eq ( membership_dsl:: silo_group_id) ) ,
939+ )
940+ . filter ( group_dsl:: silo_id. eq ( self . authz_silo . id ( ) ) )
941+ . filter (
942+ group_dsl:: user_provision_type
943+ . eq ( model:: UserProvisionType :: Scim ) ,
944+ )
945+ . filter ( group_dsl:: time_deleted. is_null ( ) )
937946 . into_boxed ( ) ;
938947
939948 match filter {
940949 Some ( FilterOp :: DisplayNameEq ( display_name) ) => {
941950 // displayName is defined as `"caseExact" : false` in RFC 7643,
942951 // section 8.7.1
943952 query = query. filter (
944- lower ( dsl:: display_name) . eq ( lower ( display_name. clone ( ) ) ) ,
953+ lower ( group_dsl:: display_name)
954+ . eq ( lower ( display_name. clone ( ) ) ) ,
945955 ) ;
946956 }
947957
@@ -959,20 +969,60 @@ impl<'a> CrdbScimProviderStore<'a> {
959969 }
960970 }
961971
962- let groups = query
963- . select ( model:: SiloGroup :: as_returning ( ) )
972+ // Select group fields and optional member user_id
973+ type GroupRow = ( model:: SiloGroup , Option < Uuid > ) ;
974+ type GroupsMap =
975+ HashMap < Uuid , ( Option < model:: SiloGroup > , Vec < SiloUserUuid > ) > ;
976+
977+ let rows: Vec < GroupRow > = query
978+ . select ( (
979+ model:: SiloGroup :: as_select ( ) ,
980+ membership_dsl:: silo_user_id. nullable ( ) ,
981+ ) )
964982 . load_async ( conn)
965983 . await ?;
966984
967- let mut returned_groups = Vec :: with_capacity ( groups. len ( ) ) ;
985+ // Group the results by group_id
986+ let mut groups_map: GroupsMap = HashMap :: new ( ) ;
968987
969- for group in groups {
970- let members = self
971- . get_group_members_for_group_in_txn (
972- conn,
973- group. identity . id . into ( ) ,
974- )
975- . await ?;
988+ for ( group, maybe_user_id) in rows {
989+ let group_id = group. identity . id . into_untyped_uuid ( ) ;
990+
991+ let entry = groups_map
992+ . entry ( group_id)
993+ . or_insert_with ( || ( None , Vec :: new ( ) ) ) ;
994+
995+ // Store the group on first occurrence
996+ if entry. 0 . is_none ( ) {
997+ entry. 0 = Some ( group) ;
998+ }
999+
1000+ // If this row has a member, add it to the group's members
1001+ if let Some ( user_id) = maybe_user_id {
1002+ entry. 1 . push ( SiloUserUuid :: from_untyped_uuid ( user_id) ) ;
1003+ }
1004+ }
1005+
1006+ // Convert to the expected return type
1007+ let mut returned_groups = Vec :: with_capacity ( groups_map. len ( ) ) ;
1008+
1009+ for ( _group_id, ( maybe_group, members) ) in groups_map {
1010+ let group = maybe_group. expect ( "group should always be present" ) ;
1011+
1012+ let members = if members. is_empty ( ) {
1013+ None
1014+ } else {
1015+ let mut id_ord_map = IdOrdMap :: with_capacity ( members. len ( ) ) ;
1016+ for user_id in members {
1017+ id_ord_map
1018+ . insert_unique ( GroupMember {
1019+ resource_type : Some ( ResourceType :: User . to_string ( ) ) ,
1020+ value : Some ( user_id. to_string ( ) ) ,
1021+ } )
1022+ . expect ( "user_id should be unique" ) ;
1023+ }
1024+ Some ( id_ord_map)
1025+ } ;
9761026
9771027 let SiloGroup :: Scim ( group) = group. into ( ) else {
9781028 // With the user provision type filter, this should never be
@@ -1720,14 +1770,7 @@ impl<'a> ProviderStore for CrdbScimProviderStore<'a> {
17201770 let err: OptionalError < ProviderStoreError > = OptionalError :: new ( ) ;
17211771
17221772 let groups = self
1723- . datastore
1724- . transaction_retry_wrapper ( "scim_list_groups" )
1725- . transaction ( & conn, |conn| {
1726- let err = err. clone ( ) ;
1727- let filter = filter. clone ( ) ;
1728-
1729- async move { self . list_groups_in_txn ( & conn, err, filter) . await }
1730- } )
1773+ . list_groups_with_members ( & conn, err. clone ( ) , filter)
17311774 . await
17321775 . map_err ( |e| {
17331776 if let Some ( e) = err. take ( ) {
0 commit comments