Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 115 additions & 8 deletions nexus/db-queries/src/db/datastore/silo_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::DataStore;
use crate::authz;
use crate::context::OpContext;
use crate::db::IncompleteOnConflictExt;
use crate::db::datastore::DbConnection;
use crate::db::datastore::RunnableQueryNoReturn;
use crate::db::model;
use crate::db::model::Silo;
Expand All @@ -32,6 +33,7 @@ use omicron_common::api::external::InternalContext;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::UpdateResult;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::SiloGroupUuid;
use omicron_uuid_kinds::SiloUserUuid;
use uuid::Uuid;
Expand Down Expand Up @@ -68,6 +70,15 @@ impl SiloGroup {
SiloGroup::Scim(u) => u.silo_id,
}
}

/// Set the member count for this group
pub fn set_member_count(&mut self, count: i64) {
match self {
SiloGroup::ApiOnly(g) => g.member_count = count,
SiloGroup::Jit(g) => g.member_count = count,
SiloGroup::Scim(g) => g.member_count = count,
}
}
}

impl From<model::SiloGroup> for SiloGroup {
Expand All @@ -85,6 +96,7 @@ impl From<model::SiloGroup> for SiloGroup {
group with provision type 'api_only' from having a \
null external_id",
),
member_count: 0,
})
}

Expand All @@ -99,6 +111,7 @@ impl From<model::SiloGroup> for SiloGroup {
group with provision type 'jit' from having a null \
external_id",
),
member_count: 0,
}),

UserProvisionType::Scim => SiloGroup::Scim(SiloGroupScim {
Expand All @@ -113,6 +126,7 @@ impl From<model::SiloGroup> for SiloGroup {
display_name",
),
external_id: record.external_id,
member_count: 0,
}),
}
}
Expand Down Expand Up @@ -148,6 +162,9 @@ pub struct SiloGroupApiOnly {

/// The identity provider's ID for this group.
pub external_id: String,

/// The number of members in this group
pub member_count: i64,
}

impl SiloGroupApiOnly {
Expand All @@ -159,6 +176,7 @@ impl SiloGroupApiOnly {
time_deleted: None,
silo_id,
external_id,
member_count: 0,
}
}
}
Expand Down Expand Up @@ -193,6 +211,7 @@ impl From<SiloGroupApiOnly> for views::Group {
// TODO the use of external_id as display_name is temporary
display_name: u.external_id,
silo_id: u.silo_id,
member_count: u.member_count,
}
}
}
Expand All @@ -207,6 +226,9 @@ pub struct SiloGroupJit {

/// The identity provider's ID for this user.
pub external_id: String,

/// The number of members in this group
pub member_count: i64,
}

impl SiloGroupJit {
Expand All @@ -218,6 +240,7 @@ impl SiloGroupJit {
time_deleted: None,
silo_id,
external_id,
member_count: 0,
}
}
}
Expand Down Expand Up @@ -252,6 +275,7 @@ impl From<SiloGroupJit> for views::Group {
// TODO the use of external_id as display_name is temporary
display_name: u.external_id,
silo_id: u.silo_id,
member_count: u.member_count,
}
}
}
Expand All @@ -268,6 +292,9 @@ pub struct SiloGroupScim {
pub display_name: String,

pub external_id: Option<String>,

/// The number of members in this group
pub member_count: i64,
}

impl SiloGroupScim {
Expand All @@ -285,6 +312,7 @@ impl SiloGroupScim {
silo_id,
display_name,
external_id,
member_count: 0,
}
}
}
Expand Down Expand Up @@ -319,6 +347,7 @@ impl From<SiloGroupScim> for views::Group {
// TODO the use of display name as display_name is temporary
display_name: u.display_name,
silo_id: u.silo_id,
member_count: u.member_count,
}
}
}
Expand All @@ -345,6 +374,23 @@ impl<'a> SiloGroupLookup<'a> {
}

