Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nexus] Allow silo admins to upload new certs #4669

Merged
merged 3 commits into from
Dec 12, 2023
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
18 changes: 16 additions & 2 deletions nexus/src/app/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,22 @@ impl super::Nexus {
.silo_required()
.internal_context("creating a Certificate")?;

let silo_fq_dns_names =
self.silo_fq_dns_names(opctx, authz_silo.id()).await?;
// The `opctx` we received is going to be checked for permission to
// create a cert below in `db_datastore.certificate_create`, but first
// we need to look up this silo's fully-qualified domain names in order
// to check that the cert we've been given is valid for this silo.
// Looking up DNS names requires reading the DNS configuration of the
// _rack_, which this user may not be able to do (even if they have
// permission to upload new certs, which almost certainly implies a
// silo-level admin). We'll use our `opctx_external_authn()` context,
// which is the same context used to create a silo. This is a higher
// privilege than the current user may have, but we believe it does not
// leak any information that a silo admin doesn't already know (the
// external DNS name(s) of the rack, which leads to their silo's DNS
// name(s)).
let silo_fq_dns_names = self
.silo_fq_dns_names(self.opctx_external_authn(), authz_silo.id())
.await?;

let kind = params.service;
let new_certificate = db::model::Certificate::new(
Expand Down
82 changes: 82 additions & 0 deletions nexus/tests/integration_tests/silos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::integration_tests::saml::SAML_IDP_DESCRIPTOR;
use dropshot::ResultsPage;
use nexus_db_queries::authn::silos::{
AuthenticatedSubject, IdentityProviderType,
};
Expand All @@ -19,6 +20,7 @@ use nexus_test_utils::resource_helpers::{
objects_list_page_authz, projects_list,
};
use nexus_test_utils_macros::nexus_test;
use nexus_types::external_api::views::Certificate;
use nexus_types::external_api::views::{
self, IdentityProvider, Project, SamlIdentityProvider, Silo,
};
Expand All @@ -27,6 +29,7 @@ use omicron_common::api::external::ObjectIdentity;
use omicron_common::api::external::{
IdentityMetadataCreateParams, LookupType, Name,
};
use omicron_test_utils::certificates::CertificateChain;
use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError};

use std::collections::{BTreeMap, BTreeSet, HashSet};
Expand Down Expand Up @@ -2437,3 +2440,82 @@ async fn check_fleet_privileges(
.unwrap();
}
}

// Test that a silo admin can create new certificates for their silo
//
// Internally, the certificate validation check requires the `authz::DNS_CONFIG`
// resource (to check that the certificate is valid for
// `{silo_name}.{external_dns_zone_name}`), which silo admins may not have. We
// have to use an alternate, elevated context to perform that check, and this
// test confirms we do so.
#[nexus_test]
async fn test_silo_admin_can_create_certs(cptestctx: &ControlPlaneTestContext) {
let client = &cptestctx.external_client;
let certs_url = "/v1/certificates";

// Create a silo with an admin user
let silo = create_silo(
client,
"silo-name",
true,
shared::SiloIdentityMode::LocalOnly,
)
.await;

let new_silo_user_id = create_local_user(
client,
&silo,
&"admin".parse().unwrap(),
params::UserPassword::LoginDisallowed,
)
.await
.id;

grant_iam(
client,
"/v1/system/silos/silo-name",
SiloRole::Admin,
new_silo_user_id,
AuthnMode::PrivilegedUser,
)
.await;

// The user should be able to create certs for this silo
let chain = CertificateChain::new(cptestctx.wildcard_silo_dns_name());
let (cert, key) =
(chain.cert_chain_as_pem(), chain.end_cert_private_key_as_pem());

let cert: Certificate = NexusRequest::objects_post(
client,
certs_url,
&params::CertificateCreate {
identity: IdentityMetadataCreateParams {
name: "test-cert".parse().unwrap(),
description: "the test cert".to_string(),
},
cert,
key,
service: shared::ServiceUsingCertificate::ExternalApi,
},
)
.authn_as(AuthnMode::SiloUser(new_silo_user_id))
.execute()
.await
.expect("failed to create certificate")
.parsed_body()
.unwrap();

// The cert should exist when listing the silo's certs as the silo admin
let silo_certs =
NexusRequest::object_get(client, &format!("{certs_url}?limit=10"))
.authn_as(AuthnMode::SiloUser(new_silo_user_id))
.execute()
.await
.expect("failed to list certificates")
.parsed_body::<ResultsPage<Certificate>>()
.expect("failed to parse body as ResultsPage<Certificate>")
.items;

assert_eq!(silo_certs.len(), 1);
assert_eq!(silo_certs[0].identity.id, cert.identity.id);
}
Loading