Show repo state and published objects through API and CLI. (#104)

This commit is contained in:
Tim Bruijnzeels
2019-11-04 14:28:56 +01:00
parent c6f38065a7
commit 714f0fcb6e
10 changed files with 278 additions and 57 deletions
+10 -2
View File
@@ -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)?;
+24 -2
View File
@@ -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<Options, Error> {
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<Options, Error> {
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),
+53 -2
View File
@@ -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<RouteAuthorization> {
}
}
}
impl Report for CaRepoDetails {
fn report(&self, format: ReportFormat) -> Result<String, ReportError> {
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(" <nothing>\n");
} else {
for el in elements.iter() {
res.push_str(&format!(" {} {}\n", el.hash(), el.uri()));
}
}
}
}
Ok(res)
}
_ => Err(ReportError::UnsupportedFormat),
}
}
}
+12 -1
View File
@@ -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,
+49 -1
View File
@@ -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)]
+7 -6
View File
@@ -176,12 +176,6 @@ impl From<String> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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
+15 -1
View File
@@ -69,7 +69,7 @@ pub fn health() -> HttpResponse {
/// Returns the server health.
pub fn api_health(server: web::Data<AppServer>, auth: Auth) -> HttpResponse {
if_api_allowed(&server, &auth, || api_ok())
if_api_allowed(&server, &auth, api_ok)
}
fn if_allowed<F>(allowed: bool, op: F) -> HttpResponse
@@ -360,6 +360,20 @@ pub fn ca_publisher_req(
})
}
pub fn ca_repo_details(
server: web::Data<AppServer>,
auth: Auth,
handle: Path<Handle>,
) -> 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<AppServer>,
auth: Auth,
+5 -1
View File
@@ -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))
+31 -4
View File
@@ -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<CaRepoDetails> {
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)?)
}
+72 -37
View File
@@ -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"),
// }
});
}