diff --git a/src/cli/client.rs b/src/cli/client.rs index 45a31cc8..f4f8511e 100644 --- a/src/cli/client.rs +++ b/src/cli/client.rs @@ -14,6 +14,7 @@ use crate::commons::remote::rfc8183; use crate::commons::remote::rfc8183::RepositoryResponse; use crate::commons::util::httpclient; use crate::constants::KRILL_CLI_API_ENV; +use commons::api::CaRepoDetails; /// Command line tool for Krill admin tasks pub struct KrillClient { @@ -89,8 +90,8 @@ impl KrillClient { Ok(ApiResponse::Rfc8183ChildRequest(req)) } - CaCommand::PublisherRequest(handle) => { - let uri = format!("api/v1/cas/{}/publisher_request", handle); + CaCommand::RepoPublisherRequest(handle) => { + let uri = format!("api/v1/cas/{}/repo/publisher_request", handle); let req: PublisherRequest = self.get_json(&uri)?; let (handle, id_cert) = req.unpack(); let req = rfc8183::PublisherRequest::new(None, handle, id_cert); @@ -98,6 +99,13 @@ impl KrillClient { Ok(ApiResponse::Rfc8183PublisherRequest(req)) } + CaCommand::RepoDetails(handle) => { + let uri = format!("api/v1/cas/{}/repo/", handle); + let details: CaRepoDetails = self.get_json(&uri)?; + + Ok(ApiResponse::RepoDetails(details)) + } + CaCommand::AddParent(handle, parent) => { let uri = format!("api/v1/cas/{}/parents", handle); self.post_json(&uri, parent)?; diff --git a/src/cli/options.rs b/src/cli/options.rs index 9b5bc198..b00c299f 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -475,10 +475,20 @@ impl Options { app.subcommand(sub) } + fn make_case_repo_show_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + let mut sub = SubCommand::with_name("show").about("Show current repo config and state."); + + sub = Self::add_general_args(sub); + sub = Self::add_my_ca_arg(sub); + + app.subcommand(sub) + } + fn make_cas_repo_sc<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { let mut sub = SubCommand::with_name("repo").about("Manage the repository for your CA."); sub = Self::make_case_repo_request_sc(sub); + sub = Self::make_case_repo_show_sc(sub); app.subcommand(sub) } @@ -871,7 +881,16 @@ impl Options { let general_args = GeneralArgs::from_matches(matches)?; let my_ca = Self::parse_my_ca(matches)?; - let command = Command::CertAuth(CaCommand::PublisherRequest(my_ca)); + let command = Command::CertAuth(CaCommand::RepoPublisherRequest(my_ca)); + + Ok(Options::make(general_args, command)) + } + + fn parse_matches_cas_repo_details(matches: &ArgMatches) -> Result { + let general_args = GeneralArgs::from_matches(matches)?; + let my_ca = Self::parse_my_ca(matches)?; + + let command = Command::CertAuth(CaCommand::RepoDetails(my_ca)); Ok(Options::make(general_args, command)) } @@ -879,6 +898,8 @@ impl Options { fn parse_matches_cas_repo(matches: &ArgMatches) -> Result { if let Some(m) = matches.subcommand_matches("request") { Self::parse_matches_cas_repo_request(m) + } else if let Some(m) = matches.subcommand_matches("show") { + Self::parse_matches_cas_repo_details(m) } else { Err(Error::UnrecognisedSubCommand) } @@ -1013,7 +1034,8 @@ pub enum CaCommand { ChildRequest(Handle), // Get the RFC8183 publisher request - PublisherRequest(Handle), + RepoPublisherRequest(Handle), + RepoDetails(Handle), // Add a parent to this CA AddParent(Handle, ParentCaReq), diff --git a/src/cli/report.rs b/src/cli/report.rs index 7ba8ed60..4e716c2c 100644 --- a/src/cli/report.rs +++ b/src/cli/report.rs @@ -1,8 +1,8 @@ use std::str::{from_utf8_unchecked, FromStr}; use crate::commons::api::{ - CertAuthHistory, CertAuthInfo, CertAuthList, CurrentObjects, ParentCaContact, PublisherDetails, - PublisherList, RouteAuthorization, + CaRepoDetails, CertAuthHistory, CertAuthInfo, CertAuthList, CurrentObjects, CurrentRepoState, + ParentCaContact, PubServerContact, PublisherDetails, PublisherList, RouteAuthorization, }; use crate::commons::remote::api::ClientInfo; use crate::commons::remote::rfc8183; @@ -30,6 +30,8 @@ pub enum ApiResponse { Rfc8183ChildRequest(rfc8183::ChildRequest), Rfc8183PublisherRequest(rfc8183::PublisherRequest), + RepoDetails(CaRepoDetails), + Empty, // Typically a successful post just gets an empty 200 response GenericBody(String), // For when the server echos Json to a successful post } @@ -58,6 +60,7 @@ impl ApiResponse { ApiResponse::Rfc8183ChildRequest(req) => Ok(Some(req.report(fmt)?)), ApiResponse::Rfc8183PublisherRequest(req) => Ok(Some(req.report(fmt)?)), ApiResponse::Rfc8183RepositoryResponse(res) => Ok(Some(res.report(fmt)?)), + ApiResponse::RepoDetails(details) => Ok(Some(details.report(fmt)?)), ApiResponse::GenericBody(body) => Ok(Some(body.clone())), ApiResponse::Empty => Ok(None), } @@ -353,3 +356,51 @@ impl Report for Vec { } } } + +impl Report for CaRepoDetails { + fn report(&self, format: ReportFormat) -> Result { + match format { + ReportFormat::Json => Ok(serde_json::to_string_pretty(self).unwrap()), + ReportFormat::Default | ReportFormat::Text => { + let mut res = String::new(); + + res.push_str("Repository Details:\n"); + match self.contact() { + PubServerContact::Embedded(repo_info) => { + res.push_str(" type: embedded\n"); + res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri())); + res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify())); + } + PubServerContact::Rfc8181(response) => { + res.push_str(" type: remote\n"); + res.push_str(&format!(" service uri: {}\n", response.service_uri())); + let repo_info = response.repo_info(); + res.push_str(&format!(" base_uri: {}\n", repo_info.base_uri())); + res.push_str(&format!(" rpki_notify: {}\n", repo_info.rpki_notify())); + } + } + + res.push_str("\n"); + res.push_str("Currently published:\n"); + match self.state() { + CurrentRepoState::Error(e) => { + res.push_str(&format!(" Error contacting repo! => {}", e)); + } + CurrentRepoState::List(list) => { + let elements = list.elements(); + if elements.is_empty() { + res.push_str(" \n"); + } else { + for el in elements.iter() { + res.push_str(&format!(" {} {}\n", el.hash(), el.uri())); + } + } + } + } + + Ok(res) + } + _ => Err(ReportError::UnsupportedFormat), + } + } +} diff --git a/src/commons/api/admin.rs b/src/commons/api/admin.rs index d80a2d82..ecad7de4 100644 --- a/src/commons/api/admin.rs +++ b/src/commons/api/admin.rs @@ -316,7 +316,7 @@ impl PublisherClientRequest { //------------ PubServerInfo ------------------------------------------------- -#[derive(Clone, Debug, Deserialize, Display, Serialize)] +#[derive(Clone, Debug, Deserialize, Display, Eq, PartialEq, Serialize)] #[allow(clippy::large_enum_variant)] pub enum PubServerContact { #[display(fmt = "Embedded server.")] @@ -331,10 +331,21 @@ impl PubServerContact { PubServerContact::Embedded(info) } + pub fn is_embedded(&self) -> bool { + match self { + PubServerContact::Embedded(_) => true, + _ => false, + } + } + pub fn rfc8183(response: rfc8183::RepositoryResponse) -> Self { PubServerContact::Rfc8181(response) } + pub fn is_rfc8183(&self) -> bool { + !self.is_embedded() + } + pub fn repo_info(&self) -> &RepoInfo { match self { PubServerContact::Embedded(info) => info, diff --git a/src/commons/api/ca.rs b/src/commons/api/ca.rs index 666728cd..faa8a39e 100644 --- a/src/commons/api/ca.rs +++ b/src/commons/api/ca.rs @@ -23,7 +23,8 @@ use crate::commons::api::admin::{Handle, ParentCaContact}; use crate::commons::api::publication; use crate::commons::api::publication::Publish; use crate::commons::api::{ - Base64, HexEncodedHash, IssuanceRequest, ParentHandle, RequestResourceLimit, RouteAuthorization, + Base64, HexEncodedHash, IssuanceRequest, ListReply, ParentHandle, PubServerContact, + RequestResourceLimit, RouteAuthorization, }; use crate::commons::eventsourcing::AggregateHistory; use crate::commons::remote::id::IdCert; @@ -1577,6 +1578,53 @@ impl fmt::Display for ResourceClassKeysInfo { } } +//------------ CaRepoDetails ------------------------------------------------- + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum CurrentRepoState { + List(ListReply), + Error(String), +} + +impl CurrentRepoState { + pub fn list(list: ListReply) -> Self { + CurrentRepoState::List(list) + } + + pub fn error(e: impl fmt::Display) -> Self { + CurrentRepoState::Error(e.to_string()) + } + + pub fn as_list(&self) -> &ListReply { + match &self { + CurrentRepoState::List(list) => list, + CurrentRepoState::Error(e) => panic!("{}", e), + } + } +} + +/// This struct contains the API details for the configure Repository server, +/// and objects published there, for a CA. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct CaRepoDetails { + contact: PubServerContact, + state: CurrentRepoState, +} + +impl CaRepoDetails { + pub fn new(contact: PubServerContact, state: CurrentRepoState) -> Self { + CaRepoDetails { contact, state } + } + + pub fn contact(&self) -> &PubServerContact { + &self.contact + } + + pub fn state(&self) -> &CurrentRepoState { + &self.state + } +} + //------------ ResSetErr ----------------------------------------------------- #[derive(Clone, Debug, Display, Eq, PartialEq)] diff --git a/src/commons/api/mod.rs b/src/commons/api/mod.rs index 5a185c31..cb2ed10b 100644 --- a/src/commons/api/mod.rs +++ b/src/commons/api/mod.rs @@ -176,12 +176,6 @@ impl From for HexEncodedHash { } } -impl ToString for HexEncodedHash { - fn to_string(&self) -> String { - unsafe { String::from_utf8_unchecked(self.0.to_vec()) } - } -} - impl Serialize for HexEncodedHash { fn serialize(&self, serializer: S) -> Result where @@ -201,6 +195,13 @@ impl<'de> Deserialize<'de> for HexEncodedHash { } } +impl fmt::Display for HexEncodedHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let string = unsafe { String::from_utf8_unchecked(self.0.to_vec()) }; + write!(f, "{}", string) + } +} + //------------ Link ---------------------------------------------------------- /// Defines a link element to include as part of a links array in a Json diff --git a/src/daemon/endpoints.rs b/src/daemon/endpoints.rs index ef92d0f3..b8c9487d 100644 --- a/src/daemon/endpoints.rs +++ b/src/daemon/endpoints.rs @@ -69,7 +69,7 @@ pub fn health() -> HttpResponse { /// Returns the server health. pub fn api_health(server: web::Data, auth: Auth) -> HttpResponse { - if_api_allowed(&server, &auth, || api_ok()) + if_api_allowed(&server, &auth, api_ok) } fn if_allowed(allowed: bool, op: F) -> HttpResponse @@ -360,6 +360,20 @@ pub fn ca_publisher_req( }) } +pub fn ca_repo_details( + server: web::Data, + auth: Auth, + handle: Path, +) -> HttpResponse { + let handle = handle.into_inner(); + if_api_allowed(&server, &auth, || { + match server.read().ca_repo_details(&handle) { + Some(req) => render_json(req), + None => api_not_found(), + } + }) +} + pub fn ca_add_parent( server: web::Data, auth: Auth, diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index fdcba4f0..048b4c27 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -78,7 +78,11 @@ pub fn start(config: &Config) -> Result<(), Error> { .route("/cas/{ca}/id", post().to(ca_update_id)) .route("/cas/{ca}/history", get().to(ca_history)) .route("/cas/{ca}/child_request", get().to(ca_child_req)) - .route("/cas/{ca}/publisher_request", get().to(ca_publisher_req)) + .route("/cas/{ca}/repo/", get().to(ca_repo_details)) + .route( + "/cas/{ca}/repo/publisher_request", + get().to(ca_publisher_req), + ) .route("/cas/{ca}/parents", post().to(ca_add_parent)) .route("/cas/{ca}/parents/{parent}", post().to(ca_update_parent)) .route("/cas/{ca}/parents/{parent}", delete().to(ca_remove_parent)) diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index e90c9f45..ba4e2c23 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -9,10 +9,11 @@ use rpki::cert::Cert; use rpki::uri; use crate::commons::api::{ - AddChildRequest, CertAuthHistory, CertAuthInfo, CertAuthInit, CertAuthList, ChildCaInfo, - ChildHandle, Handle, ListReply, ParentCaContact, ParentCaReq, ParentHandle, PublishDelta, - PublisherDetails, PublisherHandle, PublisherRequest, RepoInfo, RouteAuthorizationUpdates, - TaCertDetails, Token, UpdateChildRequest, + AddChildRequest, CaRepoDetails, CertAuthHistory, CertAuthInfo, CertAuthInit, CertAuthList, + ChildCaInfo, ChildHandle, CurrentRepoState, Handle, ListReply, ParentCaContact, ParentCaReq, + ParentHandle, PubServerContact, PublishDelta, PublisherDetails, PublisherHandle, + PublisherRequest, RepoInfo, RouteAuthorizationUpdates, TaCertDetails, Token, + UpdateChildRequest, }; use crate::commons::remote::rfc8183::{ChildRequest, RepositoryResponse}; use crate::commons::remote::sigmsg::SignedMessage; @@ -350,6 +351,32 @@ impl KrillServer { Ok(()) } + /// Return the info about the configure repository server for a given Ca, + /// and the actual objects published there, as reported by a list reply. + pub fn ca_repo_details(&self, handle: &Handle) -> Option { + self.caserver + .get_ca(handle) + .map(|ca| { + let contact = ca.pub_server_contact().clone(); + + let state = match &contact { + PubServerContact::Embedded(_) => match self.pubserver.list(handle) { + Err(e) => CurrentRepoState::error(e), + Ok(list) => CurrentRepoState::list(list), + }, + PubServerContact::Rfc8181(_response) => { + match self.caserver.send_rfc8181_list(handle) { + Err(e) => CurrentRepoState::error(e), + Ok(list) => CurrentRepoState::list(list), + } + } + }; + + CaRepoDetails::new(contact, state) + }) + .ok() + } + pub fn ca_update_id(&self, handle: Handle) -> EmptyRes { Ok(self.caserver.ca_update_id(handle)?) } diff --git a/tests/remote_publication.rs b/tests/remote_publication.rs index 98d3a1ad..fe5fc197 100644 --- a/tests/remote_publication.rs +++ b/tests/remote_publication.rs @@ -1,4 +1,5 @@ extern crate krill; +extern crate pretty; extern crate rpki; use std::path::PathBuf; @@ -7,16 +8,19 @@ use std::str::FromStr; use rpki::crypto::{PublicKeyFormat, Signer}; use rpki::uri; -use krill::cli::options::{Command, PublishersCommand}; +use krill::cli::options::{CaCommand, Command, PublishersCommand}; use krill::cli::report::ApiResponse; use krill::commons::api::rrdp::CurrentObjects; -use krill::commons::api::{Handle, PublisherHandle}; +use krill::commons::api::{CaRepoDetails, Handle, ParentCaReq, PublisherHandle, ResourceSet}; use krill::commons::remote::builder::IdCertBuilder; use krill::commons::util::softsigner::OpenSslSigner; +use krill::daemon::ca::ta_handle; use krill::daemon::test::{ - krill_admin, krill_pubd_admin, start_krill_pubd_server, test_with_krill_server, + add_child_to_ta_embedded, add_parent_to_ca, init_child, krill_admin, krill_pubd_admin, + start_krill_pubd_server, test_with_krill_server, wait_for_current_resources, }; use krill::pubd::Publisher; +use pretty::Doc::Append; fn publisher(work_dir: &PathBuf, base_uri: &str) -> Publisher { let mut signer = OpenSslSigner::build(work_dir).unwrap(); @@ -52,6 +56,14 @@ fn details_publisher(publisher: &PublisherHandle) -> ApiResponse { krill_pubd_admin(command) } +fn repo_details(ca: &Handle) -> CaRepoDetails { + let command = Command::CertAuth(CaCommand::RepoDetails(ca.clone())); + match krill_admin(command) { + ApiResponse::RepoDetails(details) => details, + _ => panic!("Expected repo details"), + } +} + /// This tests that you can run krill with an embedded TA and CA, and /// have the CA publish at another krill instance which is is set up /// as a publication server only (i.e. it just has no TA and CAs). @@ -60,44 +72,67 @@ fn remote_publication() { test_with_krill_server(|d| { start_krill_pubd_server(); - let alice_handle = Handle::from_str_unsafe("alice"); - let alice = publisher(&d, "rsync://localhost/repo/0/alice/"); + let ta_handle = ta_handle(); - // Add client "alice" - add_publisher(&alice_handle, &alice); + let child = Handle::from_str_unsafe("child"); - // Find "alice" in list - let res = list_publishers(); - match res { - ApiResponse::PublisherList(list) => assert!(list - .publishers() - .iter() - .find(|p| { p.id() == "alice" }) - .is_some()), - _ => panic!("Expected publisher list"), + // Set up child as a child of the TA + { + init_child(&child); + let child_resources = ResourceSet::from_strs("", "10.0.0.0/16", "").unwrap(); + + let parent = { + let parent_contact = add_child_to_ta_embedded(&child, child_resources.clone()); + ParentCaReq::new(ta_handle.clone(), parent_contact) + }; + + add_parent_to_ca(&child, parent); + wait_for_current_resources(&child, &child_resources); } - // Find details for alice - let details_res = details_publisher(&alice_handle); - match details_res { - ApiResponse::PublisherDetails(details) => { - assert_eq!(&alice_handle, details.handle()); - } - _ => panic!("Expected details"), - } + // Child should now publish using the embedded repo + let child_repo_details = repo_details(&child); + assert!(child_repo_details.contact().is_embedded()); + let list = child_repo_details.state().as_list(); + assert_eq!(2, list.elements().len()); - // Remove alice - remove_publisher(&alice_handle); - - // Expect that alice has been removed - let res = list_publishers(); - match res { - ApiResponse::PublisherList(list) => assert!(list - .publishers() - .iter() - .find(|p| { p.id() == "alice" }) - .is_none()), - _ => panic!("Expected publisher list"), - } + // let alice = publisher(&d, "rsync://localhost/repo/0/child/"); + // + // // Add client "alice" + // add_publisher(&alice_handle, &alice); + // + // // Find "alice" in list + // let res = list_publishers(); + // match res { + // ApiResponse::PublisherList(list) => assert!(list + // .publishers() + // .iter() + // .find(|p| { p.id() == "alice" }) + // .is_some()), + // _ => panic!("Expected publisher list"), + // } + // + // // Find details for alice + // let details_res = details_publisher(&alice_handle); + // match details_res { + // ApiResponse::PublisherDetails(details) => { + // assert_eq!(&alice_handle, details.handle()); + // } + // _ => panic!("Expected details"), + // } + // + // // Remove alice + // remove_publisher(&alice_handle); + // + // // Expect that alice has been removed + // let res = list_publishers(); + // match res { + // ApiResponse::PublisherList(list) => assert!(list + // .publishers() + // .iter() + // .find(|p| { p.id() == "alice" }) + // .is_none()), + // _ => panic!("Expected publisher list"), + // } }); }