Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion nexus/db-lookup/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,12 +875,16 @@ lookup_resource! {
primary_key_columns = [ { column_name = "id", uuid_kind = BuiltInUserKind } ]
}

// Visible outside the silo so fleet admins can manage any silo's certificates
// by ID. Lookups on behalf of the current silo must check the silo themselves
// (see `current_silo_certificate_fetch_for` in Nexus).
lookup_resource! {
name = "Certificate",
ancestors = [ "Silo" ],
lookup_by_name = true,
soft_deletes = true,
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ],
visible_outside_silo = true
}

lookup_resource! {
Expand Down
28 changes: 15 additions & 13 deletions nexus/db-queries/src/db/datastore/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use nexus_db_errors::public_error_from_diesel;
use nexus_types::identity::Resource;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::InternalContext;
use omicron_common::api::external::Error;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::ResourceType;
use omicron_common::api::external::http_pagination::PaginatedBy;
Expand All @@ -30,15 +30,20 @@ impl DataStore {
pub async fn certificate_create(
&self,
opctx: &OpContext,
authz_silo: &authz::Silo,
certificate: Certificate,
) -> CreateResult<Certificate> {
use nexus_db_schema::schema::certificate::dsl;

let authz_silo = opctx
.authn
.silo_required()
.internal_context("creating a Certificate")?;
let authz_cert_list = authz::SiloCertificateList::new(authz_silo);
if certificate.silo_id != authz_silo.id() {
return Err(Error::internal_error(&format!(
"certificate silo {} does not match authorized silo {}",
certificate.silo_id,
authz_silo.id(),
)));
}
let authz_cert_list =
authz::SiloCertificateList::new(authz_silo.clone());
opctx.authorize(authz::Action::CreateChild, &authz_cert_list).await?;

let name = certificate.name().clone();
Expand Down Expand Up @@ -66,17 +71,14 @@ impl DataStore {
opctx: &OpContext,
kind: Option<ServiceKind>,
pagparams: &PaginatedBy<'_>,
silo_only: bool,
authz_silo: Option<&authz::Silo>,
) -> ListResultVec<Certificate> {
use nexus_db_schema::schema::certificate::dsl;

let silo = if silo_only {
let authz_silo = opctx
.authn
.silo_required()
.internal_context("listing Certificates")?;
let silo = if let Some(authz_silo) = authz_silo {
let silo_id = authz_silo.id();
let authz_cert_list = authz::SiloCertificateList::new(authz_silo);
let authz_cert_list =
authz::SiloCertificateList::new(authz_silo.clone());
opctx
.authorize(authz::Action::ListChildren, &authz_cert_list)
.await?;
Expand Down
4 changes: 4 additions & 0 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ scim_token_create POST /v1/system/scim/tokens
scim_token_delete DELETE /v1/system/scim/tokens/{token_id}
scim_token_list GET /v1/system/scim/tokens
scim_token_view GET /v1/system/scim/tokens/{token_id}
silo_certificate_create POST /v1/system/certificates
silo_certificate_delete DELETE /v1/system/certificates/{certificate}
silo_certificate_list GET /v1/system/certificates
silo_certificate_view GET /v1/system/certificates/{certificate}
silo_create POST /v1/system/silos
silo_delete DELETE /v1/system/silos/{silo}
silo_identity_provider_list GET /v1/system/identity-providers
Expand Down
84 changes: 70 additions & 14 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ api_versions!([
// | date-based version should be at the top of the list.
// v
// (next_yyyy_mm_dd_nn, IDENT),
(2026_10_15_00, SYSTEM_CERTIFICATES),
(2026_10_13_00, ARTIFACT_HASH_SCHEMA),
(2026_09_15_00, REMOVE_SILO_DISCOVERABLE),
(2026_09_11_00, ALERT_PAYLOAD),
Expand Down Expand Up @@ -5078,11 +5079,7 @@ pub trait NexusExternalApi {

// Certificates

/// List certificates for external endpoints
///
/// Returns a list of TLS certificates used for the external API (for the
/// current Silo). These are sorted by creation date, with the most recent
/// certificates appearing first.
/// List TLS certificates in current silo
#[endpoint {
method = GET,
path = "/v1/certificates",
Expand All @@ -5096,10 +5093,10 @@ pub trait NexusExternalApi {
HttpError,
>;

/// Create system-wide x.509 certificate
/// Create TLS certificate in current silo
///
/// This certificate is automatically used by the Oxide Control plane to serve
/// external connections.
/// This certificate is automatically used by the Oxide control plane to serve
/// API requests.
#[endpoint {
method = POST,
path = "/v1/certificates",
Expand All @@ -5110,9 +5107,7 @@ pub trait NexusExternalApi {
new_cert: TypedBody<latest::certificate::CertificateCreate>,
) -> Result<HttpResponseCreated<latest::certificate::Certificate>, HttpError>;

/// Fetch certificate
///
/// Returns the details of a specific certificate
/// Fetch TLS certificate in current silo
#[endpoint {
method = GET,
path = "/v1/certificates/{certificate}",
Expand All @@ -5123,9 +5118,7 @@ pub trait NexusExternalApi {
path_params: Path<latest::path_params::CertificatePath>,
) -> Result<HttpResponseOk<latest::certificate::Certificate>, HttpError>;

/// Delete certificate
///
/// Permanently delete a certificate. This operation cannot be undone.
/// Delete TLS certificate in current silo
#[endpoint {
method = DELETE,
path = "/v1/certificates/{certificate}",
Expand All @@ -5136,6 +5129,69 @@ pub trait NexusExternalApi {
path_params: Path<latest::path_params::CertificatePath>,
) -> Result<HttpResponseDeleted, HttpError>;

/// List TLS certificates in silo
#[endpoint {
method = GET,
path = "/v1/system/certificates",
tags = ["system/silos"],
versions = VERSION_SYSTEM_CERTIFICATES..,
}]
async fn silo_certificate_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedByNameOrId<latest::silo::SiloSelector>>,
) -> Result<
HttpResponseOk<ResultsPage<latest::certificate::Certificate>>,
HttpError,
>;

/// Create TLS certificate in silo
///
/// This certificate is automatically used by the Oxide control plane to serve
/// API requests.
#[endpoint {
method = POST,
path = "/v1/system/certificates",
tags = ["system/silos"],
versions = VERSION_SYSTEM_CERTIFICATES..,
}]
async fn silo_certificate_create(
rqctx: RequestContext<Self::Context>,
query_params: Query<latest::silo::SiloSelector>,
new_cert: TypedBody<latest::certificate::CertificateCreate>,
) -> Result<HttpResponseCreated<latest::certificate::Certificate>, HttpError>;

/// Fetch TLS certificate in silo
///
/// The silo must be specified if the certificate is specified by name, and
/// must not be specified if the certificate is specified by ID.
#[endpoint {
method = GET,
path = "/v1/system/certificates/{certificate}",
tags = ["system/silos"],
versions = VERSION_SYSTEM_CERTIFICATES..,
}]
async fn silo_certificate_view(
rqctx: RequestContext<Self::Context>,
path_params: Path<latest::path_params::CertificatePath>,
query_params: Query<latest::silo::OptionalSiloSelector>,
) -> Result<HttpResponseOk<latest::certificate::Certificate>, HttpError>;

/// Delete TLS certificate in silo
///
/// The silo must be specified if the certificate is specified by name, and
/// must not be specified if the certificate is specified by ID.
#[endpoint {
method = DELETE,
path = "/v1/system/certificates/{certificate}",
tags = ["system/silos"],
versions = VERSION_SYSTEM_CERTIFICATES..,
}]
async fn silo_certificate_delete(
rqctx: RequestContext<Self::Context>,
path_params: Path<latest::path_params::CertificatePath>,
query_params: Query<latest::silo::OptionalSiloSelector>,
) -> Result<HttpResponseDeleted, HttpError>;

/// Create address lot
#[endpoint {
method = POST,
Expand Down
Loading
Loading