diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 6686c29b675..2f50667f457 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; @@ -8165,8 +8166,9 @@ async fn cmd_db_trust_quorum_list_configs( } let limit = fetch_opts.fetch_limit; + let authz_tq = authz::TrustQuorumConfig::for_rack_id(args.rack_id); 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..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; @@ -681,6 +682,72 @@ 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 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) + } + + 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 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, + error: Error, + actor: AnyActor, + action: Action, + ) -> Error { + self.rack().on_unauthorized(authz, error, actor, action) + } + + 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..75c0d89ca5e 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -980,10 +980,13 @@ impl DataStore { // Insert the initial trust quorum configuration if let Some(tq_config) = rack_init.initial_trust_quorum_configuration { + let authz_tq = authz::TrustQuorumConfig::for_rack_id( + RackUuid::from_untyped_uuid(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..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; @@ -139,24 +141,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 +163,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 +177,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 +197,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 +354,8 @@ impl DataStore { opctx: &OpContext, proposed: ProposedTrustQuorumConfig, ) -> Result<(), Error> { - let authz_rack = 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?; + 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?; let err = OptionalError::new(); @@ -405,7 +387,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 +636,8 @@ impl DataStore { config: trust_quorum_types::configuration::Configuration, acked_prepares: BTreeSet, ) -> Result { - 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 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?; let epoch = epoch_to_i64(config.epoch)?; @@ -816,18 +794,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 +899,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 +984,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 +1463,10 @@ mod tests { use omicron_uuid_kinds::RackUuid; use uuid::Uuid; + fn make_authz_tq(rack_id: RackUuid) -> authz::TrustQuorumConfig { + authz::TrustQuorumConfig::for_rack_id(rack_id) + } + 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 +1514,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -1638,7 +1604,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 +1642,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -1696,7 +1662,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 +1696,7 @@ mod tests { let e = datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, acked_commits, ) @@ -1765,7 +1731,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 +1767,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 +1809,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 +1849,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 +1857,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 +1896,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -1965,7 +1931,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 +1955,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 +1997,7 @@ mod tests { datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, coordinator_config .members @@ -2044,7 +2010,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 +2042,7 @@ mod tests { datastore .tq_update_commit_status( opctx, - rack_id, + make_authz_tq(rack_id), config.epoch, coordinator_config .members @@ -2089,7 +2055,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 +2082,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 +2126,7 @@ mod tests { DataStore::tq_insert_rss_config_after_handoff( opctx, &conn, - rack_id, + make_authz_tq(rack_id), members.clone(), coordinator, ) @@ -2180,13 +2146,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 +2215,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 +2237,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 +2273,7 @@ mod tests { datastore .tq_abort_config( opctx, - config2.rack_id, + make_authz_tq(config2.rack_id), config2.epoch, "test".into(), ) @@ -2331,7 +2312,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..17a4dd6d386 100644 --- a/nexus/db-queries/src/policy_test/resources.rs +++ b/nexus/db-queries/src/policy_test/resources.rs @@ -9,7 +9,9 @@ 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; use omicron_uuid_kinds::SiloUserUuid; use omicron_uuid_kinds::SupportBundleUuid; @@ -88,14 +90,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 +227,17 @@ 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::for_rack_id( + RackUuid::from_untyped_uuid(rack_id), + )); +} + /// Helper for `make_resources()` that constructs a small Silo hierarchy async fn make_silo( builder: &mut ResourceBuilder<'_>, 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 diff --git a/nexus/src/app/background/tasks/trust_quorum.rs b/nexus/src/app/background/tasks/trust_quorum.rs index a79819fd138..ab4f781b0b8 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; @@ -211,8 +212,9 @@ async fn drive_reconfiguration( rack_id: RackUuid, epoch: Epoch, ) -> Result { + let authz_tq = authz::TrustQuorumConfig::for_rack_id(rack_id); 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 +400,12 @@ async fn commit( if !acked.is_empty() { // Write state back to DB + let authz_tq = + authz::TrustQuorumConfig::for_rack_id(nexus_config.rack_id); let state = datastore .tq_update_commit_status( &opctx, - nexus_config.rack_id, + authz_tq, nexus_config.epoch, acked.clone(), ) @@ -476,8 +480,9 @@ 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::for_rack_id(rack_id); 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..e204e95cf69 100644 --- a/nexus/src/app/sled.rs +++ b/nexus/src/app/sled.rs @@ -114,6 +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::for_rack_id(rack_id); // If the sled still exists in the latest committed trust quorum // configuration, it cannot be expunged. @@ -134,7 +135,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..c19bbd74f6d 100644 --- a/nexus/src/app/trust_quorum.rs +++ b/nexus/src/app/trust_quorum.rs @@ -4,6 +4,7 @@ //! Nexus APIs for trust quorum +use nexus_auth::authz; use nexus_auth::context::OpContext; use nexus_types::trust_quorum::{ IsLrtqUpgrade, ProposedTrustQuorumConfig, TrustQuorumConfig, @@ -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,14 @@ 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::for_rack_id(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 +116,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 +131,7 @@ impl super::Nexus { let client = self .get_coordinator_client( opctx, - rack_id, + authz_tq, new_epoch, &new_config.coordinator, ) @@ -152,8 +159,11 @@ 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::for_rack_id(rack_id); + 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 +173,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 +181,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 +204,13 @@ 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::for_rack_id(rack_id); // 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 +272,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 +288,7 @@ impl super::Nexus { let client = self .get_coordinator_client( opctx, - rack_id, + authz_tq, new_epoch, &new_config.coordinator, ) @@ -296,10 +311,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 +335,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 +441,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 +473,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..e5b7e91fd29 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -6360,10 +6360,12 @@ 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::for_rack_id( + RackUuid::from_untyped_uuid(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 +6392,18 @@ 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::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, 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..3d60133a5ad 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; @@ -1080,15 +1081,17 @@ 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::for_rack_id( + RackUuid::from_untyped_uuid(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))