summaryrefslogtreecommitdiffstats
path: root/crates/core/tedge/src/cli/certificate/upload.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/tedge/src/cli/certificate/upload.rs')
-rw-r--r--crates/core/tedge/src/cli/certificate/upload.rs251
1 files changed, 251 insertions, 0 deletions
diff --git a/crates/core/tedge/src/cli/certificate/upload.rs b/crates/core/tedge/src/cli/certificate/upload.rs
new file mode 100644
index 00000000..ba5141d8
--- /dev/null
+++ b/crates/core/tedge/src/cli/certificate/upload.rs
@@ -0,0 +1,251 @@
+use super::error::{get_webpki_error_from_reqwest, CertError};
+use crate::command::Command;
+use reqwest::{StatusCode, Url};
+use std::{io::prelude::*, path::Path};
+use tedge_config::*;
+use tedge_utils::paths::pathbuf_to_string;
+
+#[derive(Debug, serde::Deserialize)]
+struct CumulocityResponse {
+ name: String,
+}
+
+#[derive(Debug, serde::Serialize)]
+#[serde(rename_all = "camelCase")]
+struct UploadCertBody {
+ name: String,
+ cert_in_pem_format: String,
+ auto_registration_enabled: bool,
+ status: String,
+}
+
+pub struct UploadCertCmd {
+ pub device_id: String,
+ pub path: FilePath,
+ pub host: ConnectUrl,
+ pub username: String,
+}
+
+impl Command for UploadCertCmd {
+ fn description(&self) -> String {
+ "upload root certificate".into()
+ }
+
+ fn execute(&self) -> anyhow::Result<()> {
+ Ok(self.upload_certificate()?)
+ }
+}
+
+impl UploadCertCmd {
+ fn upload_certificate(&self) -> Result<(), CertError> {
+ // Read the password from /dev/tty
+ // Unless a password is provided using the `C8YPASS` env var.
+ let password = match std::env::var("C8YPASS") {
+ Ok(password) => password,
+ Err(_) => rpassword::read_password_from_tty(Some("Enter password: "))?,
+ };
+
+ // Use a builder instead of `Client::new`, `new` could panic, builder adds option to allow invalid certs.
+ let client = reqwest::blocking::Client::builder().build()?;
+
+ // To post certificate c8y requires one of the following endpoints:
+ // https://<tenant_id>.cumulocity.url.io/tenant/tenants/<tenant_id>/trusted-certificates
+ // https://<tenant_domain>.cumulocity.url.io/tenant/tenants/<tenant_id>/trusted-certificates
+ // and therefore we need to get tenant_id.
+ let tenant_id = get_tenant_id_blocking(
+ &client,
+ build_get_tenant_id_url(self.host.as_str())?,
+ &self.username,
+ &password,
+ )?;
+ self.post_certificate(&client, &tenant_id, &password)
+ }
+
+ fn post_certificate(
+ &self,
+ client: &reqwest::blocking::Client,
+ tenant_id: &str,
+ password: &str,
+ ) -> Result<(), CertError> {
+ let post_url = build_upload_certificate_url(self.host.as_str(), tenant_id)?;
+
+ let post_body = UploadCertBody {
+ auto_registration_enabled: true,
+ cert_in_pem_format: read_cert_to_string(&self.path)?,
+ name: self.device_id.clone(),
+ status: "ENABLED".into(),
+ };
+
+ let res = client
+ .post(post_url)
+ .json(&post_body)
+ .basic_auth(&self.username, Some(password))
+ .send()
+ .map_err(get_webpki_error_from_reqwest)?;
+
+ match res.status() {
+ StatusCode::OK | StatusCode::CREATED => {
+ println!("Certificate uploaded successfully.");
+ Ok(())
+ }
+
+ StatusCode::CONFLICT => {
+ println!("Certificate already exists in the cloud.");
+ Ok(())
+ }
+
+ status_code => {
+ println!("Something went wrong: {}", status_code);
+ Err(CertError::StatusCode(status_code))
+ }
+ }
+ }
+}
+
+fn build_get_tenant_id_url(host: &str) -> Result<Url, CertError> {
+ let url = format!("https://{}/tenant/currentTenant", host);
+ Ok(Url::parse(&url)?)
+}
+
+fn build_upload_certificate_url(host: &str, tenant_id: &str) -> Result<Url, CertError> {
+ let url_str = format!(
+ "https://{}/tenant/tenants/{}/trusted-certificates",
+ host, tenant_id
+ );
+
+ Ok(Url::parse(&url_str)?)
+}
+
+fn get_tenant_id_blocking(
+ client: &reqwest::blocking::Client,
+ url: Url,
+ username: &str,
+ password: &str,
+) -> Result<String, CertError> {
+ let res = client
+ .get(url)
+ .basic_auth(username, Some(password))
+ .send()
+ .map_err(get_webpki_error_from_reqwest)?
+ .error_for_status()?;
+
+ let body = res.json::<CumulocityResponse>()?;
+ Ok(body.name)
+}
+
+fn read_cert_to_string(path: impl AsRef<Path>) -> Result<String, CertError> {
+ let path = path.as_ref();
+ let path = pathbuf_to_string(path.to_owned())?;
+
+ let mut file =
+ std::fs::File::open(&path).map_err(|err| CertError::CertificateReadFailed(err, path))?;
+ let mut content = String::new();
+ file.read_to_string(&mut content)?;
+
+ Ok(content)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use assert_matches::assert_matches;
+
+ #[test]
+ fn build_get_tenant_id_url_should_return_error_given_invalid_host() {
+ let result = build_get_tenant_id_url("%");
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn get_tenant_id_blocking_should_return_error_given_wrong_credentials() {
+ let client = reqwest::blocking::Client::new();
+
+ let request_url =
+ Url::parse(&format!("{}/tenant/currentTenant", mockito::server_url())).unwrap();
+
+ let auth_header_field = "authorization";
+ let auth_header_value = "Basic dGVzdDpmYWlsZWR0ZXN0"; // Base64 encoded test:failedtest
+
+ let response_body = r#"{"name":"test"}"#;
+ let expected_status = 200;
+
+ let _serv = mockito::mock("GET", "/tenant/currentTenant")
+ .match_header(auth_header_field, auth_header_value)
+ .with_body(response_body)
+ .with_status(expected_status)
+ .create();
+
+ let res = get_tenant_id_blocking(&client, request_url, "test", "test");
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn get_tenant_id_blocking_returns_correct_response() {
+ let client = reqwest::blocking::Client::new();
+
+ let request_url =
+ Url::parse(&format!("{}/tenant/currentTenant", mockito::server_url())).unwrap();
+
+ let auth_header_field = "authorization";
+ let auth_header_value = "Basic dGVzdDp0ZXN0"; // Base64 encoded test:test
+
+ let response_body = r#"{"name":"test"}"#;
+ let expected_status = 200;
+
+ let expected = "test";
+
+ let _serv = mockito::mock("GET", "/tenant/currentTenant")
+ .match_header(auth_header_field, auth_header_value)
+ .with_body(response_body)
+ .with_status(expected_status)
+ .create();
+
+ let res = get_tenant_id_blocking(&client, request_url, "test", "test").unwrap();
+
+ assert_eq!(res, expected);
+ }
+
+ #[test]
+ fn get_tenant_id_blocking_response_should_return_error_when_response_has_no_name_field() {
+ let client = reqwest::blocking::Client::new();
+
+ let request_url =
+ Url::parse(&format!("{}/tenant/currentTenant", mockito::server_url())).unwrap();
+
+ let auth_header_field = "authorization";
+ let auth_header_value = "Basic dGVzdDp0ZXN0"; // Base64 encoded test:test
+
+ let response_body = r#"{"test":"test"}"#;
+ let expected_status = 200;
+
+ let _serv = mockito::mock("GET", "/test/tenant/currentTenant")
+ .match_header(auth_header_field, auth_header_value)
+ .with_body(response_body)
+ .with_status(expected_status)
+ .create();
+
+ let res = get_tenant_id_blocking(&client, request_url, "test", "test");
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn make_upload_certificate_url_correct_parameters_return_url() {
+ let url = "test";
+ let tenant_id = "test";
+ let expected =
+ reqwest::Url::parse("https://test/tenant/tenants/test/trusted-certificates").unwrap();
+
+ let result = build_upload_certificate_url(url, tenant_id).unwrap();
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn make_upload_certificate_returns_error_given_incorrect_url() {
+ let url = "@";
+ let tenant_id = "test";
+
+ let result = build_upload_certificate_url(url, tenant_id).unwrap_err();
+ assert_matches!(result, CertError::UrlParseError(url::ParseError::EmptyHost));
+ }
+}