diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index d732a47d8b1..f4c9d39424d 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) = @@ -164,13 +156,10 @@ impl super::Nexus { pub async fn silo_list_users( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, pagparams: &DataPageParams<'_, Uuid>, ) -> ListResultVec { - 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) @@ -181,13 +170,10 @@ impl super::Nexus { pub async fn silo_user_fetch( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, silo_user_id: Uuid, ) -> LookupResult { - 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, @@ -208,13 +194,9 @@ impl super::Nexus { /// provider. async fn local_idp_fetch_silo( &self, - opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, ) -> LookupResult<(authz::Silo, db::model::Silo)> { - let (authz_silo, db_silo) = LookupPath::new(opctx, &self.db_datastore) - .silo_name(silo_name) - .fetch() - .await?; + let (authz_silo, db_silo) = silo_lookup.fetch().await?; if db_silo.user_provision_type != UserProvisionType::ApiOnly { return Err(Error::not_found_by_name( ResourceType::IdentityProvider, @@ -229,11 +211,11 @@ impl super::Nexus { pub async fn local_idp_create_user( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, new_user_params: params::UserCreate, ) -> CreateResult { let (authz_silo, db_silo) = - self.local_idp_fetch_silo(opctx, silo_name).await?; + self.local_idp_fetch_silo(silo_lookup).await?; let authz_silo_user_list = authz::SiloUserList::new(authz_silo.clone()); // TODO-cleanup This authz check belongs in silo_user_create(). opctx @@ -267,11 +249,11 @@ impl super::Nexus { pub async fn local_idp_delete_user( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, silo_user_id: Uuid, ) -> DeleteResult { - let (authz_silo, _) = - self.local_idp_fetch_silo(opctx, silo_name).await?; + let (authz_silo, _) = self.local_idp_fetch_silo(silo_lookup).await?; + let (authz_silo_user, _) = self .silo_user_lookup_by_id( opctx, @@ -394,12 +376,12 @@ impl super::Nexus { pub async fn local_idp_user_set_password( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, silo_user_id: Uuid, password_value: params::UserPassword, ) -> UpdateResult<()> { let (authz_silo, db_silo) = - self.local_idp_fetch_silo(opctx, silo_name).await?; + self.local_idp_fetch_silo(silo_lookup).await?; let (authz_silo_user, db_silo_user) = self .silo_user_lookup_by_id( opctx, @@ -506,11 +488,10 @@ impl super::Nexus { pub async fn login_local( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, credentials: params::UsernamePasswordCredentials, ) -> Result, 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 @@ -640,16 +621,46 @@ impl super::Nexus { // identity providers + pub fn saml_identity_provider_lookup<'a>( + &'a self, + opctx: &'a OpContext, + saml_identity_provider_selector: &'a params::SamlIdentityProviderSelector, + ) -> LookupResult> { + match saml_identity_provider_selector { + params::SamlIdentityProviderSelector { + saml_identity_provider: NameOrId::Id(id), + silo_selector: None, + } => { + + let saml_provider = LookupPath::new(opctx, &self.db_datastore) + .saml_identity_provider_id(*id); + Ok(saml_provider) + } + params::SamlIdentityProviderSelector { + saml_identity_provider: NameOrId::Name(name), + silo_selector: Some(silo_selector), + } => { + let saml_provider = self + .silo_lookup(opctx, &silo_selector.silo)? + .saml_identity_provider_name(Name::ref_cast(name)); + Ok(saml_provider) + } + params::SamlIdentityProviderSelector { + saml_identity_provider: NameOrId::Id(_), + silo_selector: Some(_), + } => Err(Error::invalid_request("when providing provider as an ID, silo should not be specified")), + _ => Err(Error::invalid_request("provider should either be a UUID or silo should be specified")) + } + } + 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?; + // 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) @@ -661,13 +672,11 @@ impl super::Nexus { pub async fn saml_identity_provider_create( &self, opctx: &OpContext, - silo_name: &Name, + silo_lookup: &lookup::Silo<'_>, params: params::SamlIdentityProviderCreate, ) -> CreateResult { - let (authz_silo, db_silo) = LookupPath::new(opctx, &self.db_datastore) - .silo_name(silo_name) - .fetch() - .await?; + // TODO-security: This should likely be fetch_for CreateChild on the silo + let (authz_silo, db_silo) = silo_lookup.fetch().await?; let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo); if db_silo.user_provision_type != UserProvisionType::Jit { @@ -792,21 +801,6 @@ impl super::Nexus { .await } - pub async fn saml_identity_provider_fetch( - &self, - opctx: &OpContext, - silo_name: &Name, - provider_name: &Name, - ) -> LookupResult { - let (.., saml_identity_provider) = - LookupPath::new(opctx, &self.datastore()) - .silo_name(silo_name) - .saml_identity_provider_name(provider_name) - .fetch() - .await?; - Ok(saml_identity_provider) - } - pub fn silo_group_lookup<'a>( &'a self, opctx: &'a OpContext, 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/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 0982ed5d990..bd16a2c9dec 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -322,13 +322,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)?; @@ -359,10 +374,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)?; api.register(group_view)?; @@ -515,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>, @@ -548,13 +567,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 @@ -575,13 +596,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 @@ -605,13 +628,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 @@ -638,13 +663,15 @@ 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 @@ -653,10 +680,47 @@ async fn policy_update( /// 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. +/// Use `GET /v1/system/silos` instead #[endpoint { method = GET, path = "/system/silos", tags = ["system"], + deprecated = true }] async fn silo_list( rqctx: RequestContext>, @@ -686,10 +750,33 @@ async fn silo_list( } /// 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 +/// Use `POST /v1/system/silos` instead #[endpoint { method = POST, path = "/system/silos", tags = ["system"], + deprecated = true }] async fn silo_create( rqctx: RequestContext>, @@ -706,6 +793,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 { @@ -716,44 +827,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 @@ -762,56 +879,136 @@ 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 } /// 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>, @@ -819,19 +1016,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 @@ -840,10 +1036,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>, @@ -852,13 +1084,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()) @@ -872,6 +1105,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 { @@ -882,27 +1147,26 @@ 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>, 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 @@ -910,11 +1174,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>, @@ -922,16 +1223,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()) @@ -948,10 +1252,41 @@ 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 +/// 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>, @@ -959,14 +1294,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?; @@ -975,6 +1312,40 @@ 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}", + 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 saml_identity_provider_selector = + params::SamlIdentityProviderSelector::new( + Some(query.silo), + path.provider, + ); + let (.., provider) = nexus + .saml_identity_provider_lookup( + &opctx, + &saml_identity_provider_selector, + )? + .fetch() + .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 { @@ -985,30 +1356,30 @@ 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>, 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 @@ -1018,6 +1389,41 @@ 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. +/// 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>, + 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`. @@ -1034,14 +1440,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?; @@ -1051,28 +1458,85 @@ 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, + 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 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()) + }; + 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>, 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?; + 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 +} + +/// Set or invalidate a user's password +/// +/// 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, + query_params: Query, + update: TypedBody, +) -> 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 query = query_params.into_inner(); + let silo_lookup = nexus.silo_lookup(&opctx, &query.silo)?; nexus - .local_idp_delete_user( + .local_idp_user_set_password( &opctx, - &path_params.silo_name, - path_params.user_id, + &silo_lookup, + path.user_id, + update.into_inner(), ) .await?; - Ok(HttpResponseDeleted()) + Ok(HttpResponseUpdatedNoContent()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -1081,10 +1545,12 @@ async fn local_idp_user_delete( /// /// 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>, @@ -1092,15 +1558,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?; @@ -1156,12 +1624,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/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/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() diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index f0133aed566..6dc800db888 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/identity-providers/local/users?silo={}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USERS_LIST_URL: String = format!( - "/system/silos/{}/users/all", + "/v1/system/users?silo={}", DEFAULT_SILO.identity().name, ); pub static ref DEMO_SILO_USER_ID_GET_URL: String = format!( - "/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!( - "/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!( - "/system/silos/{}/identity-providers/local/users/{{id}}/set-password", + "/v1/system/identity-providers/local/users/{{id}}/set-password?silo={}", DEFAULT_SILO.identity().name, ); @@ -426,11 +426,11 @@ 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/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 { @@ -732,7 +732,7 @@ lazy_static! { /* Silos */ VerifyEndpoint { - url: "/system/silos", + url: "/v1/system/silos", visibility: Visibility::Public, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -742,14 +742,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 cf013f604fa..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" @@ -88,9 +88,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(), @@ -98,6 +97,8 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { .unwrap() .into(), ) + .unwrap() + .fetch() .await .unwrap(); @@ -299,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" 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" diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 5573b10ccf8..11bf9fca02f 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -185,8 +185,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 @@ -198,17 +201,28 @@ 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/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_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} 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/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index 1c0a7454b39..643cb4ffd88 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}") @@ -46,6 +48,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") @@ -57,6 +60,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") @@ -82,6 +92,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}") @@ -94,3 +108,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/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index db1d50413ba..154780c4dbc 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -71,6 +71,47 @@ pub struct SnapshotPath { pub snapshot: NameOrId, } +#[derive(Serialize, Deserialize, JsonSchema)] +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, +} + +impl From for SiloSelector { + fn from(name: Name) -> Self { + SiloSelector { silo: name.into() } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct SamlIdentityProviderSelector { + #[serde(flatten)] + pub silo_selector: Option, + pub saml_identity_provider: NameOrId, +} + +// TODO-v1: delete this post migration +impl SamlIdentityProviderSelector { + pub fn new( + silo: Option, + saml_identity_provider: NameOrId, + ) -> Self { + SamlIdentityProviderSelector { + silo_selector: silo.map(|s| SiloSelector { silo: s }), + saml_identity_provider, + } + } +} + // Only by ID because groups have an `external_id` instead of a name and // therefore don't implement `ObjectIdentity`, which makes lookup by name // inconvenient. We should figure this out more generally, as there are several diff --git a/openapi/nexus.json b/openapi/nexus.json index c5f89592832..5607409097b 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -5499,6 +5499,7 @@ "system" ], "summary": "Fetch a silo by id", + "description": "Use `GET /v1/system/silos/{id}` instead.", "operationId": "silo_view_by_id", "parameters": [ { @@ -5528,7 +5529,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/certificates": { @@ -6826,7 +6828,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/sagas": { @@ -6934,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": [ { @@ -6983,6 +6986,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true }, "post": { @@ -6990,6 +6994,7 @@ "system" ], "summary": "Create a silo", + "description": "Use `POST /v1/system/silos` instead", "operationId": "silo_create", "requestBody": { "content": { @@ -7018,7 +7023,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}": { @@ -7027,7 +7033,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": [ { @@ -7057,14 +7063,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": [ { @@ -7087,7 +7094,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers": { @@ -7096,6 +7104,7 @@ "system" ], "summary": "List a silo's IDPs", + "description": "Use `/v1/system/silos/{silo}/identity-providers` instead.", "operationId": "silo_identity_provider_list", "parameters": [ { @@ -7153,6 +7162,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true } }, @@ -7211,6 +7221,7 @@ "system" ], "summary": "Delete a user", + "description": "Use `DELETE /v1/system/identity-providers/local/users/{user_id}` instead", "operationId": "local_idp_user_delete", "parameters": [ { @@ -7243,7 +7254,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers/local/users/{user_id}/set-password": { @@ -7252,7 +7264,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": [ { @@ -7295,7 +7307,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers/saml": { @@ -7304,6 +7317,7 @@ "system" ], "summary": "Create a SAML IDP", + "description": "Use `POST /v1/system/identity-providers/saml` instead.", "operationId": "saml_identity_provider_create", "parameters": [ { @@ -7343,7 +7357,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/identity-providers/saml/{provider_name}": { @@ -7352,6 +7367,7 @@ "system" ], "summary": "Fetch a SAML IDP", + "description": "Use `GET /v1/system/identity-providers/saml/{provider_name}` instead", "operationId": "saml_identity_provider_view", "parameters": [ { @@ -7390,7 +7406,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/policy": { @@ -7399,6 +7416,7 @@ "system" ], "summary": "Fetch a silo's IAM policy", + "description": "Use `GET /v1/system/silos/{silo}/policy` instead.", "operationId": "silo_policy_view", "parameters": [ { @@ -7428,13 +7446,15 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true }, "put": { "tags": [ "system" ], "summary": "Update a silo's IAM policy", + "description": "Use `PUT /v1/system/silos/{silo}/policy` instead", "operationId": "silo_policy_update", "parameters": [ { @@ -7474,7 +7494,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/silos/{silo_name}/users/all": { @@ -7483,6 +7504,7 @@ "system" ], "summary": "List users in a silo", + "description": "Use `GET /v1/system/users` instead.", "operationId": "silo_users_list", "parameters": [ { @@ -7540,6 +7562,7 @@ "$ref": "#/components/responses/Error" } }, + "deprecated": true, "x-dropshot-pagination": true } }, @@ -7549,6 +7572,7 @@ "system" ], "summary": "Fetch a user", + "description": "Use `GET /v1/system/users/{user_id}` instead", "operationId": "silo_user_view", "parameters": [ { @@ -7588,7 +7612,8 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, "/system/user": { @@ -10603,75 +10628,13 @@ "x-dropshot-pagination": true } }, - "/v1/system/policy": { - "get": { - "tags": [ - "policy" - ], - "summary": "Fetch the top-level IAM policy", - "operationId": "system_policy_view_v1", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FleetRolePolicy" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "put": { - "tags": [ - "policy" - ], - "summary": "Update the top-level IAM policy", - "operationId": "system_policy_update_v1", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FleetRolePolicy" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FleetRolePolicy" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/v1/system/sagas": { + "/v1/system/identity-providers": { "get": { "tags": [ "system" ], - "summary": "List sagas", - "operationId": "saga_list_v1", + "summary": "List a silo's IDPs_name", + "operationId": "silo_identity_provider_list_v1", "parameters": [ { "in": "query", @@ -10693,11 +10656,18 @@ "type": "string" } }, + { + "in": "query", + "name": "silo", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, { "in": "query", "name": "sort_by", "schema": { - "$ref": "#/components/schemas/IdSortMode" + "$ref": "#/components/schemas/NameOrIdSortMode" } } ], @@ -10707,7 +10677,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SagaResultsPage" + "$ref": "#/components/schemas/IdentityProviderResultsPage" } } } @@ -10722,31 +10692,41 @@ "x-dropshot-pagination": true } }, - "/v1/system/sagas/{saga_id}": { - "get": { + "/v1/system/identity-providers/local/users": { + "post": { "tags": [ "system" ], - "summary": "Fetch a saga", - "operationId": "saga_view_v1", + "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. Use `POST /v1/system/identity-providers/local/users` instead", + "operationId": "local_idp_user_create_v1", "parameters": [ { - "in": "path", - "name": "saga_id", + "in": "query", + "name": "silo", "required": true, "schema": { - "type": "string", - "format": "uuid" + "$ref": "#/components/schemas/NameOrId" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "successful operation", + "201": { + "description": "successful creation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Saga" + "$ref": "#/components/schemas/User" } } } @@ -10757,55 +10737,40 @@ "5XX": { "$ref": "#/components/responses/Error" } - } + }, + "deprecated": true } }, - "/v1/system/update/components": { - "get": { + "/v1/system/identity-providers/local/users/{user_id}": { + "delete": { "tags": [ "system" ], - "summary": "View version and update status of component tree", - "operationId": "system_component_version_list", + "summary": "Delete a user", + "operationId": "local_idp_user_delete_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", + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, "schema": { - "nullable": true, - "type": "string" + "type": "string", + "format": "uuid" } }, { "in": "query", - "name": "sort_by", + "name": "silo", + "required": true, "schema": { - "$ref": "#/components/schemas/IdSortMode" + "$ref": "#/components/schemas/NameOrId" } } ], "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateableComponentResultsPage" - } - } - } + "204": { + "description": "successful deletion" }, "4XX": { "$ref": "#/components/responses/Error" @@ -10813,74 +10778,672 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "x-dropshot-pagination": true + } } }, - "/v1/system/update/deployments": { - "get": { + "/v1/system/identity-providers/local/users/{user_id}/set-password": { + "post": { "tags": [ "system" ], - "summary": "List all update deployments", - "operationId": "update_deployments_list", + "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": "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", + "in": "path", + "name": "user_id", + "description": "The user's internal id", + "required": true, "schema": { - "nullable": true, - "type": "string" + "type": "string", + "format": "uuid" } }, { "in": "query", - "name": "sort_by", + "name": "silo", + "required": true, "schema": { - "$ref": "#/components/schemas/IdSortMode" + "$ref": "#/components/schemas/NameOrId" } } ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateDeploymentResultsPage" - } + "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" } - }, - "x-dropshot-pagination": true + } } }, - "/v1/system/update/deployments/{id}": { - "get": { + "/v1/system/identity-providers/saml": { + "post": { "tags": [ "system" ], - "summary": "Fetch a system update deployment", - "operationId": "update_deployment_view", + "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": [ + "policy" + ], + "summary": "Fetch the top-level IAM policy", + "operationId": "system_policy_view_v1", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "policy" + ], + "summary": "Update the top-level IAM policy", + "operationId": "system_policy_update_v1", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FleetRolePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/sagas": { + "get": { + "tags": [ + "system" + ], + "summary": "List sagas", + "operationId": "saga_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/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SagaResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/system/sagas/{saga_id}": { + "get": { + "tags": [ + "system" + ], + "summary": "Fetch a saga", + "operationId": "saga_view_v1", + "parameters": [ + { + "in": "path", + "name": "saga_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Saga" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/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}/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": [ + "system" + ], + "summary": "View version and update status of component tree", + "operationId": "system_component_version_list", + "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/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateableComponentResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/system/update/deployments": { + "get": { + "tags": [ + "system" + ], + "summary": "List all update deployments", + "operationId": "update_deployments_list", + "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/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDeploymentResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": true + } + }, + "/v1/system/update/deployments/{id}": { + "get": { + "tags": [ + "system" + ], + "summary": "Fetch a system update deployment", + "operationId": "update_deployment_view", "parameters": [ { "in": "path", @@ -11148,6 +11711,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": [