Skip to content
164 changes: 79 additions & 85 deletions nexus/src/app/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,49 @@

use crate::authz::ApiResource;
use crate::context::OpContext;
use crate::db;
use crate::db::identity::{Asset, Resource};
use crate::db::lookup::LookupPath;
use crate::db::model::Name;
use crate::db::model::SshKey;
use crate::db::{self, lookup};
use crate::external_api::params;
use crate::external_api::shared;
use crate::{authn, authz};
use anyhow::Context;
use nexus_db_model::UserProvisionType;
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::UpdateResult;
use omicron_common::api::external::{CreateResult, LookupType};
use omicron_common::api::external::{DataPageParams, ResourceType};
use omicron_common::api::external::{DeleteResult, NameOrId};
use omicron_common::bail_unless;
use ref_cast::RefCast;
use std::str::FromStr;
use uuid::Uuid;

impl super::Nexus {
// Silos
pub fn silo_lookup<'a>(
&'a self,
opctx: &'a OpContext,
silo: &'a NameOrId,
) -> LookupResult<lookup::Silo<'a>> {
match silo {
NameOrId::Id(id) => {
let silo =
LookupPath::new(opctx, &self.db_datastore).silo_id(*id);
Ok(silo)
}
NameOrId::Name(name) => {
let silo = LookupPath::new(opctx, &self.db_datastore)
.silo_name(Name::ref_cast(name));
Ok(silo)
}
}
}
Comment on lines +34 to +51
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 lookup mechanism differs from the normal lookups in that it doesn't take a selector. Once we've moved over to the V1 API I fully expect we'll simplify the selector implementations anyway so I think it's fine that this one case is slightly different.

Copy link
Contributor

Choose a reason for hiding this comment

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


