From c4c8aa9ebb60ae0c289d21b9745811e8c82f8f2f Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Thu, 26 Feb 2026 21:00:51 +0000 Subject: [PATCH 1/5] Create a synthetic authz resource for trust quorum and use it --- dev-tools/omdb/src/bin/omdb/db.rs | 9 +- nexus/auth/src/authz/api_resources.rs | 52 ++++++ nexus/auth/src/authz/omicron.polar | 11 ++ nexus/auth/src/authz/oso_generic.rs | 1 + nexus/db-queries/src/db/datastore/rack.rs | 8 +- .../src/db/datastore/trust_quorum.rs | 157 +++++++++--------- .../src/policy_test/resource_builder.rs | 17 ++ nexus/db-queries/src/policy_test/resources.rs | 19 ++- .../src/app/background/tasks/trust_quorum.rs | 23 ++- nexus/src/app/sled.rs | 8 +- nexus/src/app/trust_quorum.rs | 82 ++++++--- nexus/src/external_api/http_entrypoints.rs | 21 ++- nexus/src/lockstep_api/http_entrypoints.rs | 12 +- 13 files changed, 289 insertions(+), 131 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 6686c29b675..40f33783da6 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -112,6 +112,7 @@ use nexus_db_model::VolumeResourceUsage; use nexus_db_model::VpcSubnet; use nexus_db_model::Zpool; use nexus_db_model::to_db_typed_uuid; +use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_db_queries::db; use nexus_db_queries::db::DataStore; @@ -151,6 +152,7 @@ use omicron_common::api::external; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Generation; use omicron_common::api::external::InstanceState; +use omicron_common::api::external::LookupType; use omicron_common::api::external::MacAddr; use omicron_uuid_kinds::CollectionUuid; use omicron_uuid_kinds::DatasetUuid; @@ -8165,8 +8167,13 @@ async fn cmd_db_trust_quorum_list_configs( } let limit = fetch_opts.fetch_limit; + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + args.rack_id.into_untyped_uuid(), + LookupType::ById(args.rack_id.into_untyped_uuid()), + )); let configs = datastore - .tq_list_config(opctx, args.rack_id, &first_page::(limit)) + .tq_list_config(opctx, authz_tq, &first_page::(limit)) .await .context("listing trust quorum configurations")?; diff --git a/nexus/auth/src/authz/api_resources.rs b/nexus/auth/src/authz/api_resources.rs index 926f1801fb5..2ef06674c88 100644 --- a/nexus/auth/src/authz/api_resources.rs +++ b/nexus/auth/src/authz/api_resources.rs @@ -681,6 +681,58 @@ impl AuthorizedResource for Inventory { } } +/// Synthetic resource to model accessing trust quorum configurations for a +/// given rack +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TrustQuorumConfig(Rack); + +impl TrustQuorumConfig { + pub fn new(rack: Rack) -> TrustQuorumConfig { + TrustQuorumConfig(rack) + } + + pub fn rack(&self) -> &Rack { + &self.0 + } +} + +impl oso::PolarClass for TrustQuorumConfig { + fn get_polar_class_builder() -> oso::ClassBuilder { + oso::Class::builder() + .with_equality_check() + .add_attribute_getter("rack", |config: &TrustQuorumConfig| { + config.0.clone() + }) + } +} + +impl AuthorizedResource for TrustQuorumConfig { + fn load_roles<'fut>( + &'fut self, + opctx: &'fut OpContext, + authn: &'fut authn::Context, + roleset: &'fut mut RoleSet, + ) -> futures::future::BoxFuture<'fut, Result<(), Error>> { + // There are no roles on this resource, but we still need to load the + // Rack-related roles. + self.rack().load_roles(opctx, authn, roleset) + } + + fn on_unauthorized( + &self, + _: &Authz, + error: Error, + _: AnyActor, + _: Action, + ) -> Error { + error + } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } +} + /// Synthetic resource describing the list of Certificates associated with a /// Silo #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/nexus/auth/src/authz/omicron.polar b/nexus/auth/src/authz/omicron.polar index 35e29206700..db913e20d82 100644 --- a/nexus/auth/src/authz/omicron.polar +++ b/nexus/auth/src/authz/omicron.polar @@ -611,6 +611,17 @@ resource DeviceAuthRequestList { has_relation(fleet: Fleet, "parent_fleet", collection: DeviceAuthRequestList) if collection.fleet = fleet; +# Describes the policy for creating and managing trust quorum configurations +# This may change in a multirack future to a per rack parent +resource TrustQuorumConfig { + permissions = [ "read", "modify" ]; + relations = { parent_fleet: Fleet }; + "read" if "viewer" on "parent_fleet"; + "modify" if "admin" on "parent_fleet"; +} +has_relation(fleet: Fleet, "parent_fleet", config: TrustQuorumConfig) + if config.rack.fleet = fleet; + # Describes the policy for creating and managing Silo certificates resource SiloCertificateList { permissions = [ "list_children", "create_child" ]; diff --git a/nexus/auth/src/authz/oso_generic.rs b/nexus/auth/src/authz/oso_generic.rs index 97a84a11adf..d0517182a79 100644 --- a/nexus/auth/src/authz/oso_generic.rs +++ b/nexus/auth/src/authz/oso_generic.rs @@ -126,6 +126,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result { AlertClassList::get_polar_class(), ScimClientBearerTokenList::get_polar_class(), MulticastGroupList::get_polar_class(), + TrustQuorumConfig::get_polar_class(), ]; for c in classes { oso_builder = oso_builder.register_class(c)?; diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 432ee8ab89b..8e9fa4b5dc0 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -69,7 +69,6 @@ use omicron_common::api::external::UserId; use omicron_common::api::internal::shared::PrivateIpConfig; use omicron_common::bail_unless; use omicron_uuid_kinds::GenericUuid; -use omicron_uuid_kinds::RackUuid; use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; @@ -980,10 +979,15 @@ impl DataStore { // Insert the initial trust quorum configuration if let Some(tq_config) = rack_init.initial_trust_quorum_configuration { + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id, + LookupType::ById(rack_id), + )); Self::tq_insert_rss_config_after_handoff( opctx, &conn, - RackUuid::from_untyped_uuid(rack_id), + authz_tq, tq_config.members, tq_config.coordinator ).await.map_err(|e| { diff --git a/nexus/db-queries/src/db/datastore/trust_quorum.rs b/nexus/db-queries/src/db/datastore/trust_quorum.rs index 9a2c99e43c0..7d19f4bc6a3 100644 --- a/nexus/db-queries/src/db/datastore/trust_quorum.rs +++ b/nexus/db-queries/src/db/datastore/trust_quorum.rs @@ -139,24 +139,20 @@ impl DataStore { pub async fn tq_insert_rss_config_after_handoff( opctx: &OpContext, conn: &async_bb8_diesel::Connection, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, initial_members: BTreeSet, coordinator: BaseboardId, ) -> Result<(), Error> { - let authz_rack = authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Modify, &authz_rack).await?; + opctx.authorize(authz::Action::Modify, &authz_tq).await?; + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); let initial_config = TrustQuorumConfig::new_rss_committed_config( rack_id, initial_members, coordinator, ); - Self::insert_tq_config_conn(opctx, conn, initial_config) + Self::insert_tq_config_conn(conn, initial_config) .await .map_err(|err| err.into_public_ignore_retries()) } @@ -165,15 +161,11 @@ impl DataStore { pub async fn tq_get_latest_config( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, ) -> OptionalLookupResult { - let authz_rack = authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Read, &authz_rack).await?; + opctx.authorize(authz::Action::Read, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); Self::tq_get_latest_config_with_members_conn(conn, rack_id) .await @@ -183,17 +175,13 @@ impl DataStore { pub async fn tq_list_config( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, pagparams: &DataPageParams<'_, i64>, ) -> ListResultVec { - let authz_rack = authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Read, &authz_rack).await?; + opctx.authorize(authz::Action::Read, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); use nexus_db_schema::schema::trust_quorum_configuration::dsl; paginated(dsl::trust_quorum_configuration, dsl::epoch, pagparams) .filter(dsl::rack_id.eq(DbTypedUuid::::from(rack_id))) @@ -207,16 +195,12 @@ impl DataStore { pub async fn tq_get_config( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, epoch: Epoch, ) -> OptionalLookupResult { - let authz_rack = authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Read, &authz_rack).await?; + opctx.authorize(authz::Action::Read, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); Self::tq_get_config_with_members_from_epoch_conn(conn, rack_id, epoch) .await @@ -368,12 +352,12 @@ impl DataStore { opctx: &OpContext, proposed: ProposedTrustQuorumConfig, ) -> Result<(), Error> { - let authz_rack = authz::Rack::new( + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( authz::FLEET, proposed.rack_id.into_untyped_uuid(), LookupType::ById(proposed.rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Modify, &authz_rack).await?; + )); + opctx.authorize(authz::Action::Modify, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; let err = OptionalError::new(); @@ -405,7 +389,7 @@ impl DataStore { } // Save the new config - Self::insert_tq_config_conn(opctx, &c, validated.config) + Self::insert_tq_config_conn(&c, validated.config) .await .map_err(|txn_error| txn_error.into_diesel(&err)) } @@ -654,12 +638,12 @@ impl DataStore { config: trust_quorum_types::configuration::Configuration, acked_prepares: BTreeSet, ) -> Result { - let authz_rack = authz::Rack::new( + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( authz::FLEET, config.rack_id.into_untyped_uuid(), LookupType::ById(config.rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Modify, &authz_rack).await?; + )); + opctx.authorize(authz::Action::Modify, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; let epoch = epoch_to_i64(config.epoch)?; @@ -816,18 +800,14 @@ impl DataStore { pub async fn tq_update_commit_status( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, epoch: Epoch, acked_commits: BTreeSet, ) -> Result { - let authz_rack = authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Modify, &authz_rack).await?; + opctx.authorize(authz::Action::Modify, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); let epoch = epoch_to_i64(epoch)?; let err = OptionalError::new(); @@ -925,18 +905,14 @@ impl DataStore { pub async fn tq_abort_config( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, epoch: Epoch, abort_reason: String, ) -> Result<(), Error> { - let authz_rack = authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Modify, &authz_rack).await?; + opctx.authorize(authz::Action::Modify, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); let epoch = epoch_to_i64(epoch)?; let err = OptionalError::new(); @@ -1014,17 +990,9 @@ impl DataStore { // Unconditional insert async fn insert_tq_config_conn( - opctx: &OpContext, conn: &async_bb8_diesel::Connection, config: TrustQuorumConfig, ) -> Result<(), TransactionError> { - let authz_rack = authz::Rack::new( - authz::FLEET, - config.rack_id.into_untyped_uuid(), - LookupType::ById(config.rack_id.into_untyped_uuid()), - ); - opctx.authorize(authz::Action::Modify, &authz_rack).await?; - let hw_baseboard_ids = Self::lookup_hw_baseboard_ids_conn( conn, config.members.keys().cloned(), @@ -1501,6 +1469,14 @@ mod tests { use omicron_uuid_kinds::RackUuid; use uuid::Uuid; + fn make_authz_tq(rack_id: RackUuid) -> authz::TrustQuorumConfig { + authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id.into_untyped_uuid(), + LookupType::ById(rack_id.into_untyped_uuid()), + )) + } + async fn insert_hw_baseboard_ids(db: &TestDatabase) -> Vec { let (_, datastore) = (db.opctx(), db.datastore()); let conn = datastore.pool_connection_for_tests().await.unwrap(); @@ -1548,7 +1524,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -1638,7 +1614,7 @@ mod tests { // Read the config back and check that it's preparing for LRTQ upgrade // with no acks. let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1676,7 +1652,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -1696,7 +1672,7 @@ mod tests { // Read the config back and check that it's preparing with no acks let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1730,7 +1706,7 @@ mod tests { let e = datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, acked_commits, ) @@ -1765,7 +1741,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1801,7 +1777,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1843,7 +1819,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1883,7 +1859,7 @@ mod tests { datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, coordinator_config.members.keys().cloned().collect(), ) @@ -1891,7 +1867,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1930,7 +1906,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -1965,7 +1941,7 @@ mod tests { }; let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -1989,7 +1965,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -2031,7 +2007,7 @@ mod tests { datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, coordinator_config .members @@ -2044,7 +2020,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -2076,7 +2052,7 @@ mod tests { datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, coordinator_config .members @@ -2089,7 +2065,7 @@ mod tests { .unwrap(); let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -2116,7 +2092,7 @@ mod tests { datastore.tq_insert_latest_config(opctx, next_config).await.unwrap(); let read_config = datastore - .tq_get_config(opctx, rack_id, Epoch(2)) + .tq_get_config(opctx, make_authz_tq(rack_id), Epoch(2)) .await .expect("no error") .expect("returned config"); @@ -2160,7 +2136,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -2180,13 +2156,23 @@ mod tests { // Aborting should succeed, since we haven't committed datastore - .tq_abort_config(opctx, config.rack_id, config.epoch, "test".into()) + .tq_abort_config( + opctx, + make_authz_tq(config.rack_id), + config.epoch, + "test".into(), + ) .await .unwrap(); // Aborting is idempotent datastore - .tq_abort_config(opctx, config.rack_id, config.epoch, "test".into()) + .tq_abort_config( + opctx, + make_authz_tq(config.rack_id), + config.epoch, + "test".into(), + ) .await .unwrap(); @@ -2239,7 +2225,7 @@ mod tests { // Retrieve the configuration and ensure it is actually aborted let read_config = datastore - .tq_get_latest_config(opctx, rack_id) + .tq_get_latest_config(opctx, make_authz_tq(rack_id)) .await .expect("no error") .expect("returned config"); @@ -2261,7 +2247,12 @@ mod tests { // Trying to abort the old config will fail because it's stale datastore - .tq_abort_config(opctx, config.rack_id, config.epoch, "test".into()) + .tq_abort_config( + opctx, + make_authz_tq(config.rack_id), + config.epoch, + "test".into(), + ) .await .unwrap_err(); @@ -2292,7 +2283,7 @@ mod tests { datastore .tq_abort_config( opctx, - config2.rack_id, + make_authz_tq(config2.rack_id), config2.epoch, "test".into(), ) @@ -2331,7 +2322,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) diff --git a/nexus/db-queries/src/policy_test/resource_builder.rs b/nexus/db-queries/src/policy_test/resource_builder.rs index 7cccfbeff1e..36077531740 100644 --- a/nexus/db-queries/src/policy_test/resource_builder.rs +++ b/nexus/db-queries/src/policy_test/resource_builder.rs @@ -302,6 +302,23 @@ impl_dyn_authorized_resource_for_global!(authz::SubnetPoolList); impl_dyn_authorized_resource_for_global!(authz::TargetReleaseConfig); impl_dyn_authorized_resource_for_global!(authz::UpdateTrustRootList); +impl DynAuthorizedResource for authz::TrustQuorumConfig { + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } + + fn resource_name(&self) -> String { + format!("{}: trust quorum configuration", self.rack().resource_name()) + } +} + impl DynAuthorizedResource for authz::SiloCertificateList { fn do_authorize<'a, 'b>( &'a self, diff --git a/nexus/db-queries/src/policy_test/resources.rs b/nexus/db-queries/src/policy_test/resources.rs index 0a2d8522b0e..f45db51b616 100644 --- a/nexus/db-queries/src/policy_test/resources.rs +++ b/nexus/db-queries/src/policy_test/resources.rs @@ -88,14 +88,10 @@ pub async fn make_resources( make_silo(&mut builder, "silo1", main_silo_id, true).await; make_silo(&mut builder, "silo2", Uuid::new_v4(), false).await; - // Various other resources - let rack_id = "c037e882-8b6d-c8b5-bef4-97e848eb0a50".parse().unwrap(); - builder.new_resource(authz::Rack::new( - authz::FLEET, - rack_id, - LookupType::ById(rack_id), - )); + // Rack hierarchy + make_rack(&mut builder); + // Various other resources let sled_id = "8a785566-adaf-c8d8-e886-bee7f9b73ca7".parse().unwrap(); builder.new_resource(authz::Sled::new( authz::FLEET, @@ -229,6 +225,15 @@ async fn make_services(builder: &mut ResourceBuilder<'_>) { )); } +/// Helper for `make_resources()` that constructs a small Rack hierarchy +fn make_rack(builder: &mut ResourceBuilder<'_>) { + let rack_id = "c037e882-8b6d-c8b5-bef4-97e848eb0a50".parse().unwrap(); + let rack = + authz::Rack::new(authz::FLEET, rack_id, LookupType::ById(rack_id)); + builder.new_resource(rack.clone()); + builder.new_resource(authz::TrustQuorumConfig::new(rack)); +} + /// Helper for `make_resources()` that constructs a small Silo hierarchy async fn make_silo( builder: &mut ResourceBuilder<'_>, diff --git a/nexus/src/app/background/tasks/trust_quorum.rs b/nexus/src/app/background/tasks/trust_quorum.rs index a79819fd138..6e62385e739 100644 --- a/nexus/src/app/background/tasks/trust_quorum.rs +++ b/nexus/src/app/background/tasks/trust_quorum.rs @@ -8,6 +8,7 @@ use crate::app::background::BackgroundTask; use crate::app::rack::rack_subnet; use anyhow::{Context, Error, anyhow, bail}; use futures::future::BoxFuture; +use nexus_auth::authz; use nexus_auth::context::OpContext; use nexus_db_model::SledUnderlaySubnetAllocation; use nexus_db_queries::db::DataStore; @@ -22,6 +23,7 @@ use nexus_types::trust_quorum::{ TrustQuorumMemberState, }; use omicron_common::address::{Ipv6Subnet, RACK_PREFIX, get_64_subnet}; +use omicron_common::api::external::LookupType; use omicron_uuid_kinds::{GenericUuid, RackUuid, SledUuid}; use parallel_task_set::ParallelTaskSet; use rand::seq::SliceRandom; @@ -211,8 +213,13 @@ async fn drive_reconfiguration( rack_id: RackUuid, epoch: Epoch, ) -> Result { + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id.into_untyped_uuid(), + LookupType::ById(rack_id.into_untyped_uuid()), + )); let Some(config) = datastore - .tq_get_config(&opctx, rack_id, epoch) + .tq_get_config(&opctx, authz_tq.clone(), epoch) .await .context("Failed to get tq configuration")? else { @@ -398,10 +405,15 @@ async fn commit( if !acked.is_empty() { // Write state back to DB + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + nexus_config.rack_id.into_untyped_uuid(), + LookupType::ById(nexus_config.rack_id.into_untyped_uuid()), + )); let state = datastore .tq_update_commit_status( &opctx, - nexus_config.rack_id, + authz_tq, nexus_config.epoch, acked.clone(), ) @@ -476,8 +488,13 @@ async fn allocate_subnets_and_start_sled_agents( // Retrieve the last committed configuration so we can diff members and see // who was added. + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id.into_untyped_uuid(), + LookupType::ById(rack_id.into_untyped_uuid()), + )); let Some(last_committed_config) = - datastore.tq_get_config(&opctx, rack_id, last_committed_epoch).await? + datastore.tq_get_config(&opctx, authz_tq, last_committed_epoch).await? else { bail!( "Failed to retrieve config from DB for rack {rack_id}, \ diff --git a/nexus/src/app/sled.rs b/nexus/src/app/sled.rs index c11f0577ec8..175f1951920 100644 --- a/nexus/src/app/sled.rs +++ b/nexus/src/app/sled.rs @@ -22,6 +22,7 @@ use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::LookupType; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::InstanceUuid; @@ -114,6 +115,11 @@ impl super::Nexus { sled_lookup.fetch_for(authz::Action::Modify).await?; let rack_id = RackUuid::from_untyped_uuid(sled.rack_id); + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + sled.rack_id, + LookupType::ById(sled.rack_id), + )); // If the sled still exists in the latest committed trust quorum // configuration, it cannot be expunged. @@ -134,7 +140,7 @@ impl super::Nexus { // for now given that the user already has to confirm a bunch of prompts // before kicking off the operation. let (tq_latest_committed_config, _) = self - .tq_load_latest_possible_committed_config(opctx, rack_id) + .tq_load_latest_possible_committed_config(opctx, authz_tq) .await?; let baseboard_id = BaseboardId { part_number: sled.part_number().to_string(), diff --git a/nexus/src/app/trust_quorum.rs b/nexus/src/app/trust_quorum.rs index f1776837620..170f40f6264 100644 --- a/nexus/src/app/trust_quorum.rs +++ b/nexus/src/app/trust_quorum.rs @@ -4,11 +4,12 @@ //! Nexus APIs for trust quorum +use nexus_auth::authz; use nexus_auth::context::OpContext; use nexus_types::trust_quorum::{ IsLrtqUpgrade, ProposedTrustQuorumConfig, TrustQuorumConfig, }; -use omicron_common::api::external::Error; +use omicron_common::api::external::{Error, LookupType}; use omicron_uuid_kinds::{GenericUuid, RackUuid, SledUuid}; use sled_hardware_types::BaseboardId; use std::collections::BTreeSet; @@ -24,11 +25,12 @@ impl super::Nexus { pub(crate) async fn tq_add_sleds( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, new_sleds: BTreeSet, ) -> Result { + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); let (latest_committed_config, latest_epoch) = self - .tq_load_latest_possible_committed_config(opctx, rack_id) + .tq_load_latest_possible_committed_config(opctx, authz_tq.clone()) .await?; let new_epoch = latest_epoch.next(); let proposed = self @@ -42,8 +44,10 @@ impl super::Nexus { // Read back the real configuration from the database. Importantly this // includes a chosen coordinator. - let Some(new_config) = - self.db_datastore.tq_get_config(opctx, rack_id, new_epoch).await? + let Some(new_config) = self + .db_datastore + .tq_get_config(opctx, authz_tq.clone(), new_epoch) + .await? else { return Err(Error::internal_error(&format!( "Cannot retrieve newly inserted trust quorum \ @@ -55,7 +59,7 @@ impl super::Nexus { let client = self .get_coordinator_client( opctx, - rack_id, + authz_tq, new_epoch, &new_config.coordinator, ) @@ -91,13 +95,18 @@ impl super::Nexus { // Look up the sled to get its rack_id and baseboard_id let (.., sled) = self.sled_lookup(opctx, &sled_id)?.fetch().await?; let rack_id = RackUuid::from_untyped_uuid(sled.rack_id); + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + sled.rack_id, + LookupType::ById(sled.rack_id), + )); let sled_to_remove = BaseboardId { part_number: sled.part_number().to_string(), serial_number: sled.serial_number().to_string(), }; let (latest_committed_config, latest_epoch) = self - .tq_load_latest_possible_committed_config(opctx, rack_id) + .tq_load_latest_possible_committed_config(opctx, authz_tq.clone()) .await?; let new_epoch = latest_epoch.next(); let proposed = self @@ -111,8 +120,10 @@ impl super::Nexus { // Read back the real configuration from the database. Importantly this // includes a chosen coordinator. - let Some(new_config) = - self.db_datastore.tq_get_config(opctx, rack_id, new_epoch).await? + let Some(new_config) = self + .db_datastore + .tq_get_config(opctx, authz_tq.clone(), new_epoch) + .await? else { return Err(Error::internal_error(&format!( "Cannot retrieve newly inserted trust quorum \ @@ -124,7 +135,7 @@ impl super::Nexus { let client = self .get_coordinator_client( opctx, - rack_id, + authz_tq, new_epoch, &new_config.coordinator, ) @@ -152,8 +163,15 @@ impl super::Nexus { opctx: &OpContext, rack_id: RackUuid, ) -> Result { - let Some(latest_config) = - self.db_datastore.tq_get_latest_config(opctx, rack_id).await? + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id.into_untyped_uuid(), + LookupType::ById(rack_id.into_untyped_uuid()), + )); + let Some(latest_config) = self + .db_datastore + .tq_get_latest_config(opctx, authz_tq.clone()) + .await? else { return Err(Error::non_resourcetype_not_found( "No trust quorum configuration exists for this rack", @@ -163,7 +181,7 @@ impl super::Nexus { self.db_datastore .tq_abort_config( opctx, - rack_id, + authz_tq.clone(), latest_config.epoch, "Aborted via API request".to_string(), ) @@ -171,7 +189,7 @@ impl super::Nexus { // Return the updated configuration self.db_datastore - .tq_get_config(opctx, rack_id, latest_config.epoch) + .tq_get_config(opctx, authz_tq, latest_config.epoch) .await? .ok_or_else(|| { Error::internal_error( @@ -194,10 +212,17 @@ impl super::Nexus { ) -> Result { // We are only operating on a single rack here. let rack_id = RackUuid::from_untyped_uuid(self.rack_id()); + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id.into_untyped_uuid(), + LookupType::ById(rack_id.into_untyped_uuid()), + )); // Let's first see if a configuration exists. - let new_epoch = if let Some(latest_config) = - self.db_datastore.tq_get_latest_config(opctx, rack_id).await? + let new_epoch = if let Some(latest_config) = self + .db_datastore + .tq_get_latest_config(opctx, authz_tq.clone()) + .await? { // Is there a committed configuration? `committing` is irreversable, // and also indicates trust quorum has taken over. @@ -259,8 +284,10 @@ impl super::Nexus { // Read back the real configuration from the database. Importantly this // includes a chosen coordinator. - let Some(new_config) = - self.db_datastore.tq_get_config(opctx, rack_id, new_epoch).await? + let Some(new_config) = self + .db_datastore + .tq_get_config(opctx, authz_tq.clone(), new_epoch) + .await? else { return Err(Error::internal_error(&format!( "Cannot retrieve newly inserted trust quorum \ @@ -273,7 +300,7 @@ impl super::Nexus { let client = self .get_coordinator_client( opctx, - rack_id, + authz_tq, new_epoch, &new_config.coordinator, ) @@ -296,10 +323,11 @@ impl super::Nexus { async fn get_coordinator_client( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, epoch: Epoch, coordinator: &BaseboardId, ) -> Result { + let rack_id = RackUuid::from_untyped_uuid(authz_tq.rack().id()); // Retrieve the sled for the coordinator let Some(sled) = self .db_datastore @@ -319,7 +347,7 @@ impl super::Nexus { ); self.db_datastore - .tq_abort_config(opctx, rack_id, epoch, msg.clone()) + .tq_abort_config(opctx, authz_tq, epoch, msg.clone()) .await .map_err(|e| { Error::conflict(format!( @@ -425,11 +453,15 @@ impl super::Nexus { pub async fn tq_load_latest_possible_committed_config( &self, opctx: &OpContext, - rack_id: RackUuid, + authz_tq: authz::TrustQuorumConfig, ) -> Result<(TrustQuorumConfig, Epoch), Error> { + let rack_id = authz_tq.rack().id(); + // First get the latest configuration for this rack. - let Some(latest_config) = - self.db_datastore.tq_get_latest_config(opctx, rack_id).await? + let Some(latest_config) = self + .db_datastore + .tq_get_latest_config(opctx, authz_tq.clone()) + .await? else { return Err(Error::invalid_request(format!( "Missing trust quorum configurations for rack {rack_id}. \ @@ -453,7 +485,7 @@ impl super::Nexus { // Load the configuration for the last commmitted epoch let Some(latest_committed_config) = - self.db_datastore.tq_get_config(opctx, rack_id, epoch).await? + self.db_datastore.tq_get_config(opctx, authz_tq, epoch).await? else { return Err(Error::invalid_request(format!( "Missing expected last committed trust quorum \ diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 48aa7ffb552..cfe5fef406c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -92,6 +92,7 @@ use omicron_common::api::external::InstanceNetworkInterface; use omicron_common::api::external::InternalContext; use omicron_common::api::external::LldpLinkConfig; use omicron_common::api::external::LldpNeighbor; +use omicron_common::api::external::LookupType; use omicron_common::api::external::LoopbackAddress; use omicron_common::api::external::NameOrId; use omicron_common::api::external::Probe; @@ -6360,10 +6361,14 @@ impl NexusExternalApi for NexusExternalApiImpl { ) -> Result, HttpError> { audit_and_time(&rqctx, |opctx, nexus| async move { let req = req.into_inner(); - let rack_id = - RackUuid::from_untyped_uuid(path_params.into_inner().rack_id); + let rack_id = path_params.into_inner().rack_id; + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + rack_id, + LookupType::ById(rack_id), + )); let status = - nexus.tq_add_sleds(&opctx, rack_id, req.sled_ids).await?; + nexus.tq_add_sleds(&opctx, authz_tq, req.sled_ids).await?; Ok(HttpResponseOk(status.into())) }) .await @@ -6390,16 +6395,20 @@ impl NexusExternalApi for NexusExternalApiImpl { let apictx = rqctx.context(); let nexus = &apictx.context.nexus; let path_params = path_params.into_inner(); - let rack_id = RackUuid::from_untyped_uuid(path_params.rack_id); let version = query_params.into_inner().version; let handler = async { let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + path_params.rack_id, + LookupType::ById(path_params.rack_id), + )); let status = if let Some(version) = version { let epoch = Epoch(version.0); - nexus.datastore().tq_get_config(&opctx, rack_id, epoch).await? + nexus.datastore().tq_get_config(&opctx, authz_tq, epoch).await? } else { - nexus.datastore().tq_get_latest_config(&opctx, rack_id).await? + nexus.datastore().tq_get_latest_config(&opctx, authz_tq).await? }; if let Some(status) = status { Ok(HttpResponseOk(status.into())) diff --git a/nexus/src/lockstep_api/http_entrypoints.rs b/nexus/src/lockstep_api/http_entrypoints.rs index 8c241323c8d..b9ebd9080a0 100644 --- a/nexus/src/lockstep_api/http_entrypoints.rs +++ b/nexus/src/lockstep_api/http_entrypoints.rs @@ -23,6 +23,7 @@ use dropshot::ResultsPage; use dropshot::TypedBody; use http::Response; use http::StatusCode; +use nexus_db_queries::authz; use nexus_lockstep_api::*; use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintMetadata; @@ -52,6 +53,7 @@ use nexus_types::trust_quorum::TrustQuorumConfig; use nexus_types_versions::latest::headers::RangeRequest; use omicron_common::api::external::Error; use omicron_common::api::external::Instance; +use omicron_common::api::external::LookupType; use omicron_common::api::external::http_pagination::PaginatedById; use omicron_common::api::external::http_pagination::PaginatedByTimeAndId; use omicron_common::api::external::http_pagination::ScanById; @@ -1080,15 +1082,19 @@ impl NexusLockstepApi for NexusLockstepApiImpl { let apictx = rqctx.context(); let nexus = &apictx.context.nexus; let path_params = path_params.into_inner(); - let rack_id = RackUuid::from_untyped_uuid(path_params.rack_id); let epoch = query_params.into_inner().epoch; let handler = async { let opctx = crate::context::op_context_for_internal_api(&rqctx).await; + let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( + authz::FLEET, + path_params.rack_id, + LookupType::ById(path_params.rack_id), + )); let config = if let Some(epoch) = epoch { - nexus.datastore().tq_get_config(&opctx, rack_id, epoch).await? + nexus.datastore().tq_get_config(&opctx, authz_tq, epoch).await? } else { - nexus.datastore().tq_get_latest_config(&opctx, rack_id).await? + nexus.datastore().tq_get_latest_config(&opctx, authz_tq).await? }; if let Some(config) = config { Ok(HttpResponseOk(config)) From f7fddb3dbcd7b6ac2d3a405c3a27cd2f015211e0 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Fri, 27 Feb 2026 23:16:31 +0000 Subject: [PATCH 2/5] expectorate --- nexus/db-queries/tests/output/authz-roles.out | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nexus/db-queries/tests/output/authz-roles.out b/nexus/db-queries/tests/output/authz-roles.out index a3e26758270..76c4477186f 100644 --- a/nexus/db-queries/tests/output/authz-roles.out +++ b/nexus/db-queries/tests/output/authz-roles.out @@ -1715,6 +1715,23 @@ resource: Rack id "c037e882-8b6d-c8b5-bef4-97e848eb0a50" unauthenticated ! ! ! ! ! ! ! ! scim ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ +resource: Rack id "c037e882-8b6d-c8b5-bef4-97e848eb0a50": trust quorum configuration + + USER Q R LC RP M MP CC D + fleet-admin ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ + fleet-collaborator ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-limited-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-limited-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! + scim ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + resource: Sled id "8a785566-adaf-c8d8-e886-bee7f9b73ca7" USER Q R LC RP M MP CC D From 48545ff47ced29a8d8c001d66ef2a4c5abf56eea Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Sat, 28 Feb 2026 08:07:55 +0000 Subject: [PATCH 3/5] Handle unauthorized requests properly --- nexus/auth/src/authz/api_resources.rs | 38 ++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/nexus/auth/src/authz/api_resources.rs b/nexus/auth/src/authz/api_resources.rs index 2ef06674c88..2eb17724a2f 100644 --- a/nexus/auth/src/authz/api_resources.rs +++ b/nexus/auth/src/authz/api_resources.rs @@ -694,6 +694,12 @@ impl TrustQuorumConfig { pub fn rack(&self) -> &Rack { &self.0 } + + fn not_found(&self) -> Error { + // The information that we are preventing from leaking is anything + // having to do with a given rack. + LookupType::ById(self.0.id()).into_not_found(ResourceType::Rack) + } } impl oso::PolarClass for TrustQuorumConfig { @@ -713,19 +719,39 @@ impl AuthorizedResource for TrustQuorumConfig { authn: &'fut authn::Context, roleset: &'fut mut RoleSet, ) -> futures::future::BoxFuture<'fut, Result<(), Error>> { - // There are no roles on this resource, but we still need to load the - // Rack-related roles. + // There are no roles on this resource, but we still need to walk the + // tree to get to the `fleet`. self.rack().load_roles(opctx, authn, roleset) } + // We want the trust quorum config to have the same visibility as the rack + // it is a part of. + // + // In a multirack world, we'll probably end up providing roles for racks. + // For now though, we just ensure that unauthorized users cannot know that a + // rack id exists, in the same manner as is done for an [`ApiResource`]. fn on_unauthorized( &self, - _: &Authz, + authz: &Authz, error: Error, - _: AnyActor, - _: Action, + actor: AnyActor, + action: Action, ) -> Error { - error + if action == Action::Read { + return self.not_found(); + } + + // If the user failed an authz check, and they can't even read this + // resource, then we should produce a 404 rather than a 401/403. + match authz.is_allowed(&actor, Action::Read, self) { + Err(error) => Error::internal_error(&format!( + "failed to compute read authorization to determine visibility: \ + {:#}", + error + )), + Ok(false) => self.not_found(), + Ok(true) => error, + } } fn polar_class(&self) -> oso::Class { From c36066fa606c009804c3b3e203dd76a09ee2beb6 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Tue, 3 Mar 2026 23:57:12 +0000 Subject: [PATCH 4/5] Review fixes --- dev-tools/omdb/src/bin/omdb/db.rs | 7 +---- nexus/auth/src/authz/api_resources.rs | 31 ++++++------------- nexus/db-queries/src/db/datastore/rack.rs | 9 +++--- .../src/db/datastore/trust_quorum.rs | 22 ++++--------- nexus/db-queries/src/policy_test/resources.rs | 5 ++- .../src/app/background/tasks/trust_quorum.rs | 20 +++--------- nexus/src/app/sled.rs | 7 +---- nexus/src/app/trust_quorum.rs | 20 +++--------- nexus/src/external_api/http_entrypoints.rs | 17 ++++------ nexus/src/lockstep_api/http_entrypoints.rs | 9 ++---- 10 files changed, 43 insertions(+), 104 deletions(-) diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 40f33783da6..2f50667f457 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -152,7 +152,6 @@ use omicron_common::api::external; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Generation; use omicron_common::api::external::InstanceState; -use omicron_common::api::external::LookupType; use omicron_common::api::external::MacAddr; use omicron_uuid_kinds::CollectionUuid; use omicron_uuid_kinds::DatasetUuid; @@ -8167,11 +8166,7 @@ async fn cmd_db_trust_quorum_list_configs( } let limit = fetch_opts.fetch_limit; - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - args.rack_id.into_untyped_uuid(), - LookupType::ById(args.rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(args.rack_id); let configs = datastore .tq_list_config(opctx, authz_tq, &first_page::(limit)) .await diff --git a/nexus/auth/src/authz/api_resources.rs b/nexus/auth/src/authz/api_resources.rs index 2eb17724a2f..ae721e6f495 100644 --- a/nexus/auth/src/authz/api_resources.rs +++ b/nexus/auth/src/authz/api_resources.rs @@ -42,6 +42,7 @@ use futures::future::BoxFuture; use nexus_db_fixed_data::FLEET_ID; use nexus_types::external_api::policy::{FleetRole, ProjectRole, SiloRole}; use omicron_common::api::external::{Error, LookupType, ResourceType}; +use omicron_uuid_kinds::{GenericUuid, RackUuid}; use oso::PolarClass; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -687,6 +688,14 @@ impl AuthorizedResource for Inventory { pub struct TrustQuorumConfig(Rack); impl TrustQuorumConfig { + pub fn for_rack_id(rack_id: RackUuid) -> TrustQuorumConfig { + Self::new(Rack::new( + FLEET, + rack_id.into_untyped_uuid(), + LookupType::ById(rack_id.into_untyped_uuid()), + )) + } + pub fn new(rack: Rack) -> TrustQuorumConfig { TrustQuorumConfig(rack) } @@ -694,12 +703,6 @@ impl TrustQuorumConfig { pub fn rack(&self) -> &Rack { &self.0 } - - fn not_found(&self) -> Error { - // The information that we are preventing from leaking is anything - // having to do with a given rack. - LookupType::ById(self.0.id()).into_not_found(ResourceType::Rack) - } } impl oso::PolarClass for TrustQuorumConfig { @@ -737,21 +740,7 @@ impl AuthorizedResource for TrustQuorumConfig { actor: AnyActor, action: Action, ) -> Error { - if action == Action::Read { - return self.not_found(); - } - - // If the user failed an authz check, and they can't even read this - // resource, then we should produce a 404 rather than a 401/403. - match authz.is_allowed(&actor, Action::Read, self) { - Err(error) => Error::internal_error(&format!( - "failed to compute read authorization to determine visibility: \ - {:#}", - error - )), - Ok(false) => self.not_found(), - Ok(true) => error, - } + self.rack().on_unauthorized(authz, error, actor, action) } fn polar_class(&self) -> oso::Class { diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 8e9fa4b5dc0..75c0d89ca5e 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -69,6 +69,7 @@ use omicron_common::api::external::UserId; use omicron_common::api::internal::shared::PrivateIpConfig; use omicron_common::bail_unless; use omicron_uuid_kinds::GenericUuid; +use omicron_uuid_kinds::RackUuid; use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; @@ -979,11 +980,9 @@ impl DataStore { // Insert the initial trust quorum configuration if let Some(tq_config) = rack_init.initial_trust_quorum_configuration { - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id, - LookupType::ById(rack_id), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id( + RackUuid::from_untyped_uuid(rack_id), + ); Self::tq_insert_rss_config_after_handoff( opctx, &conn, diff --git a/nexus/db-queries/src/db/datastore/trust_quorum.rs b/nexus/db-queries/src/db/datastore/trust_quorum.rs index 7d19f4bc6a3..1f0cc09c37c 100644 --- a/nexus/db-queries/src/db/datastore/trust_quorum.rs +++ b/nexus/db-queries/src/db/datastore/trust_quorum.rs @@ -3,6 +3,9 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Trust quorum related queries +//! +//! All methods that end with `_conn` in this module take a connection as a +//! parameter that is already authorized, so there is no need to reauthorize. use super::DataStore; use crate::authz; @@ -31,7 +34,6 @@ use nexus_types::trust_quorum::{ use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; -use omicron_common::api::external::LookupType; use omicron_common::api::external::OptionalLookupResult; use omicron_common::bail_unless; use omicron_uuid_kinds::GenericUuid; @@ -352,11 +354,7 @@ impl DataStore { opctx: &OpContext, proposed: ProposedTrustQuorumConfig, ) -> Result<(), Error> { - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - proposed.rack_id.into_untyped_uuid(), - LookupType::ById(proposed.rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(proposed.rack_id); opctx.authorize(authz::Action::Modify, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; @@ -638,11 +636,7 @@ impl DataStore { config: trust_quorum_types::configuration::Configuration, acked_prepares: BTreeSet, ) -> Result { - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - config.rack_id.into_untyped_uuid(), - LookupType::ById(config.rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(config.rack_id); opctx.authorize(authz::Action::Modify, &authz_tq).await?; let conn = &*self.pool_connection_authorized(opctx).await?; @@ -1470,11 +1464,7 @@ mod tests { use uuid::Uuid; fn make_authz_tq(rack_id: RackUuid) -> authz::TrustQuorumConfig { - authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - )) + authz::TrustQuorumConfig::for_rack_id(rack_id) } async fn insert_hw_baseboard_ids(db: &TestDatabase) -> Vec { diff --git a/nexus/db-queries/src/policy_test/resources.rs b/nexus/db-queries/src/policy_test/resources.rs index f45db51b616..8ce25a87fd1 100644 --- a/nexus/db-queries/src/policy_test/resources.rs +++ b/nexus/db-queries/src/policy_test/resources.rs @@ -10,6 +10,7 @@ use nexus_auth::authz; use omicron_common::api::external::LookupType; use omicron_uuid_kinds::AccessTokenKind; use omicron_uuid_kinds::PhysicalDiskUuid; +use omicron_uuid_kinds::RackUuid; use omicron_uuid_kinds::SiloGroupUuid; use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SupportBundleUuid; @@ -231,7 +232,9 @@ fn make_rack(builder: &mut ResourceBuilder<'_>) { let rack = authz::Rack::new(authz::FLEET, rack_id, LookupType::ById(rack_id)); builder.new_resource(rack.clone()); - builder.new_resource(authz::TrustQuorumConfig::new(rack)); + builder.new_resource(authz::TrustQuorumConfig::for_rack_id( + RackUuid::from_untyped_uuid(rack_id), + )); } /// Helper for `make_resources()` that constructs a small Silo hierarchy diff --git a/nexus/src/app/background/tasks/trust_quorum.rs b/nexus/src/app/background/tasks/trust_quorum.rs index 6e62385e739..ab4f781b0b8 100644 --- a/nexus/src/app/background/tasks/trust_quorum.rs +++ b/nexus/src/app/background/tasks/trust_quorum.rs @@ -23,7 +23,6 @@ use nexus_types::trust_quorum::{ TrustQuorumMemberState, }; use omicron_common::address::{Ipv6Subnet, RACK_PREFIX, get_64_subnet}; -use omicron_common::api::external::LookupType; use omicron_uuid_kinds::{GenericUuid, RackUuid, SledUuid}; use parallel_task_set::ParallelTaskSet; use rand::seq::SliceRandom; @@ -213,11 +212,7 @@ async fn drive_reconfiguration( rack_id: RackUuid, epoch: Epoch, ) -> Result { - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); let Some(config) = datastore .tq_get_config(&opctx, authz_tq.clone(), epoch) .await @@ -405,11 +400,8 @@ async fn commit( if !acked.is_empty() { // Write state back to DB - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - nexus_config.rack_id.into_untyped_uuid(), - LookupType::ById(nexus_config.rack_id.into_untyped_uuid()), - )); + let authz_tq = + authz::TrustQuorumConfig::for_rack_id(nexus_config.rack_id); let state = datastore .tq_update_commit_status( &opctx, @@ -488,11 +480,7 @@ async fn allocate_subnets_and_start_sled_agents( // Retrieve the last committed configuration so we can diff members and see // who was added. - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); let Some(last_committed_config) = datastore.tq_get_config(&opctx, authz_tq, last_committed_epoch).await? else { diff --git a/nexus/src/app/sled.rs b/nexus/src/app/sled.rs index 175f1951920..e204e95cf69 100644 --- a/nexus/src/app/sled.rs +++ b/nexus/src/app/sled.rs @@ -22,7 +22,6 @@ use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; -use omicron_common::api::external::LookupType; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::InstanceUuid; @@ -115,11 +114,7 @@ impl super::Nexus { sled_lookup.fetch_for(authz::Action::Modify).await?; let rack_id = RackUuid::from_untyped_uuid(sled.rack_id); - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - sled.rack_id, - LookupType::ById(sled.rack_id), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); // If the sled still exists in the latest committed trust quorum // configuration, it cannot be expunged. diff --git a/nexus/src/app/trust_quorum.rs b/nexus/src/app/trust_quorum.rs index 170f40f6264..c19bbd74f6d 100644 --- a/nexus/src/app/trust_quorum.rs +++ b/nexus/src/app/trust_quorum.rs @@ -9,7 +9,7 @@ use nexus_auth::context::OpContext; use nexus_types::trust_quorum::{ IsLrtqUpgrade, ProposedTrustQuorumConfig, TrustQuorumConfig, }; -use omicron_common::api::external::{Error, LookupType}; +use omicron_common::api::external::Error; use omicron_uuid_kinds::{GenericUuid, RackUuid, SledUuid}; use sled_hardware_types::BaseboardId; use std::collections::BTreeSet; @@ -95,11 +95,7 @@ impl super::Nexus { // Look up the sled to get its rack_id and baseboard_id let (.., sled) = self.sled_lookup(opctx, &sled_id)?.fetch().await?; let rack_id = RackUuid::from_untyped_uuid(sled.rack_id); - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - sled.rack_id, - LookupType::ById(sled.rack_id), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); let sled_to_remove = BaseboardId { part_number: sled.part_number().to_string(), serial_number: sled.serial_number().to_string(), @@ -163,11 +159,7 @@ impl super::Nexus { opctx: &OpContext, rack_id: RackUuid, ) -> Result { - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); let Some(latest_config) = self .db_datastore .tq_get_latest_config(opctx, authz_tq.clone()) @@ -212,11 +204,7 @@ impl super::Nexus { ) -> Result { // We are only operating on a single rack here. let rack_id = RackUuid::from_untyped_uuid(self.rack_id()); - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id.into_untyped_uuid(), - LookupType::ById(rack_id.into_untyped_uuid()), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); // Let's first see if a configuration exists. let new_epoch = if let Some(latest_config) = self diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index cfe5fef406c..e5b7e91fd29 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -92,7 +92,6 @@ use omicron_common::api::external::InstanceNetworkInterface; use omicron_common::api::external::InternalContext; use omicron_common::api::external::LldpLinkConfig; use omicron_common::api::external::LldpNeighbor; -use omicron_common::api::external::LookupType; use omicron_common::api::external::LoopbackAddress; use omicron_common::api::external::NameOrId; use omicron_common::api::external::Probe; @@ -6362,11 +6361,9 @@ impl NexusExternalApi for NexusExternalApiImpl { audit_and_time(&rqctx, |opctx, nexus| async move { let req = req.into_inner(); let rack_id = path_params.into_inner().rack_id; - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - rack_id, - LookupType::ById(rack_id), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id( + RackUuid::from_untyped_uuid(rack_id), + ); let status = nexus.tq_add_sleds(&opctx, authz_tq, req.sled_ids).await?; Ok(HttpResponseOk(status.into())) @@ -6399,11 +6396,9 @@ impl NexusExternalApi for NexusExternalApiImpl { let handler = async { let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - path_params.rack_id, - LookupType::ById(path_params.rack_id), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id( + RackUuid::from_untyped_uuid(path_params.rack_id), + ); let status = if let Some(version) = version { let epoch = Epoch(version.0); nexus.datastore().tq_get_config(&opctx, authz_tq, epoch).await? diff --git a/nexus/src/lockstep_api/http_entrypoints.rs b/nexus/src/lockstep_api/http_entrypoints.rs index b9ebd9080a0..3d60133a5ad 100644 --- a/nexus/src/lockstep_api/http_entrypoints.rs +++ b/nexus/src/lockstep_api/http_entrypoints.rs @@ -53,7 +53,6 @@ use nexus_types::trust_quorum::TrustQuorumConfig; use nexus_types_versions::latest::headers::RangeRequest; use omicron_common::api::external::Error; use omicron_common::api::external::Instance; -use omicron_common::api::external::LookupType; use omicron_common::api::external::http_pagination::PaginatedById; use omicron_common::api::external::http_pagination::PaginatedByTimeAndId; use omicron_common::api::external::http_pagination::ScanById; @@ -1086,11 +1085,9 @@ impl NexusLockstepApi for NexusLockstepApiImpl { let handler = async { let opctx = crate::context::op_context_for_internal_api(&rqctx).await; - let authz_tq = authz::TrustQuorumConfig::new(authz::Rack::new( - authz::FLEET, - path_params.rack_id, - LookupType::ById(path_params.rack_id), - )); + let authz_tq = authz::TrustQuorumConfig::for_rack_id( + RackUuid::from_untyped_uuid(path_params.rack_id), + ); let config = if let Some(epoch) = epoch { nexus.datastore().tq_get_config(&opctx, authz_tq, epoch).await? } else { From 1f27a9468800a60ca45d15cff9020e77ecbf2c62 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Thu, 5 Mar 2026 00:20:40 +0000 Subject: [PATCH 5/5] whoopsie --- nexus/db-queries/src/policy_test/resources.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nexus/db-queries/src/policy_test/resources.rs b/nexus/db-queries/src/policy_test/resources.rs index 8ce25a87fd1..17a4dd6d386 100644 --- a/nexus/db-queries/src/policy_test/resources.rs +++ b/nexus/db-queries/src/policy_test/resources.rs @@ -9,6 +9,7 @@ use super::resource_builder::ResourceSet; use nexus_auth::authz; use omicron_common::api::external::LookupType; use omicron_uuid_kinds::AccessTokenKind; +use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::PhysicalDiskUuid; use omicron_uuid_kinds::RackUuid; use omicron_uuid_kinds::SiloGroupUuid;