Skip to content

Commit 934a3b7

Browse files
committed
rewrite list users to use a single query
1 parent ce74d63 commit 934a3b7

File tree

1 file changed

+99
-19
lines changed

1 file changed

+99
-19
lines changed

nexus/db-queries/src/db/datastore/scim_provider_store.rs

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)