Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
9 changes: 8 additions & 1 deletion dev-tools/omdb/src/bin/omdb/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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::<i64>(limit))
.tq_list_config(opctx, authz_tq, &first_page::<i64>(limit))
.await
.context("listing trust quorum configurations")?;

Expand Down
78 changes: 78 additions & 0 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,84 @@ 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
}

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about self.rack().not_found() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need for this actually goes away when calling self.rack().on_unauthorized() below.

}
}

impl oso::PolarClass for TrustQuorumConfig {
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
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 {
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,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this behavior is wrong but it feels wrong that you should have to duplicate it here. Even though it's short, it's pretty tricky and load-bearing.

I wonder if it would make sense (or even work) to call self.rack().on_unauthorized(authz, error, actor, action), since logically you're trying to match the rack's behavior.

The other obvious option would be to impl ApiResource but I guess that doesn't make sense here because there's no ResourceType for it and we don't want one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thought about implementing ApiResource but that seemed to contradict the whole synthetic resource thing.

I'll try calling self.rack().on_unauthorized. I didn't think of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to have worked. Thanks!

}

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)]
Expand Down
11 changes: 11 additions & 0 deletions nexus/auth/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -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" ];
Expand Down
1 change: 1 addition & 0 deletions nexus/auth/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
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)?;
Expand Down
8 changes: 6 additions & 2 deletions nexus/db-queries/src/db/datastore/rack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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| {
Expand Down
Loading
Loading