impl DataStore {
/// Helper function to fetch member counts for a list of groups
async fn silo_group_member_counts(
conn: &async_bb8_diesel::Connection<DbConnection>,
group_ids: Vec<Uuid>,
) -> Result<std::collections::HashMap<Uuid, i64>, Error> {
use nexus_db_schema::schema::silo_group_membership::dsl;

dsl::silo_group_membership
.filter(dsl::silo_group_id.eq_any(group_ids))
.group_by(dsl::silo_group_id)
.select((dsl::silo_group_id, diesel::dsl::count(dsl::silo_user_id)))
.load_async::<(Uuid, i64)>(conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
.map(|counts| counts.into_iter().collect())
}

pub(super) async fn silo_group_ensure_query(
opctx: &OpContext,
authz_silo: &authz::Silo,
Expand Down Expand Up @@ -591,16 +637,38 @@ impl DataStore {
silo_group as sg, silo_group_membership as sgm,
};

let page = paginated(sg::dsl::silo_group, sg::id, pagparams)
let conn = self.pool_connection_authorized(opctx).await?;

// First get the groups this user belongs to, in the correct paginated order
let groups = paginated(sg::dsl::silo_group, sg::id, pagparams)
.inner_join(sgm::table.on(sgm::silo_group_id.eq(sg::id)))
.filter(sgm::silo_user_id.eq(to_db_typed_uuid(silo_user_id)))
.filter(sg::time_deleted.is_null())
.select(model::SiloGroup::as_returning())
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
.select(model::SiloGroup::as_select())
.load_async::<model::SiloGroup>(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

if groups.is_empty() {
return Ok(Vec::new());
}

let group_ids: Vec<Uuid> =
groups.iter().map(|g| *g.id().as_untyped_uuid()).collect();

let member_counts =
DataStore::silo_group_member_counts(&conn, group_ids).await?;

let page = groups
.into_iter()
.map(|group: model::SiloGroup| group.into())
.map(|group| {
let group_id = *group.id().as_untyped_uuid();
let member_count =
member_counts.get(&group_id).copied().unwrap_or(0);
let mut silo_group: SiloGroup = group.into();
silo_group.set_member_count(member_count);
silo_group
})
.collect::<Vec<SiloGroup>>();

Ok(page)
Expand Down Expand Up @@ -756,18 +824,57 @@ impl DataStore {
})?
};

let page = paginated(dsl::silo_group, dsl::id, pagparams)
let groups = paginated(dsl::silo_group, dsl::id, pagparams)
.filter(dsl::silo_id.eq(authz_silo.id()))
.filter(dsl::time_deleted.is_null())
.filter(dsl::user_provision_type.eq(silo.user_provision_type))
.select(model::SiloGroup::as_select())
.load_async::<model::SiloGroup>(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

if groups.is_empty() {
return Ok(Vec::new());
}

let group_ids: Vec<Uuid> =
groups.iter().map(|g| *g.id().as_untyped_uuid()).collect();

let member_counts =
DataStore::silo_group_member_counts(&conn, group_ids).await?;

let page = groups
.into_iter()
.map(|group: model::SiloGroup| group.into())
.map(|group| {
let group_id = *group.id().as_untyped_uuid();
let member_count =
member_counts.get(&group_id).copied().unwrap_or(0);
let mut silo_group: SiloGroup = group.into();
silo_group.set_member_count(member_count);
silo_group
})
.collect::<Vec<SiloGroup>>();

Ok(page)
}

/// Fetch the member count for a single silo group
pub async fn silo_group_member_count(
&self,
opctx: &OpContext,
group_id: SiloGroupUuid,
) -> Result<i64, Error> {
use nexus_db_schema::schema::silo_group_membership::dsl;

let conn = self.pool_connection_authorized(opctx).await?;

let count = dsl::silo_group_membership
.filter(dsl::silo_group_id.eq(to_db_typed_uuid(group_id)))
.count()
.get_result_async::<i64>(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(count)
}
}
44 changes: 44 additions & 0 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use openapiv3::OpenAPI;
/// Copies of data types that changed between versions
mod v2025112000;
mod v2025120300;
mod v2025121500;

api_versions!([
// API versions are in the format YYYYMMDDNN.0.0, defined below as
Expand Down Expand Up @@ -65,6 +66,7 @@ api_versions!([
// | date-based version should be at the top of the list.
// v
// (next_yyyymmddnn, IDENT),
(2025121500, GROUP_MEMBER_COUNT),
(2025121200, BGP_PEER_COLLISION_STATE),
(2025120300, LOCAL_STORAGE),
(2025112000, INITIAL),
Expand Down Expand Up @@ -3831,20 +3833,62 @@ pub trait NexusExternalApi {

/// List groups
#[endpoint {
operation_id = "group_list",
method = GET,
path = "/v1/groups",
tags = ["silos"],
versions = ..VERSION_GROUP_MEMBER_COUNT,
}]
async fn v2025121200_group_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<v2025121500::Group>>, HttpError>
{
match Self::group_list(rqctx, query_params).await {
Ok(HttpResponseOk(page)) => Ok(HttpResponseOk(ResultsPage {
items: page.items.into_iter().map(Into::into).collect(),
next_page: page.next_page,
})),
Err(e) => Err(e),
}
}

/// List groups
#[endpoint {
method = GET,
path = "/v1/groups",
tags = ["silos"],
versions = VERSION_GROUP_MEMBER_COUNT..,
}]
async fn group_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<views::Group>>, HttpError>;

/// Fetch group
#[endpoint {
operation_id = "group_view",
method = GET,
path = "/v1/groups/{group_id}",
tags = ["silos"],
versions = ..VERSION_GROUP_MEMBER_COUNT,
}]
async fn v2025121200_group_view(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::GroupPath>,
) -> Result<HttpResponseOk<v2025121500::Group>, HttpError> {
match Self::group_view(rqctx, path_params).await {
Ok(HttpResponseOk(group)) => Ok(HttpResponseOk(group.into())),
Err(e) => Err(e),
}
}

/// Fetch group
#[endpoint {
method = GET,
path = "/v1/groups/{group_id}",
tags = ["silos"],
versions = VERSION_GROUP_MEMBER_COUNT..,
}]
async fn group_view(
rqctx: RequestContext<Self::Context>,
Expand Down
47 changes: 47 additions & 0 deletions nexus/external-api/src/v2025121500.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Nexus external types that changed from 2025121200 to 2025121500

use nexus_types::external_api::views;
use omicron_uuid_kinds::SiloGroupUuid;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// View of a Group (without member_count field)
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, JsonSchema)]
pub struct Group {
#[schemars(with = "Uuid")]
pub id: SiloGroupUuid,

/// Human-readable name that can identify the group
pub display_name: String,

/// Uuid of the silo to which this group belongs
pub silo_id: Uuid,
}

impl From<Group> for views::Group {
fn from(old: Group) -> views::Group {
views::Group {
id: old.id,
display_name: old.display_name,
silo_id: old.silo_id,
// Default member_count to 0 for old clients
member_count: 0,
}
}
}

impl From<views::Group> for Group {
fn from(new: views::Group) -> Group {
Group {
id: new.id,
display_name: new.display_name,
silo_id: new.silo_id,
// Drop member_count when converting to old version
}
}
}
1 change: 1 addition & 0 deletions nexus/src/app/iam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ impl super::Nexus {
.authn
.silo_required()
.internal_context("listing current silo's groups")?;

self.db_datastore
.silo_groups_list_by_id(opctx, &authz_silo, pagparams)
.await
Expand Down
Loading
Loading