From a7d5bc11f876871c44e9f9a6186ac6f39dba4f9c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 27 Feb 2023 12:19:42 -0500 Subject: [PATCH 01/10] WIP: Silos, identity providers, users --- nexus/src/app/silo.rs | 99 ++--- nexus/src/db/datastore/identity_provider.rs | 28 +- nexus/src/external_api/http_entrypoints.rs | 386 +++++++++++++++++--- nexus/tests/integration_tests/endpoints.rs | 28 +- nexus/tests/integration_tests/saml.rs | 7 +- nexus/tests/output/nexus_tags.txt | 7 + nexus/types/src/external_api/params.rs | 33 ++ openapi/nexus.json | 327 ++++++++++++++++- 8 files changed, 772 insertions(+), 143 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 10bf77721e9..966c8d45a48 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -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> { + 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) + } + } + } pub async fn silo_create( &self, @@ -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 { - 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 { - 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 } @@ -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> { let (.., authz_silo) = silo_lookup.lookup_for(authz::Action::ReadPolicy).await?; @@ -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, ) -> UpdateResult> { let (.., authz_silo) = @@ -640,16 +632,22 @@ impl super::Nexus { // identity providers + pub fn identity_provider_lookup<'a>( + &'a self, + opctx: &'a OpContext, + idp_id: Uuid, + ) -> LookupResult> { + LookupPath::new(opctx, self).identity_provider_id(idp_id) + } + pub async fn identity_provider_list( &self, opctx: &OpContext, - silo_name: &Name, - pagparams: &DataPageParams<'_, Name>, + silo_lookup: &lookup::Silo<'_>, + pagparams: &PaginatedBy<'_>, ) -> ListResultVec { - let (authz_silo, ..) = LookupPath::new(opctx, &self.db_datastore) - .silo_name(silo_name) - .fetch() - .await?; + let (.., authz_silo) = + silo_lookup.lookup_for(authz::Action::ListChildren).await?; let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo); self.db_datastore .identity_provider_list(opctx, &authz_idp_list, pagparams) @@ -661,13 +659,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 { - 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_for(authz::Action::CreateChild).await?; let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo); if db_silo.user_provision_type != UserProvisionType::Jit { @@ -791,19 +787,4 @@ impl super::Nexus { .saml_identity_provider_create(opctx, &authz_idp_list, provider) .await } - - pub async fn saml_identity_provider_fetch( - &self, - opctx: &OpContext, - silo_name: &Name, - provider_name: &Name, - ) -> LookupResult { - 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) - } } diff --git a/nexus/src/db/datastore/identity_provider.rs b/nexus/src/db/datastore/identity_provider.rs index ef7a3800090..4d725d1cf48 100644 --- a/nexus/src/db/datastore/identity_provider.rs +++ b/nexus/src/db/datastore/identity_provider.rs @@ -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 { 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::(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::(self.pool_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server)) } pub async fn saml_identity_provider_create( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 31a3d2db6ef..6d04691912b 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -321,13 +321,28 @@ pub fn external_api() -> NexusApiDescription { api.register(silo_policy_view)?; api.register(silo_policy_update)?; + api.register(silo_list_v1)?; + api.register(silo_create_v1)?; + api.register(silo_view_v1)?; + api.register(silo_delete_v1)?; + api.register(silo_identity_provider_list_v1)?; + api.register(silo_policy_view_v1)?; + api.register(silo_policy_update_v1)?; + api.register(saml_identity_provider_create)?; api.register(saml_identity_provider_view)?; + api.register(saml_identity_provider_create_v1)?; + api.register(saml_identity_provider_view_v1)?; + api.register(local_idp_user_create)?; api.register(local_idp_user_delete)?; api.register(local_idp_user_set_password)?; + api.register(local_idp_user_create_v1)?; + api.register(local_idp_user_delete_v1)?; + api.register(local_idp_user_set_password_v1)?; + api.register(certificate_list)?; api.register(certificate_create)?; api.register(certificate_view)?; @@ -544,13 +559,15 @@ pub async fn policy_view_v1( let handler = async { let nexus = &apictx.nexus; let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_silo = opctx + let silo: NameOrId = opctx .authn .silo_required() - .internal_context("loading current silo")?; + .internal_context("loading current silo")? + .id() + .into(); - let lookup = nexus.db_lookup(&opctx).silo_id(authz_silo.id()); - let policy = nexus.silo_fetch_policy(&opctx, lookup).await?; + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let policy = nexus.silo_fetch_policy(&opctx, &silo_lookup).await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -571,13 +588,15 @@ pub async fn policy_view( let nexus = &apictx.nexus; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_silo = opctx + let silo: NameOrId = opctx .authn .silo_required() - .internal_context("loading current silo")?; + .internal_context("loading current silo")? + .id() + .into(); - let lookup = nexus.db_lookup(&opctx).silo_id(authz_silo.id()); - let policy = nexus.silo_fetch_policy(&opctx, lookup).await?; + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let policy = nexus.silo_fetch_policy(&opctx, &silo_lookup).await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -601,13 +620,15 @@ async fn policy_update_v1( // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_silo = opctx + let silo: NameOrId = opctx .authn .silo_required() - .internal_context("loading current silo")?; - let lookup = nexus.db_lookup(&opctx).silo_id(authz_silo.id()); + .internal_context("loading current silo")? + .id() + .into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; let policy = - nexus.silo_update_policy(&opctx, lookup, &new_policy).await?; + nexus.silo_update_policy(&opctx, &silo_lookup, &new_policy).await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -634,18 +655,55 @@ async fn policy_update( // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let authz_silo = opctx + let silo: NameOrId = opctx .authn .silo_required() - .internal_context("loading current silo")?; - let lookup = nexus.db_lookup(&opctx).silo_id(authz_silo.id()); + .internal_context("loading current silo")? + .id() + .into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; let policy = - nexus.silo_update_policy(&opctx, lookup, &new_policy).await?; + nexus.silo_update_policy(&opctx, &silo_lookup, &new_policy).await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// List silos +/// +/// Lists silos that are discoverable based on the current permissions. +#[endpoint { + method = GET, + path = "/v1/system/silos", + tags = ["system"], +}] +async fn silo_list_v1( + rqctx: RequestContext>, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let opctx = OpContext::for_external_api(&rqctx).await?; + let silos = nexus + .silos_list(&opctx, &paginated_by) + .await? + .into_iter() + .map(|p| p.try_into()) + .collect::, Error>>()?; + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + silos, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List silos /// /// Lists silos that are discoverable based on the current permissions. @@ -681,6 +739,27 @@ async fn silo_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Create a silo +#[endpoint { + method = POST, + path = "/v1/system/silos", + tags = ["system"], +}] +async fn silo_create_v1( + rqctx: RequestContext>, + new_silo_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let silo = + nexus.silo_create(&opctx, new_silo_params.into_inner()).await?; + Ok(HttpResponseCreated(silo.try_into()?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a silo #[endpoint { method = POST, @@ -702,6 +781,30 @@ async fn silo_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch a silo +/// +/// Fetch a silo by name. +#[endpoint { + method = GET, + path = "/v1/system/silos/{silo}", + tags = ["system"], +}] +async fn silo_view_v1( + rqctx: RequestContext>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &path.silo)?; + let (.., silo) = silo_lookup.fetch().await?; + Ok(HttpResponseOk(silo.try_into()?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Silo requests #[derive(Deserialize, JsonSchema)] struct SiloPathParam { @@ -712,44 +815,50 @@ struct SiloPathParam { /// Fetch a silo /// /// Fetch a silo by name. +/// Use `GET /v1/system/silos/{silo}` instead. #[endpoint { method = GET, path = "/system/silos/{silo_name}", tags = ["system"], + deprecated = true, }] async fn silo_view( rqctx: RequestContext>, path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let silo_name = &path.silo_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let silo = nexus.silo_fetch(&opctx, &silo_name).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let (.., silo) = silo_lookup.fetch().await?; Ok(HttpResponseOk(silo.try_into()?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a silo by id +/// Use `GET /v1/system/silos/{id}` instead. #[endpoint { method = GET, path = "/system/by-id/silos/{id}", - tags = ["system"] + tags = ["system"], + deprecated = true }] async fn silo_view_by_id( rqctx: RequestContext>, path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let id = &path.id; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let silo = nexus.silo_fetch_by_id(&opctx, id).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.id.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let (.., silo) = silo_lookup.fetch().await?; Ok(HttpResponseOk(silo.try_into()?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -758,46 +867,124 @@ async fn silo_view_by_id( /// Delete a silo /// /// Delete a silo by name. +#[endpoint { + method = DELETE, + path = "/v1/system/silos/{silo}", + tags = ["system"], +}] +async fn silo_delete_v1( + rqctx: RequestContext>, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, ¶ms.silo)?; + nexus.silo_delete(&opctx, &silo_lookup).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a silo +/// +/// Delete a silo by name. +/// Use `DELETE /v1/system/silos/{silo}` instead. #[endpoint { method = DELETE, path = "/system/silos/{silo_name}", tags = ["system"], + deprecated = true, }] async fn silo_delete( rqctx: RequestContext>, path_params: Path, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let params = path_params.into_inner(); - let silo_name = ¶ms.silo_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus.silo_delete(&opctx, &silo_name).await?; + let nexus = &apictx.nexus; + let params = path_params.into_inner(); + let silo = params.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + nexus.silo_delete(&opctx, &silo_lookup).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } /// Fetch a silo's IAM policy +#[endpoint { + method = GET, + path = "/v1/system/silos/{silo}/policy", + tags = ["system"], +}] +async fn silo_policy_view_v1( + rqctx: RequestContext>, + path_params: Path, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &path.silo)?; + let policy = nexus.silo_fetch_policy(&opctx, &silo_lookup).await?; + Ok(HttpResponseOk(policy)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Fetch a silo's IAM policy +/// Use `GET /v1/system/silos/{silo}/policy` instead. #[endpoint { method = GET, path = "/system/silos/{silo_name}/policy", tags = ["system"], + deprecated = true }] async fn silo_policy_view( rqctx: RequestContext>, path_params: Path, ) -> Result>, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let silo_name = &path.silo_name; + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = &path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let policy = nexus.silo_fetch_policy(&opctx, &silo_lookup).await?; + Ok(HttpResponseOk(policy)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} +/// Update a silo's IAM policy +#[endpoint { + method = PUT, + path = "/v1/system/silos/{silo}/policy", + tags = ["system"], +}] +async fn silo_policy_update_v1( + rqctx: RequestContext>, + path_params: Path, + new_policy: TypedBody>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); let handler = async { + let new_policy = new_policy.into_inner(); + let nasgns = new_policy.role_assignments.len(); + // This should have been validated during parsing. + bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let lookup = nexus.db_lookup(&opctx).silo_name(silo_name); - let policy = nexus.silo_fetch_policy(&opctx, lookup).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &path.silo)?; + let policy = + nexus.silo_update_policy(&opctx, &silo_lookup, &new_policy).await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -815,19 +1002,18 @@ async fn silo_policy_update( new_policy: TypedBody>, ) -> Result>, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let new_policy = new_policy.into_inner(); - let silo_name = &path.silo_name; - let handler = async { + let new_policy = new_policy.into_inner(); let nasgns = new_policy.role_assignments.len(); // This should have been validated during parsing. bail_unless!(nasgns <= shared::MAX_ROLE_ASSIGNMENTS_PER_RESOURCE); let opctx = OpContext::for_external_api(&rqctx).await?; - let lookup = nexus.db_lookup(&opctx).silo_name(silo_name); + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo_name = &path.silo_name; + let silo_lookup = nexus.db_lookup(&opctx).silo_name(silo_name); let policy = - nexus.silo_update_policy(&opctx, lookup, &new_policy).await?; + nexus.silo_update_policy(&opctx, &silo_lookup, &new_policy).await?; Ok(HttpResponseOk(policy)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -906,11 +1092,48 @@ async fn silo_user_view( // Silo identity providers +/// List a silo's IDPs_name +#[endpoint { + method = GET, + path = "/v1/system/identity-providers", + tags = ["system"], +}] +async fn silo_identity_provider_list_v1( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let silo_lookup = + nexus.silo_lookup(&opctx, &scan_params.selector.silo)?; + let identity_providers = nexus + .identity_provider_list(&opctx, &silo_lookup, &paginated_by) + .await? + .into_iter() + .map(|x| x.into()) + .collect(); + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + identity_providers, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// List a silo's IDPs +/// Use `/v1/system/silos/{silo}/identity-providers` instead. #[endpoint { method = GET, path = "/system/silos/{silo_name}/identity-providers", tags = ["system"], + deprecated = true }] async fn silo_identity_provider_list( rqctx: RequestContext>, @@ -918,16 +1141,19 @@ async fn silo_identity_provider_list( query_params: Query, ) -> Result>, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path = path_params.into_inner(); - let silo_name = &path.silo_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); let query = query_params.into_inner(); - let pagination_params = data_page_params_for(&rqctx, &query)? - .map_name(|n| Name::ref_cast(n)); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; let identity_providers = nexus - .identity_provider_list(&opctx, &silo_name, &pagination_params) + .identity_provider_list( + &opctx, + &silo_lookup, + &PaginatedBy::Name(data_page_params_for(&rqctx, &query)?), + ) .await? .into_iter() .map(|x| x.into()) @@ -943,6 +1169,35 @@ async fn silo_identity_provider_list( // Silo SAML identity providers +/// Create a SAML IDP +#[endpoint { + method = POST, + path = "/v1/system/identity-providers/saml", + tags = ["system"], +}] +async fn saml_identity_provider_create_v1( + rqctx: RequestContext>, + query_params: Query, + new_provider: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &query.silo)?; + let provider = nexus + .saml_identity_provider_create( + &opctx, + &silo_lookup, + new_provider.into_inner(), + ) + .await?; + Ok(HttpResponseCreated(provider.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a SAML IDP #[endpoint { method = POST, @@ -971,6 +1226,39 @@ async fn saml_identity_provider_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Fetch a SAML IDP +#[endpoint { + method = GET, + path = "/v1/system/identity-providers/saml/{provider_name}", + tags = ["system"], +}] +async fn saml_identity_provider_view_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let silo_lookup = nexus + .silo_lookup(&opctx, &query.silo)? + .saml_identity_provider_name(&path.name); + let provider = nexus + .saml_identity_provider_fetch( + &opctx, + &path_params.silo_name, + &path_params.provider_name, + ) + .await?; + + Ok(HttpResponseOk(provider.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Silo SAML identity provider requests #[derive(Deserialize, JsonSchema)] struct SiloSamlPathParam { @@ -981,10 +1269,12 @@ struct SiloSamlPathParam { } /// Fetch a SAML IDP +/// Use `GET /v1/system/identity-providers/saml/{provider_name}` instead #[endpoint { method = GET, path = "/system/silos/{silo_name}/identity-providers/saml/{provider_name}", tags = ["system"], + deprecated = true, }] async fn saml_identity_provider_view( rqctx: RequestContext>, @@ -1152,12 +1442,12 @@ async fn organization_list( ) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; let nexus = &apictx.nexus; let query = query_params.into_inner(); let pag_params = data_page_params_for(&rqctx, &query)?; let scan_params = ScanByNameOrId::from_query(&query)?; let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; - let opctx = OpContext::for_external_api(&rqctx).await?; let organizations = nexus .organizations_list(&opctx, &paginated_by) .await? diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index e266a4bb976..f825b401498 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -53,9 +53,9 @@ lazy_static! { // Silo used for testing pub static ref DEMO_SILO_NAME: Name = "demo-silo".parse().unwrap(); pub static ref DEMO_SILO_URL: String = - format!("/system/silos/{}", *DEMO_SILO_NAME); + format!("/v1/system/silos/{}", *DEMO_SILO_NAME); pub static ref DEMO_SILO_POLICY_URL: String = - format!("/system/silos/{}/policy", *DEMO_SILO_NAME); + format!("/v1/system/silos/{}/policy", *DEMO_SILO_NAME); pub static ref DEMO_SILO_CREATE: params::SiloCreate = params::SiloCreate { identity: IdentityMetadataCreateParams { @@ -68,23 +68,23 @@ lazy_static! { }; // Use the default Silo for testing the local IdP pub static ref DEMO_SILO_USERS_CREATE_URL: String = format!( - "/system/silos/{}/identity-providers/local/users", + "/v1/system/silos/{}/identity-providers/local/users", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USERS_LIST_URL: String = format!( - "/system/silos/{}/users/all", + "/v1/system/silos/{}/users/all", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_GET_URL: String = format!( - "/system/silos/{}/users/id/{{id}}", + "/v1/system/silos/{}/users/id/{{id}}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_DELETE_URL: String = format!( - "/system/silos/{}/identity-providers/local/users/{{id}}", + "/v1/system/silos/{}/identity-providers/local/users/{{id}}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_SET_PASSWORD_URL: String = format!( - "/system/silos/{}/identity-providers/local/users/{{id}}/set-password", + "/v1/system/silos/{}/identity-providers/local/users/{{id}}/set-password", DEFAULT_SILO.identity().name, ); @@ -427,8 +427,8 @@ lazy_static! { lazy_static! { // Identity providers - pub static ref IDENTITY_PROVIDERS_URL: String = format!("/system/silos/demo-silo/identity-providers"); - pub static ref SAML_IDENTITY_PROVIDERS_URL: String = format!("/system/silos/demo-silo/identity-providers/saml"); + pub static ref IDENTITY_PROVIDERS_URL: String = format!("/v1/system/silos/demo-silo/identity-providers"); + pub static ref SAML_IDENTITY_PROVIDERS_URL: String = format!("/v1/system/silos/demo-silo/identity-providers/saml"); pub static ref DEMO_SAML_IDENTITY_PROVIDER_NAME: Name = "demo-saml-provider".parse().unwrap(); pub static ref SPECIFIC_SAML_IDENTITY_PROVIDER_URL: String = format!("{}/{}", *SAML_IDENTITY_PROVIDERS_URL, *DEMO_SAML_IDENTITY_PROVIDER_NAME); @@ -733,7 +733,7 @@ lazy_static! { /* Silos */ VerifyEndpoint { - url: "/system/silos", + url: "/v1/system/silos", visibility: Visibility::Public, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -743,14 +743,6 @@ lazy_static! { ) ], }, - VerifyEndpoint { - url: "/system/by-id/silos/{id}", - visibility: Visibility::Protected, - unprivileged_access: UnprivilegedAccess::None, - allowed_methods: vec![ - AllowedMethod::Get, - ], - }, VerifyEndpoint { url: &DEMO_SILO_URL, visibility: Visibility::Protected, diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index 5206a69cc95..1a10dce9d4c 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -89,9 +89,8 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { // Assert external authenticator opctx can read it let nexus = &cptestctx.server.apictx().nexus; - - let _retrieved_silo_nexus = nexus - .silo_fetch( + let (.., _retrieved_silo_nexus) = nexus + .silo_lookup( &nexus.opctx_external_authn(), &omicron_common::api::external::Name::try_from( SILO_NAME.to_string(), @@ -99,6 +98,8 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { .unwrap() .into(), ) + .unwrap() + .fetch() .await .unwrap(); diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 580e5779d0b..fa5691f1222 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -196,15 +196,22 @@ saga_view_v1 /v1/system/sagas/{saga_id} saml_identity_provider_create /system/silos/{silo_name}/identity-providers/saml saml_identity_provider_view /system/silos/{silo_name}/identity-providers/saml/{provider_name} silo_create /system/silos +silo_create_v1 /v1/system/silos silo_delete /system/silos/{silo_name} +silo_delete_v1 /v1/system/silos/{silo} silo_identity_provider_list /system/silos/{silo_name}/identity-providers +silo_identity_provider_list_v1 /v1/system/silos/{silo}/identity-providers silo_list /system/silos +silo_list_v1 /v1/system/silos silo_policy_update /system/silos/{silo_name}/policy +silo_policy_update_v1 /v1/system/silos/{silo}/policy silo_policy_view /system/silos/{silo_name}/policy +silo_policy_view_v1 /v1/system/silos/{silo}/policy silo_user_view /system/silos/{silo_name}/users/id/{user_id} silo_users_list /system/silos/{silo_name}/users/all silo_view /system/silos/{silo_name} silo_view_by_id /system/by-id/silos/{id} +silo_view_v1 /v1/system/silos/{silo} sled_list /system/hardware/sleds sled_list_v1 /v1/system/hardware/sleds sled_physical_disk_list /system/hardware/sleds/{sled_id}/disks diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index e7b9dcd491c..ef6d80734a1 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -71,6 +71,39 @@ pub struct SnapshotPath { pub snapshot: NameOrId, } +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct SiloPath { + pub silo: NameOrId, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct SiloSelector { + pub silo: NameOrId, +} + +impl From for SiloSelector { + fn from(name: Name) -> Self { + SiloSelector { silo: name.into() } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct IdentityProviderSelector { + #[serde(flatten)] + pub silo_selector: Option, + pub identity_provider: NameOrId, +} + +// TODO-v1: delete this post migration +impl IdentityProviderSelector { + pub fn new(silo: Option, identity_provider: NameOrId) -> Self { + IdentityProviderSelector { + silo_selector: silo.map(|s| SiloSelector { silo: s }), + identity_provider, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct OrganizationSelector { pub organization: NameOrId, diff --git a/openapi/nexus.json b/openapi/nexus.json index 9a328823cc6..b6acf00b785 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -5495,6 +5495,7 @@ "system" ], "summary": "Fetch a silo by id", + "description": "Use `GET /v1/system/silos/{id}` instead.", "operationId": "silo_view_by_id", "parameters": [ { @@ -5524,7 +5525,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/certificates": { @@ -7023,7 +7025,7 @@ "system" ], "summary": "Fetch a silo", - "description": "Fetch a silo by name.", + "description": "Fetch a silo by name. Use `GET /v1/system/silos/{silo}` instead.", "operationId": "silo_view", "parameters": [ { @@ -7053,14 +7055,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "delete": { "tags": [ "system" ], "summary": "Delete a silo", - "description": "Delete a silo by name.", + "description": "Delete a silo by name. Use `DELETE /v1/system/silos/{silo}` instead.", "operationId": "silo_delete", "parameters": [ { @@ -7083,7 +7086,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers": { @@ -7092,6 +7096,7 @@ "system" ], "summary": "List a silo's IDPs", + "description": "Use `/v1/system/silos/{silo}/identity-providers` instead.", "operationId": "silo_identity_provider_list", "parameters": [ { @@ -7149,6 +7154,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true } }, @@ -7395,6 +7401,7 @@ "system" ], "summary": "Fetch a silo's IAM policy", + "description": "Use `GET /v1/system/silos/{silo}/policy` instead.", "operationId": "silo_policy_view", "parameters": [ { @@ -7424,7 +7431,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ @@ -10608,6 +10616,313 @@ } } }, + "/v1/system/silos": { + "get": { + "tags": [ + "system" + ], + "summary": "List silos", + "description": "Lists silos that are discoverable based on the current permissions.", + "operationId": "silo_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + }, + "post": { + "tags": [ + "system" + ], + "summary": "Create a silo", + "operationId": "silo_create_v1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}": { + "get": { + "tags": [ + "system" + ], + "summary": "Fetch a silo", + "description": "Fetch a silo by name.", + "operationId": "silo_view_v1", + "parameters": [ + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Silo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system" + ], + "summary": "Delete a silo", + "description": "Delete a silo by name.", + "operationId": "silo_delete_v1", + "parameters": [ + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/silos/{silo}/identity-providers": { + "get": { + "tags": [ + "system" + ], + "summary": "List a silo's IDPs_name", + "operationId": "silo_identity_provider_list_v1", + "parameters": [ + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/system/silos/{silo}/policy": { + "get": { + "tags": [ + "system" + ], + "summary": "Fetch a silo's IAM policy", + "operationId": "silo_policy_view_v1", + "parameters": [ + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system" + ], + "summary": "Update a silo's IAM policy", + "operationId": "silo_policy_update_v1", + "parameters": [ + { + "in": "path", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/update/components": { "get": { "tags": [ From a0b960eef8012c083f4644eb212e94ef251ed102 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 27 Feb 2023 13:45:50 -0500 Subject: [PATCH 02/10] WIP: Update some local idp endpoints --- nexus/src/app/silo.rs | 40 ++++++-- nexus/src/external_api/http_entrypoints.rs | 105 ++++++++++++++++----- nexus/types/src/external_api/params.rs | 20 ++-- 3 files changed, 127 insertions(+), 38 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 966c8d45a48..72010a64ea9 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -201,12 +201,9 @@ impl super::Nexus { 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, @@ -221,11 +218,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 { let (authz_silo, db_silo) = - self.local_idp_fetch_silo(opctx, silo_name).await?; + self.local_idp_fetch_silo(opctx, 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 @@ -632,12 +629,35 @@ impl super::Nexus { // identity providers - pub fn identity_provider_lookup<'a>( + pub fn saml_identity_provider_lookup<'a>( &'a self, opctx: &'a OpContext, - idp_id: Uuid, + saml_identity_provider_selector: &'a params::SamlIdentityProviderSelector, ) -> LookupResult> { - LookupPath::new(opctx, self).identity_provider_id(idp_id) + 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); + 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")) + } } pub async fn identity_provider_list( diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 6d04691912b..bc3a3fd0934 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1229,12 +1229,12 @@ async fn saml_identity_provider_create( /// Fetch a SAML IDP #[endpoint { method = GET, - path = "/v1/system/identity-providers/saml/{provider_name}", + path = "/v1/system/identity-providers/saml/{provider}", tags = ["system"], }] async fn saml_identity_provider_view_v1( rqctx: RequestContext>, - path_params: Path, + path_params: Path, query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); @@ -1243,17 +1243,18 @@ async fn saml_identity_provider_view_v1( let nexus = &apictx.nexus; let path = path_params.into_inner(); let query = query_params.into_inner(); - let silo_lookup = nexus - .silo_lookup(&opctx, &query.silo)? - .saml_identity_provider_name(&path.name); - let provider = nexus - .saml_identity_provider_fetch( + let saml_identity_provider_selector = + params::SamlIdentityProviderSelector::new( + Some(query.silo), + path.provider, + ); + let (.., provider) = nexus + .saml_identity_provider_lookup( &opctx, - &path_params.silo_name, - &path_params.provider_name, - ) + &saml_identity_provider_selector, + )? + .fetch() .await?; - Ok(HttpResponseOk(provider.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -1281,20 +1282,18 @@ async fn saml_identity_provider_view( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - - let path_params = path_params.into_inner(); - let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let provider = nexus - .saml_identity_provider_fetch( - &opctx, - &path_params.silo_name, - &path_params.provider_name, - ) + let nexus = &apictx.nexus; + let path_params = path_params.into_inner(); + let provider_selector = params::SamlIdentityProviderSelector::new( + Some(path_params.silo_name.into()), + path_params.provider_name.into(), + ); + let (.., provider) = nexus + .saml_identity_provider_lookup(&opctx, &provider_selector)? + .fetch() .await?; - Ok(HttpResponseOk(provider.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -1304,6 +1303,39 @@ async fn saml_identity_provider_view( // "Local" Identity Provider +/// Create a user +/// +/// Users can only be created in Silos with `provision_type` == `Fixed`. +/// Otherwise, Silo users are just-in-time (JIT) provisioned when a user first +/// logs in using an external Identity Provider. +#[endpoint { + method = POST, + path = "/v1/system/identity-providers/local/users", + tags = ["system"], +}] +async fn local_idp_user_create_v1( + rqctx: RequestContext>, + query_params: Query, + new_user_params: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &query.silo)?; + let user = nexus + .local_idp_create_user( + &opctx, + &silo_lookup, + new_user_params.into_inner(), + ) + .await?; + Ok(HttpResponseCreated(user.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Create a user /// /// Users can only be created in Silos with `provision_type` == `Fixed`. @@ -1337,10 +1369,39 @@ async fn local_idp_user_create( } /// Delete a user +#[endpoint { + method = DELETE, + path = "/v1/system/identity-providers/local/users/{user_id}", + tags = ["system"], +}] +async fn local_idp_user_delete_v1( + rqctx: RequestContext>, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path_params = path_params.into_inner(); + nexus + .local_idp_delete_user( + &opctx, + &path_params.silo_name, + path_params.user_id, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete a user +/// Use `DELETE /v1/system/identity-providers/local/users/{user_id}` instead #[endpoint { method = DELETE, path = "/system/silos/{silo_name}/identity-providers/local/users/{user_id}", tags = ["system"], + deprecated = true }] async fn local_idp_user_delete( rqctx: RequestContext>, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index ef6d80734a1..14a372cc761 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -76,6 +76,11 @@ pub struct SiloPath { pub silo: NameOrId, } +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct ProviderPath { + pub provider: NameOrId, +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct SiloSelector { pub silo: NameOrId, @@ -88,18 +93,21 @@ impl From for SiloSelector { } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub struct IdentityProviderSelector { +pub struct SamlIdentityProviderSelector { #[serde(flatten)] pub silo_selector: Option, - pub identity_provider: NameOrId, + pub saml_identity_provider: NameOrId, } // TODO-v1: delete this post migration -impl IdentityProviderSelector { - pub fn new(silo: Option, identity_provider: NameOrId) -> Self { - IdentityProviderSelector { +impl SamlIdentityProviderSelector { + pub fn new( + silo: Option, + saml_identity_provider: NameOrId, + ) -> Self { + SamlIdentityProviderSelector { silo_selector: silo.map(|s| SiloSelector { silo: s }), - identity_provider, + saml_identity_provider, } } } From 2dd33e8ebb4dc80d17baf2279f12a2e97c8404c4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 27 Feb 2023 13:53:46 -0500 Subject: [PATCH 03/10] Stub out remaining endpoints --- nexus/src/app/silo.rs | 2 +- nexus/src/external_api/http_entrypoints.rs | 49 +++++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 72010a64ea9..b28e83b5273 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -633,7 +633,7 @@ impl super::Nexus { &'a self, opctx: &'a OpContext, saml_identity_provider_selector: &'a params::SamlIdentityProviderSelector, - ) -> LookupResult> { + ) -> LookupResult> { match saml_identity_provider_selector { params::SamlIdentityProviderSelector { saml_identity_provider: NameOrId::Id(id), diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index bc3a3fd0934..5e515fa5d5f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1210,14 +1210,16 @@ async fn saml_identity_provider_create( new_provider: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; let provider = nexus .saml_identity_provider_create( &opctx, - &path_params.into_inner().silo_name, + &silo_lookup, new_provider.into_inner(), ) .await?; @@ -1352,14 +1354,15 @@ async fn local_idp_user_create( new_user_params: TypedBody, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let silo_name = path_params.into_inner().silo_name; let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let silo = path_params.into_inner().silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; let user = nexus .local_idp_create_user( &opctx, - &silo_name, + &silo_lookup, new_user_params.into_inner(), ) .await?; @@ -1428,10 +1431,44 @@ async fn local_idp_user_delete( /// /// Passwords can only be updated for users in Silos with identity mode /// `LocalOnly`. +#[endpoint { + method = POST, + path = "/v1/system/identity-providers/local/users/{user_id}/set-password", + tags = ["system"], +}] +async fn local_idp_user_set_password_v1( + rqctx: RequestContext>, + path_params: Path, + update: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let path_params = path_params.into_inner(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + nexus + .local_idp_user_set_password( + &opctx, + &path_params.silo_name, + path_params.user_id, + update.into_inner(), + ) + .await?; + Ok(HttpResponseUpdatedNoContent()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Set or invalidate a user's password +/// +/// Passwords can only be updated for users in Silos with identity mode +/// `LocalOnly`. +/// Use `POST /v1/system/identity-providers/local/users/{user_id}/set-password` instead #[endpoint { method = POST, path = "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password", tags = ["system"], + deprecated = true }] async fn local_idp_user_set_password( rqctx: RequestContext>, From 3cbbf9f2b2bf39a34d3cd89a6af599059df24574 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 28 Feb 2023 16:42:42 -0500 Subject: [PATCH 04/10] Fixup endpoints --- nexus/src/app/silo.rs | 21 +++++----- nexus/src/db/lookup.rs | 11 ++++++ nexus/src/external_api/console_api.rs | 8 ++-- nexus/src/external_api/http_entrypoints.rs | 46 ++++++++++------------ 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index b28e83b5273..f2e07251131 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -200,7 +200,6 @@ impl super::Nexus { /// provider. async fn local_idp_fetch_silo( &self, - opctx: &OpContext, silo_lookup: &lookup::Silo<'_>, ) -> LookupResult<(authz::Silo, db::model::Silo)> { let (authz_silo, db_silo) = silo_lookup.fetch().await?; @@ -222,7 +221,7 @@ impl super::Nexus { new_user_params: params::UserCreate, ) -> CreateResult { let (authz_silo, db_silo) = - self.local_idp_fetch_silo(opctx, silo_lookup).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 @@ -256,11 +255,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, @@ -383,12 +382,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, @@ -495,11 +494,10 @@ impl super::Nexus { pub async fn login_local( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, credentials: params::UsernamePasswordCredentials, ) -> Result, 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 @@ -639,6 +637,7 @@ impl super::Nexus { 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) @@ -649,7 +648,7 @@ impl super::Nexus { } => { let saml_provider = self .silo_lookup(opctx, &silo_selector.silo)? - .saml_identity_provider_name(name); + .saml_identity_provider_name(Name::ref_cast(name)); Ok(saml_provider) } params::SamlIdentityProviderSelector { diff --git a/nexus/src/db/lookup.rs b/nexus/src/db/lookup.rs index 68ebcb4187b..d96b56548c0 100644 --- a/nexus/src/db/lookup.rs +++ b/nexus/src/db/lookup.rs @@ -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 diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index 6181c10f3b7..af4c583b1c7 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -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 diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 5e515fa5d5f..e2161f68f8c 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1385,14 +1385,10 @@ async fn local_idp_user_delete_v1( let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let nexus = &apictx.nexus; - let path_params = path_params.into_inner(); - nexus - .local_idp_delete_user( - &opctx, - &path_params.silo_name, - path_params.user_id, - ) - .await?; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + nexus.local_idp_delete_user(&opctx, &silo_lookup, path.user_id).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -1411,17 +1407,13 @@ async fn local_idp_user_delete( path_params: Path, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path_params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - nexus - .local_idp_delete_user( - &opctx, - &path_params.silo_name, - path_params.user_id, - ) - .await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + nexus.local_idp_delete_user(&opctx, &silo_lookup, path.user_id).await?; Ok(HttpResponseDeleted()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -1442,15 +1434,17 @@ async fn local_idp_user_set_password_v1( update: TypedBody, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path_params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; nexus .local_idp_user_set_password( &opctx, - &path_params.silo_name, - path_params.user_id, + &silo_lookup, + path.user_id, update.into_inner(), ) .await?; @@ -1476,15 +1470,17 @@ async fn local_idp_user_set_password( update: TypedBody, ) -> Result { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path_params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; nexus .local_idp_user_set_password( &opctx, - &path_params.silo_name, - path_params.user_id, + &silo_lookup, + path.user_id, update.into_inner(), ) .await?; From 0b5f8b05de01cd1290dea243e74ba1a4c0bcffa4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 28 Feb 2023 16:58:51 -0500 Subject: [PATCH 05/10] Add new saml endpoints --- nexus/src/external_api/http_entrypoints.rs | 21 +- nexus/tests/output/nexus_tags.txt | 7 +- openapi/nexus.json | 373 +++++++++++++++++---- 3 files changed, 325 insertions(+), 76 deletions(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e2161f68f8c..ca40d0c205a 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1371,6 +1371,13 @@ async fn local_idp_user_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Path parameters for Silo User requests +#[derive(Deserialize, JsonSchema)] +struct UserParam { + /// The user's internal id + user_id: Uuid, +} + /// Delete a user #[endpoint { method = DELETE, @@ -1379,15 +1386,16 @@ async fn local_idp_user_create( }] async fn local_idp_user_delete_v1( rqctx: RequestContext>, - path_params: Path, + path_params: Path, + query_params: Query, ) -> Result { let apictx = rqctx.context(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; let nexus = &apictx.nexus; let path = path_params.into_inner(); - let silo = path.silo_name.into(); - let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let query = query_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &query.silo)?; nexus.local_idp_delete_user(&opctx, &silo_lookup, path.user_id).await?; Ok(HttpResponseDeleted()) }; @@ -1430,7 +1438,8 @@ async fn local_idp_user_delete( }] async fn local_idp_user_set_password_v1( rqctx: RequestContext>, - path_params: Path, + path_params: Path, + query_params: Query, update: TypedBody, ) -> Result { let apictx = rqctx.context(); @@ -1438,8 +1447,8 @@ async fn local_idp_user_set_password_v1( let opctx = OpContext::for_external_api(&rqctx).await?; let nexus = &apictx.nexus; let path = path_params.into_inner(); - let silo = path.silo_name.into(); - let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let query = query_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &query.silo)?; nexus .local_idp_user_set_password( &opctx, diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index fa5691f1222..adfb62137be 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -181,8 +181,11 @@ ip_pool_update /system/ip-pools/{pool_name} ip_pool_view /system/ip-pools/{pool_name} ip_pool_view_by_id /system/by-id/ip-pools/{id} local_idp_user_create /system/silos/{silo_name}/identity-providers/local/users +local_idp_user_create_v1 /v1/system/identity-providers/local/users local_idp_user_delete /system/silos/{silo_name}/identity-providers/local/users/{user_id} +local_idp_user_delete_v1 /v1/system/identity-providers/local/users/{user_id} local_idp_user_set_password /system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password +local_idp_user_set_password_v1 /v1/system/identity-providers/local/users/{user_id}/set-password physical_disk_list /system/hardware/disks physical_disk_list_v1 /v1/system/hardware/disks rack_list /system/hardware/racks @@ -194,13 +197,15 @@ saga_list_v1 /v1/system/sagas saga_view /system/sagas/{saga_id} saga_view_v1 /v1/system/sagas/{saga_id} saml_identity_provider_create /system/silos/{silo_name}/identity-providers/saml +saml_identity_provider_create_v1 /v1/system/identity-providers/saml saml_identity_provider_view /system/silos/{silo_name}/identity-providers/saml/{provider_name} +saml_identity_provider_view_v1 /v1/system/identity-providers/saml/{provider} silo_create /system/silos silo_create_v1 /v1/system/silos silo_delete /system/silos/{silo_name} silo_delete_v1 /v1/system/silos/{silo} silo_identity_provider_list /system/silos/{silo_name}/identity-providers -silo_identity_provider_list_v1 /v1/system/silos/{silo}/identity-providers +silo_identity_provider_list_v1 /v1/system/identity-providers silo_list /system/silos silo_list_v1 /v1/system/silos silo_policy_update /system/silos/{silo_name}/policy diff --git a/openapi/nexus.json b/openapi/nexus.json index b6acf00b785..d63861d7422 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7213,6 +7213,7 @@ "system" ], "summary": "Delete a user", + "description": "Use `DELETE /v1/system/identity-providers/local/users/{user_id}` instead", "operationId": "local_idp_user_delete", "parameters": [ { @@ -7245,7 +7246,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password": { @@ -7254,7 +7256,7 @@ "system" ], "summary": "Set or invalidate a user's password", - "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", + "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`. Use `POST /v1/system/identity-providers/local/users/{user_id}/set-password` instead", "operationId": "local_idp_user_set_password", "parameters": [ { @@ -7297,7 +7299,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers/saml": { @@ -7354,6 +7357,7 @@ "system" ], "summary": "Fetch a SAML IDP", + "description": "Use `GET /v1/system/identity-providers/saml/{provider_name}` instead", "operationId": "saml_identity_provider_view", "parameters": [ { @@ -7392,7 +7396,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/policy": { @@ -10459,6 +10464,301 @@ "x-dropshot-pagination": true } }, + "/v1/system/identity-providers": { + "get": { + "tags": [ + "system" + ], + "summary": "List a silo's IDPs_name", + "operationId": "silo_identity_provider_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/system/identity-providers/local/users": { + "post": { + "tags": [ + "system" + ], + "summary": "Create a user", + "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", + "operationId": "local_idp_user_create_v1", + "parameters": [ + { + "in": "query", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/local/users/{user_id}": { + "delete": { + "tags": [ + "system" + ], + "summary": "Delete a user", + "operationId": "local_idp_user_delete_v1", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/local/users/{user_id}/set-password": { + "post": { + "tags": [ + "system" + ], + "summary": "Set or invalidate a user's password", + "description": "Passwords can only be updated for users in Silos with identity mode `LocalOnly`.", + "operationId": "local_idp_user_set_password_v1", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPassword" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/saml": { + "post": { + "tags": [ + "system" + ], + "summary": "Create a SAML IDP", + "operationId": "saml_identity_provider_create_v1", + "parameters": [ + { + "in": "query", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProviderCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProvider" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/identity-providers/saml/{provider}": { + "get": { + "tags": [ + "system" + ], + "summary": "Fetch a SAML IDP", + "operationId": "saml_identity_provider_view_v1", + "parameters": [ + { + "in": "path", + "name": "provider", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SamlIdentityProvider" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/policy": { "get": { "tags": [ @@ -10776,71 +11076,6 @@ } } }, - "/v1/system/silos/{silo}/identity-providers": { - "get": { - "tags": [ - "system" - ], - "summary": "List a silo's IDPs_name", - "operationId": "silo_identity_provider_list_v1", - "parameters": [ - { - "in": "path", - "name": "silo", - "required": true, - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - }, - { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 - } - }, - { - "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", - "schema": { - "nullable": true, - "type": "string" - } - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IdentityProviderResultsPage" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - }, - "x-dropshot-pagination": true - } - }, "/v1/system/silos/{silo}/policy": { "get": { "tags": [ From cdc3c47f8ecfb669b5215fc20a42d6c575564ac7 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 1 Mar 2023 14:21:31 -0500 Subject: [PATCH 06/10] Add silo user endpoints --- nexus/src/app/silo.rs | 14 +-- nexus/src/external_api/http_entrypoints.rs | 102 ++++++++++++---- nexus/tests/integration_tests/endpoints.rs | 16 +-- nexus/tests/output/nexus_tags.txt | 2 + .../output/uncovered-authz-endpoints.txt | 15 +++ openapi/nexus.json | 113 ++++++++++++++++++ 6 files changed, 224 insertions(+), 38 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index d3d04e6c714..c0165c8fbc2 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -156,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 { - 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) @@ -173,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 { - 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 (_, db_silo_user) = self .silo_user_lookup_by_id( opctx, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index ff2051ab636..ee24448c4a6 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -373,10 +373,13 @@ pub fn external_api() -> NexusApiDescription { api.register(update_deployment_view)?; api.register(user_list)?; - api.register(user_list_v1)?; api.register(silo_users_list)?; api.register(silo_user_view)?; api.register(group_list)?; + + api.register(user_list_v1)?; + api.register(silo_users_list_v1)?; + api.register(silo_user_view_v1)?; api.register(group_list_v1)?; // Console API operations @@ -1024,10 +1027,46 @@ async fn silo_policy_update( // Silo-specific user endpoints /// List users in a silo +#[endpoint { + method = GET, + path = "/v1/system/users", + tags = ["system"], +}] +async fn silo_users_list_v1( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanById::from_query(&query)?; + let silo_lookup = + nexus.silo_lookup(&opctx, &scan_params.selector.silo)?; + let users = nexus + .silo_list_users(&opctx, &silo_lookup, &pag_params) + .await? + .into_iter() + .map(|i| i.into()) + .collect(); + Ok(HttpResponseOk(ScanById::results_page( + &query, + users, + &|_, user: &User| user.id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// List users in a silo +/// Use `GET /v1/system/users` instead. #[endpoint { method = GET, path = "/system/silos/{silo_name}/users/all", tags = ["system"], + deprecated = true }] async fn silo_users_list( rqctx: RequestContext>, @@ -1036,13 +1075,14 @@ async fn silo_users_list( ) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; let nexus = &apictx.nexus; - let silo_name = path_params.into_inner().silo_name; + let silo = path_params.into_inner().silo_name.into(); let query = query_params.into_inner(); let pagparams = data_page_params_for(&rqctx, &query)?; - let opctx = OpContext::for_external_api(&rqctx).await?; + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; let users = nexus - .silo_list_users(&opctx, &silo_name, &pagparams) + .silo_list_users(&opctx, &silo_lookup, &pagparams) .await? .into_iter() .map(|i| i.into()) @@ -1056,6 +1096,38 @@ async fn silo_users_list( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Path parameters for Silo User requests +#[derive(Deserialize, JsonSchema)] +struct UserParam { + /// The user's internal id + user_id: Uuid, +} + +/// Fetch a user +#[endpoint { + method = GET, + path = "/v1/system/users/{user_id}", + tags = ["system"], +}] +async fn silo_user_view_v1( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = OpContext::for_external_api(&rqctx).await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let query = query_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &query.silo)?; + let user = + nexus.silo_user_fetch(&opctx, &silo_lookup, path.user_id).await?; + Ok(HttpResponseOk(user.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Path parameters for Silo User requests #[derive(Deserialize, JsonSchema)] struct UserPathParam { @@ -1076,17 +1148,14 @@ async fn silo_user_view( path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let nexus = &apictx.nexus; - let path_params = path_params.into_inner(); let handler = async { let opctx = OpContext::for_external_api(&rqctx).await?; - let user = nexus - .silo_user_fetch( - &opctx, - &path_params.silo_name, - path_params.user_id, - ) - .await?; + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let silo = path.silo_name.into(); + let silo_lookup = nexus.silo_lookup(&opctx, &silo)?; + let user = + nexus.silo_user_fetch(&opctx, &silo_lookup, path.user_id).await?; Ok(HttpResponseOk(user.into())) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await @@ -1373,13 +1442,6 @@ async fn local_idp_user_create( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } -/// Path parameters for Silo User requests -#[derive(Deserialize, JsonSchema)] -struct UserParam { - /// The user's internal id - user_id: Uuid, -} - /// Delete a user #[endpoint { method = DELETE, diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 3ea0999790a..eb9645f57b7 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -68,23 +68,23 @@ lazy_static! { }; // Use the default Silo for testing the local IdP pub static ref DEMO_SILO_USERS_CREATE_URL: String = format!( - "/v1/system/silos/{}/identity-providers/local/users", + "/v1/system/identity-providers/local/users?silo={}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USERS_LIST_URL: String = format!( - "/v1/system/silos/{}/users/all", + "/v1/system/users?silo={}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_GET_URL: String = format!( - "/v1/system/silos/{}/users/id/{{id}}", + "/v1/system/users/{{id}}?silo={}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_DELETE_URL: String = format!( - "/v1/system/silos/{}/identity-providers/local/users/{{id}}", + "/v1/system/identity-providers/local/users/{{id}}?silo={}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_SET_PASSWORD_URL: String = format!( - "/v1/system/silos/{}/identity-providers/local/users/{{id}}/set-password", + "/v1/system/identity-providers/local/users/{{id}}/set-password?silo={}", DEFAULT_SILO.identity().name, ); @@ -427,11 +427,11 @@ lazy_static! { lazy_static! { // Identity providers - pub static ref IDENTITY_PROVIDERS_URL: String = format!("/v1/system/silos/demo-silo/identity-providers"); - pub static ref SAML_IDENTITY_PROVIDERS_URL: String = format!("/v1/system/silos/demo-silo/identity-providers/saml"); + pub static ref IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers?silo=demo-silo"); + pub static ref SAML_IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers/saml?silo=demo-silo"); pub static ref DEMO_SAML_IDENTITY_PROVIDER_NAME: Name = "demo-saml-provider".parse().unwrap(); - pub static ref SPECIFIC_SAML_IDENTITY_PROVIDER_URL: String = format!("{}/{}", *SAML_IDENTITY_PROVIDERS_URL, *DEMO_SAML_IDENTITY_PROVIDER_NAME); + pub static ref SPECIFIC_SAML_IDENTITY_PROVIDER_URL: String = format!("/v1/system/identity-providers/saml/{}?silo=demo-silo", *DEMO_SAML_IDENTITY_PROVIDER_NAME); pub static ref SAML_IDENTITY_PROVIDER: params::SamlIdentityProviderCreate = params::SamlIdentityProviderCreate { diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 5b8c3845d28..426f048ccb5 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -215,7 +215,9 @@ silo_policy_update_v1 /v1/system/silos/{silo}/policy silo_policy_view /system/silos/{silo_name}/policy silo_policy_view_v1 /v1/system/silos/{silo}/policy silo_user_view /system/silos/{silo_name}/users/id/{user_id} +silo_user_view_v1 /v1/system/users/{user_id} silo_users_list /system/silos/{silo_name}/users/all +silo_users_list_v1 /v1/system/users silo_view /system/silos/{silo_name} silo_view_by_id /system/by-id/silos/{id} silo_view_v1 /v1/system/silos/{silo} diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index ed4449009eb..1fa58f13a5c 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -10,6 +10,8 @@ vpc_router_delete (delete "/organizations/{organization_n vpc_router_route_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes/{route_name}") vpc_subnet_delete (delete "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") certificate_delete (delete "/system/certificates/{certificate}") +silo_delete (delete "/system/silos/{silo_name}") +local_idp_user_delete (delete "/system/silos/{silo_name}/identity-providers/local/users/{user_id}") disk_view_by_id (get "/by-id/disks/{id}") instance_view_by_id (get "/by-id/instances/{id}") instance_network_interface_view_by_id (get "/by-id/network-interfaces/{id}") @@ -45,6 +47,7 @@ vpc_router_route_view (get "/organizations/{organization_n vpc_subnet_list (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") vpc_subnet_view (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") policy_view (get "/policy") +silo_view_by_id (get "/system/by-id/silos/{id}") certificate_list (get "/system/certificates") certificate_view (get "/system/certificates/{certificate}") physical_disk_list (get "/system/hardware/disks") @@ -56,6 +59,13 @@ sled_physical_disk_list (get "/system/hardware/sleds/{sled_i system_policy_view (get "/system/policy") saga_list (get "/system/sagas") saga_view (get "/system/sagas/{saga_id}") +silo_list (get "/system/silos") +silo_view (get "/system/silos/{silo_name}") +silo_identity_provider_list (get "/system/silos/{silo_name}/identity-providers") +saml_identity_provider_view (get "/system/silos/{silo_name}/identity-providers/saml/{provider_name}") +silo_policy_view (get "/system/silos/{silo_name}/policy") +silo_users_list (get "/system/silos/{silo_name}/users/all") +silo_user_view (get "/system/silos/{silo_name}/users/id/{user_id}") user_list (get "/users") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") @@ -81,6 +91,10 @@ vpc_router_create (post "/organizations/{organization_n vpc_router_route_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/routers/{router_name}/routes") vpc_subnet_create (post "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets") certificate_create (post "/system/certificates") +silo_create (post "/system/silos") +local_idp_user_create (post "/system/silos/{silo_name}/identity-providers/local/users") +local_idp_user_set_password (post "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password") +saml_identity_provider_create (post "/system/silos/{silo_name}/identity-providers/saml") organization_update (put "/organizations/{organization_name}") organization_policy_update (put "/organizations/{organization_name}/policy") project_update (put "/organizations/{organization_name}/projects/{project_name}") @@ -93,3 +107,4 @@ vpc_router_route_update (put "/organizations/{organization_n vpc_subnet_update (put "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}") policy_update (put "/policy") system_policy_update (put "/system/policy") +silo_policy_update (put "/system/silos/{silo_name}/policy") diff --git a/openapi/nexus.json b/openapi/nexus.json index 75768ec1a50..ea774c688e3 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -7494,6 +7494,7 @@ "system" ], "summary": "List users in a silo", + "description": "Use `GET /v1/system/users` instead.", "operationId": "silo_users_list", "parameters": [ { @@ -7551,6 +7552,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true } }, @@ -11607,6 +11609,117 @@ } } }, + "/v1/system/users": { + "get": { + "tags": [ + "system" + ], + "summary": "List users in a silo", + "operationId": "silo_users_list_v1", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/system/users/{user_id}": { + "get": { + "tags": [ + "system" + ], + "summary": "Fetch a user", + "operationId": "silo_user_view_v1", + "parameters": [ + { + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/users": { "get": { "tags": [ From b120d3fc8a0a51817f8c329f43f4547e3481f7db Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 1 Mar 2023 16:07:30 -0500 Subject: [PATCH 07/10] Add Update idp tests --- nexus/tests/integration_tests/silos.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nexus/tests/integration_tests/silos.rs b/nexus/tests/integration_tests/silos.rs index 59d9cd016c6..f8f1412fcd6 100644 --- a/nexus/tests/integration_tests/silos.rs +++ b/nexus/tests/integration_tests/silos.rs @@ -644,7 +644,7 @@ async fn test_saml_idp_metadata_data_invalid( RequestBuilder::new( client, Method::POST, - &format!("/system/silos/{}/identity-providers/saml", SILO_NAME), + &format!("/v1/system/identity-providers/saml?silo={}", SILO_NAME), ) .body(Some(¶ms::SamlIdentityProviderCreate { identity: IdentityMetadataCreateParams { @@ -1832,7 +1832,7 @@ async fn test_local_silo_constraints(cptestctx: &ControlPlaneTestContext) { .id; grant_iam( client, - "/system/silos/fixed", + "/v1/system/silos/fixed", SiloRole::Admin, new_silo_user_id, AuthnMode::PrivilegedUser, @@ -1845,7 +1845,7 @@ async fn test_local_silo_constraints(cptestctx: &ControlPlaneTestContext) { client, StatusCode::BAD_REQUEST, Method::POST, - "/system/silos/fixed/identity-providers/saml", + "/v1/system/identity-providers/saml?silo=fixed", ¶ms::SamlIdentityProviderCreate { identity: IdentityMetadataCreateParams { name: "some-totally-real-saml-provider" From 970e4fa40d18959c61ec689f7b05d29ce6fd1256 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 3 Mar 2023 13:30:46 -0500 Subject: [PATCH 08/10] Add deprecations --- nexus/src/app/silo.rs | 14 +++++------ nexus/src/external_api/http_entrypoints.rs | 13 +++++++++++ openapi/nexus.json | 27 +++++++++++++++------- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 0430f774721..4718ae2416a 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -800,12 +800,12 @@ impl super::Nexus { .saml_identity_provider_create(opctx, &authz_idp_list, provider) .await } -} -pub fn silo_group_lookup<'a>( - &'a self, - opctx: &'a OpContext, - group_id: &'a Uuid, -) -> db::lookup::SiloGroup<'a> { - LookupPath::new(opctx, &self.db_datastore).silo_group_id(*group_id) + pub fn silo_group_lookup<'a>( + &'a self, + opctx: &'a OpContext, + group_id: &'a Uuid, + ) -> db::lookup::SiloGroup<'a> { + LookupPath::new(opctx, &self.db_datastore).silo_group_id(*group_id) + } } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 67493280a2d..bd16a2c9dec 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -533,6 +533,7 @@ async fn system_policy_update_v1( method = PUT, path = "/system/policy", tags = ["policy"], + deprecated = true }] async fn system_policy_update( rqctx: RequestContext>, @@ -714,10 +715,12 @@ async fn silo_list_v1( /// List silos /// /// Lists silos that are discoverable based on the current permissions. +/// Use `GET /v1/system/silos` instead #[endpoint { method = GET, path = "/system/silos", tags = ["system"], + deprecated = true }] async fn silo_list( rqctx: RequestContext>, @@ -768,10 +771,12 @@ async fn silo_create_v1( } /// Create a silo +/// Use `POST /v1/system/silos` instead #[endpoint { method = POST, path = "/system/silos", tags = ["system"], + deprecated = true }] async fn silo_create( rqctx: RequestContext>, @@ -998,10 +1003,12 @@ async fn silo_policy_update_v1( } /// Update a silo's IAM policy +/// Use `PUT /v1/system/silos/{silo}/policy` instead #[endpoint { method = PUT, path = "/system/silos/{silo_name}/policy", tags = ["system"], + deprecated = true }] async fn silo_policy_update( rqctx: RequestContext>, @@ -1140,10 +1147,12 @@ struct UserPathParam { } /// Fetch a user +/// Use `GET /v1/system/users/{user_id}` instead #[endpoint { method = GET, path = "/system/silos/{silo_name}/users/id/{user_id}", tags = ["system"], + deprecated = true }] async fn silo_user_view( rqctx: RequestContext>, @@ -1272,10 +1281,12 @@ async fn saml_identity_provider_create_v1( } /// Create a SAML IDP +/// Use `POST /v1/system/identity-providers/saml` instead. #[endpoint { method = POST, path = "/system/silos/{silo_name}/identity-providers/saml", tags = ["system"], + deprecated = true }] async fn saml_identity_provider_create( rqctx: RequestContext>, @@ -1383,10 +1394,12 @@ async fn saml_identity_provider_view( /// Users can only be created in Silos with `provision_type` == `Fixed`. /// Otherwise, Silo users are just-in-time (JIT) provisioned when a user first /// logs in using an external Identity Provider. +/// Use `POST /v1/system/identity-providers/local/users` instead #[endpoint { method = POST, path = "/v1/system/identity-providers/local/users", tags = ["system"], + deprecated = true }] async fn local_idp_user_create_v1( rqctx: RequestContext>, diff --git a/openapi/nexus.json b/openapi/nexus.json index 39efafef95b..5607409097b 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -6828,7 +6828,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/sagas": { @@ -6936,7 +6937,7 @@ "system" ], "summary": "List silos", - "description": "Lists silos that are discoverable based on the current permissions.", + "description": "Lists silos that are discoverable based on the current permissions. Use `GET /v1/system/silos` instead", "operationId": "silo_list", "parameters": [ { @@ -6985,6 +6986,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -6992,6 +6994,7 @@ "system" ], "summary": "Create a silo", + "description": "Use `POST /v1/system/silos` instead", "operationId": "silo_create", "requestBody": { "content": { @@ -7020,7 +7023,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}": { @@ -7313,6 +7317,7 @@ "system" ], "summary": "Create a SAML IDP", + "description": "Use `POST /v1/system/identity-providers/saml` instead.", "operationId": "saml_identity_provider_create", "parameters": [ { @@ -7352,7 +7357,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers/saml/{provider_name}": { @@ -7448,6 +7454,7 @@ "system" ], "summary": "Update a silo's IAM policy", + "description": "Use `PUT /v1/system/silos/{silo}/policy` instead", "operationId": "silo_policy_update", "parameters": [ { @@ -7487,7 +7494,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/users/all": { @@ -7564,6 +7572,7 @@ "system" ], "summary": "Fetch a user", + "description": "Use `GET /v1/system/users/{user_id}` instead", "operationId": "silo_user_view", "parameters": [ { @@ -7603,7 +7612,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/user": { @@ -10688,7 +10698,7 @@ "system" ], "summary": "Create a user", - "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider.", + "description": "Users can only be created in Silos with `provision_type` == `Fixed`. Otherwise, Silo users are just-in-time (JIT) provisioned when a user first logs in using an external Identity Provider. Use `POST /v1/system/identity-providers/local/users` instead", "operationId": "local_idp_user_create_v1", "parameters": [ { @@ -10727,7 +10737,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/v1/system/identity-providers/local/users/{user_id}": { From 45276ddd3c56cdfd4df85eac41804bfdb41aa7ce Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 6 Mar 2023 10:35:26 -0500 Subject: [PATCH 09/10] Revert perm changes for now --- nexus/src/app/silo.rs | 4 ++-- nexus/test-utils/src/resource_helpers.rs | 2 +- nexus/tests/integration_tests/saml.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 4718ae2416a..5d7bf3ab858 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -675,8 +675,8 @@ impl super::Nexus { silo_lookup: &lookup::Silo<'_>, params: params::SamlIdentityProviderCreate, ) -> CreateResult { - let (authz_silo, db_silo) = - silo_lookup.fetch_for(authz::Action::CreateChild).await?; + // TODO-security: This should likely be fetch_for CreateChild on the silo + let (authz_silo, db_silo) = silo_lookup.fetch().await?; let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo); if db_silo.user_provision_type != UserProvisionType::Jit { diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 34cba03156b..51786f2492b 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -232,7 +232,7 @@ pub async fn create_silo( ) -> Silo { object_create( client, - "/system/silos", + "/v1/system/silos", ¶ms::SiloCreate { identity: IdentityMetadataCreateParams { name: silo_name.parse().unwrap(), diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index 01e93691db6..594a4d7c254 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -40,7 +40,7 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { .await; let silo: Silo = NexusRequest::object_get( &client, - &format!("/system/silos/{}", SILO_NAME,), + &format!("/v1/system/silos/{}", SILO_NAME,), ) .authn_as(AuthnMode::PrivilegedUser) .execute() @@ -59,7 +59,7 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { let silo_saml_idp: views::SamlIdentityProvider = object_create( client, - &format!("/system/silos/{}/identity-providers/saml", SILO_NAME), + &format!("/v1/system/identity-providers/saml?silo={}", SILO_NAME), ¶ms::SamlIdentityProviderCreate { identity: IdentityMetadataCreateParams { name: "some-totally-real-saml-provider" @@ -300,7 +300,7 @@ async fn test_create_a_hidden_silo_saml_idp( let silo_saml_idp: views::SamlIdentityProvider = object_create( client, - "/system/silos/hidden/identity-providers/saml", + "/v1/system/identity-providers/saml?silo=hidden", ¶ms::SamlIdentityProviderCreate { identity: IdentityMetadataCreateParams { name: "some-totally-real-saml-provider" From 52478a2ea925231a0567a9381d1fa3213eb523b5 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 6 Mar 2023 11:24:02 -0500 Subject: [PATCH 10/10] Fix failing tests due to permission issues --- nexus/src/app/silo.rs | 4 ++-- nexus/tests/integration_tests/authz.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 5d7bf3ab858..f4c9d39424d 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -659,8 +659,8 @@ impl super::Nexus { silo_lookup: &lookup::Silo<'_>, pagparams: &PaginatedBy<'_>, ) -> ListResultVec { - let (.., authz_silo) = - silo_lookup.lookup_for(authz::Action::ListChildren).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) diff --git a/nexus/tests/integration_tests/authz.rs b/nexus/tests/integration_tests/authz.rs index 84d4406333e..26e58bd77b4 100644 --- a/nexus/tests/integration_tests/authz.rs +++ b/nexus/tests/integration_tests/authz.rs @@ -296,7 +296,7 @@ async fn test_list_silo_idps_for_unpriv(cptestctx: &ControlPlaneTestContext) { let _users: ResultsPage = NexusRequest::object_get( client, - &"/system/silos/authz/identity-providers", + &"/v1/system/identity-providers?silo=authz", ) .authn_as(AuthnMode::SiloUser(new_silo_user_id)) .execute() @@ -373,7 +373,7 @@ async fn test_silo_read_for_unpriv(cptestctx: &ControlPlaneTestContext) { // That user can access their own silo let _silo: views::Silo = - NexusRequest::object_get(client, &"/system/silos/authz") + NexusRequest::object_get(client, &"/v1/system/silos/authz") .authn_as(AuthnMode::SiloUser(new_silo_user_id)) .execute() .await @@ -383,8 +383,12 @@ async fn test_silo_read_for_unpriv(cptestctx: &ControlPlaneTestContext) { // But not others NexusRequest::new( - RequestBuilder::new(client, http::Method::GET, &"/system/silos/other") - .expect_status(Some(http::StatusCode::NOT_FOUND)), + RequestBuilder::new( + client, + http::Method::GET, + &"/v1/system/silos/other", + ) + .expect_status(Some(http::StatusCode::NOT_FOUND)), ) .authn_as(AuthnMode::SiloUser(new_silo_user_id)) .execute()