pub async fn silo_create(
&self,
Expand All @@ -54,40 +73,13 @@ impl super::Nexus {
self.db_datastore.silos_list(opctx, pagparams).await
}

pub async fn silo_fetch(
&self,
opctx: &OpContext,
name: &Name,
) -> LookupResult<db::model::Silo> {
let (.., db_silo) = LookupPath::new(opctx, &self.db_datastore)
.silo_name(name)
.fetch()
.await?;
Ok(db_silo)
}

pub async fn silo_fetch_by_id(
&self,
opctx: &OpContext,
silo_id: &Uuid,
) -> LookupResult<db::model::Silo> {
let (.., db_silo) = LookupPath::new(opctx, &self.db_datastore)
.silo_id(*silo_id)
.fetch()
.await?;
Ok(db_silo)
}

pub async fn silo_delete(
&self,
opctx: &OpContext,
name: &Name,
silo_lookup: &lookup::Silo<'_>,
) -> DeleteResult {
let (.., authz_silo, db_silo) =
LookupPath::new(opctx, &self.db_datastore)
.silo_name(name)
.fetch_for(authz::Action::Delete)
.await?;
silo_lookup.fetch_for(authz::Action::Delete).await?;
self.db_datastore.silo_delete(opctx, &authz_silo, &db_silo).await
}

Expand All @@ -96,7 +88,7 @@ impl super::Nexus {
pub async fn silo_fetch_policy(
&self,
opctx: &OpContext,
silo_lookup: db::lookup::Silo<'_>,
silo_lookup: &lookup::Silo<'_>,
) -> LookupResult<shared::Policy<authz::SiloRole>> {
let (.., authz_silo) =
silo_lookup.lookup_for(authz::Action::ReadPolicy).await?;
Expand All @@ -114,7 +106,7 @@ impl super::Nexus {
pub async fn silo_update_policy(
&self,
opctx: &OpContext,
silo_lookup: db::lookup::Silo<'_>,
silo_lookup: &lookup::Silo<'_>,
policy: &shared::Policy<authz::SiloRole>,
) -> UpdateResult<shared::Policy<authz::SiloRole>> {
let (.., authz_silo) =
Expand Down Expand Up @@ -164,13 +156,10 @@ impl super::Nexus {
pub async fn silo_list_users(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<db::model::SiloUser> {
let (authz_silo,) = LookupPath::new(opctx, self.datastore())
.silo_name(silo_name)
.lookup_for(authz::Action::Read)
.await?;
let (authz_silo,) = silo_lookup.lookup_for(authz::Action::Read).await?;
let authz_silo_user_list = authz::SiloUserList::new(authz_silo);
self.db_datastore
.silo_users_list(opctx, &authz_silo_user_list, pagparams)
Expand All @@ -181,13 +170,10 @@ impl super::Nexus {
pub async fn silo_user_fetch(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
silo_user_id: Uuid,
) -> LookupResult<db::model::SiloUser> {
let (authz_silo,) = LookupPath::new(opctx, self.datastore())
.silo_name(silo_name)
.lookup_for(authz::Action::Read)
.await?;
let (authz_silo,) = silo_lookup.lookup_for(authz::Action::Read).await?;
Copy link
Contributor

@david-crespo david-crespo Mar 6, 2023

Choose a reason for hiding this comment

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

As discussed in chat with @zephraph, we considered the possibility that this would succeed at looking up a user by ID as long as the specified silo exists, even if the silo doesn't match the user's ID (because the silo is technically not needed for the lookup if we have a user ID to look up directly). But there is an explicit check that the retrieved user's silo ID matches the one passed in:

if db_silo_user.silo_id != authz_silo.id() {
return Err(authz_silo_user.not_found());
}

let (_, db_silo_user) = self
.silo_user_lookup_by_id(
opctx,
Expand All @@ -208,13 +194,9 @@ impl super::Nexus {
/// provider.
async fn local_idp_fetch_silo(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
) -> LookupResult<(authz::Silo, db::model::Silo)> {
let (authz_silo, db_silo) = LookupPath::new(opctx, &self.db_datastore)
.silo_name(silo_name)
.fetch()
.await?;
let (authz_silo, db_silo) = silo_lookup.fetch().await?;
if db_silo.user_provision_type != UserProvisionType::ApiOnly {
return Err(Error::not_found_by_name(
ResourceType::IdentityProvider,
Expand All @@ -229,11 +211,11 @@ impl super::Nexus {
pub async fn local_idp_create_user(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
new_user_params: params::UserCreate,
) -> CreateResult<db::model::SiloUser> {
let (authz_silo, db_silo) =
self.local_idp_fetch_silo(opctx, silo_name).await?;
self.local_idp_fetch_silo(silo_lookup).await?;
let authz_silo_user_list = authz::SiloUserList::new(authz_silo.clone());
// TODO-cleanup This authz check belongs in silo_user_create().
opctx
Expand Down Expand Up @@ -267,11 +249,11 @@ impl super::Nexus {
pub async fn local_idp_delete_user(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
silo_user_id: Uuid,
) -> DeleteResult {
let (authz_silo, _) =
self.local_idp_fetch_silo(opctx, silo_name).await?;
let (authz_silo, _) = self.local_idp_fetch_silo(silo_lookup).await?;

let (authz_silo_user, _) = self
.silo_user_lookup_by_id(
opctx,
Expand Down Expand Up @@ -394,12 +376,12 @@ impl super::Nexus {
pub async fn local_idp_user_set_password(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
silo_user_id: Uuid,
password_value: params::UserPassword,
) -> UpdateResult<()> {
let (authz_silo, db_silo) =
self.local_idp_fetch_silo(opctx, silo_name).await?;
self.local_idp_fetch_silo(silo_lookup).await?;
let (authz_silo_user, db_silo_user) = self
.silo_user_lookup_by_id(
opctx,
Expand Down Expand Up @@ -506,11 +488,10 @@ impl super::Nexus {
pub async fn login_local(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
credentials: params::UsernamePasswordCredentials,
) -> Result<Option<db::model::SiloUser>, Error> {
let (authz_silo, _) =
self.local_idp_fetch_silo(opctx, silo_name).await?;
let (authz_silo, _) = self.local_idp_fetch_silo(silo_lookup).await?;

// NOTE: It's very important that we not bail out early if we fail to
// find a user with this external id. See the note in
Expand Down Expand Up @@ -640,16 +621,46 @@ impl super::Nexus {

// identity providers

pub fn saml_identity_provider_lookup<'a>(
&'a self,
opctx: &'a OpContext,
saml_identity_provider_selector: &'a params::SamlIdentityProviderSelector,
) -> LookupResult<lookup::SamlIdentityProvider<'a>> {
match saml_identity_provider_selector {
params::SamlIdentityProviderSelector {
saml_identity_provider: NameOrId::Id(id),
silo_selector: None,
} => {

let saml_provider = LookupPath::new(opctx, &self.db_datastore)
.saml_identity_provider_id(*id);
Ok(saml_provider)
}
params::SamlIdentityProviderSelector {
saml_identity_provider: NameOrId::Name(name),
silo_selector: Some(silo_selector),
} => {
let saml_provider = self
.silo_lookup(opctx, &silo_selector.silo)?
.saml_identity_provider_name(Name::ref_cast(name));
Ok(saml_provider)
}
params::SamlIdentityProviderSelector {
saml_identity_provider: NameOrId::Id(_),
silo_selector: Some(_),
} => Err(Error::invalid_request("when providing provider as an ID, silo should not be specified")),
_ => Err(Error::invalid_request("provider should either be a UUID or silo should be specified"))
Copy link
Contributor

Choose a reason for hiding this comment

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

nittiest of nits

Suggested change
_ => Err(Error::invalid_request("provider should either be a UUID or silo should be specified"))
_ => Err(Error::invalid_request("either provider should be a UUID or silo should be specified"))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm 👍 to this, but the message is consistent between all the other lookup functions. Maybe we should make this change across all of them at the same time?

Also makes me wonder if the should just be a lookup error that wraps around these cases 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, probably do a followup to change them all.

}
}

pub async fn identity_provider_list(
&self,
opctx: &OpContext,
silo_name: &Name,
pagparams: &DataPageParams<'_, Name>,
silo_lookup: &lookup::Silo<'_>,
pagparams: &PaginatedBy<'_>,
) -> ListResultVec<db::model::IdentityProvider> {
let (authz_silo, ..) = LookupPath::new(opctx, &self.db_datastore)
.silo_name(silo_name)
.fetch()
.await?;
// TODO-security: This should likely be lookup_for ListChildren on the silo
let (.., authz_silo, _) = silo_lookup.fetch().await?;
let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo);
self.db_datastore
.identity_provider_list(opctx, &authz_idp_list, pagparams)
Expand All @@ -661,13 +672,11 @@ impl super::Nexus {
pub async fn saml_identity_provider_create(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_lookup: &lookup::Silo<'_>,
params: params::SamlIdentityProviderCreate,
) -> CreateResult<db::model::SamlIdentityProvider> {
let (authz_silo, db_silo) = LookupPath::new(opctx, &self.db_datastore)
.silo_name(silo_name)
.fetch()
.await?;
// TODO-security: This should likely be fetch_for CreateChild on the silo
let (authz_silo, db_silo) = silo_lookup.fetch().await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

wouldn't that auth check get done when you actually do the creating?

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 is a bit of an imperfect process across the API. We do a lot of the authz calls twice. Once at the entrance of the nexus layer and again at the datastore layer. Sometimes the nexus layer has more authz checks because there are logical permissions that must be validated beyond just what the datastore layer would check. @davepacheco has mentioned potentially caching duplicatively authz queries.

The pattern here is fairly typical though. Do an authz check at the entrance of the nexus function.

let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo);

if db_silo.user_provision_type != UserProvisionType::Jit {
Expand Down Expand Up @@ -792,21 +801,6 @@ impl super::Nexus {
.await
}

pub async fn saml_identity_provider_fetch(
&self,
opctx: &OpContext,
silo_name: &Name,
provider_name: &Name,
) -> LookupResult<db::model::SamlIdentityProvider> {
let (.., saml_identity_provider) =
LookupPath::new(opctx, &self.datastore())
.silo_name(silo_name)
.saml_identity_provider_name(provider_name)
.fetch()
.await?;
Ok(saml_identity_provider)
}

pub fn silo_group_lookup<'a>(
&'a self,
opctx: &'a OpContext,
Expand Down
28 changes: 19 additions & 9 deletions nexus/src/db/datastore/identity_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,38 @@ use crate::db::pagination::paginated;
use async_bb8_diesel::AsyncConnection;
use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::prelude::*;
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DataPageParams;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::ResourceType;
use ref_cast::RefCast;

impl DataStore {
pub async fn identity_provider_list(
&self,
opctx: &OpContext,
authz_idp_list: &authz::SiloIdentityProviderList,
pagparams: &DataPageParams<'_, Name>,
pagparams: &PaginatedBy<'_>,
) -> ListResultVec<IdentityProvider> {
opctx.authorize(authz::Action::ListChildren, authz_idp_list).await?;

use db::schema::identity_provider::dsl;
paginated(dsl::identity_provider, dsl::name, pagparams)
.filter(dsl::silo_id.eq(authz_idp_list.silo().id()))
.filter(dsl::time_deleted.is_null())
.select(IdentityProvider::as_select())
.load_async::<IdentityProvider>(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
match pagparams {
PaginatedBy::Id(pagparams) => {
paginated(dsl::identity_provider, dsl::id, pagparams)
}
PaginatedBy::Name(pagparams) => paginated(
dsl::identity_provider,
dsl::name,
&pagparams.map_name(|n| Name::ref_cast(n)),
),
}
.filter(dsl::silo_id.eq(authz_idp_list.silo().id()))
.filter(dsl::time_deleted.is_null())
.select(IdentityProvider::as_select())
.load_async::<IdentityProvider>(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

pub async fn saml_identity_provider_create(
Expand Down
11 changes: 11 additions & 0 deletions nexus/src/db/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ impl<'a> LookupPath<'a> {
{
Certificate::PrimaryKey(Root { lookup_root: self }, id)
}

/// Select a resource of type SamlIdentityProvider, identified by its id
pub fn saml_identity_provider_id<'b>(
self,
id: Uuid,
) -> SamlIdentityProvider<'b>
where
'a: 'b,
{
SamlIdentityProvider::PrimaryKey(Root { lookup_root: self }, id)
}
}

/// Represents the head of the selection path for a resource
Expand Down
8 changes: 4 additions & 4 deletions nexus/src/external_api/console_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,16 +412,16 @@ pub async fn login_local(
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.nexus;
let path_params = path_params.into_inner();
let path = path_params.into_inner();
let credentials = credentials.into_inner();
let silo = path.silo_name.into();

// By definition, this request is not authenticated. These operations
// happen using the Nexus "external authentication" context, which we
// keep specifically for this purpose.
let opctx = nexus.opctx_external_authn();
let user = nexus
.login_local(&opctx, &path_params.silo_name, credentials)
.await?;
let silo_lookup = nexus.silo_lookup(&opctx, &silo)?;
let user = nexus.login_local(&opctx, &silo_lookup, credentials).await?;
login_finish(&opctx, apictx, user, None).await
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
Expand Down
Loading