diff options
author | Lukasz Woznicki <75632179+makr11st@users.noreply.github.com> | 2021-11-24 20:54:56 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-24 20:54:56 +0000 |
commit | a4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch) | |
tree | 9583f187114913a92866571920dd3bb205bd50a3 /crates/core/tedge | |
parent | 8217e80670e76dbf9168780f5e0545355a39f8f3 (diff) |
Restructure directories of the workspace (#559)
* Restructure directories of the workspace
* Rename c8y_translator_lib to c8y_translator
* Update comment on how to get dummy plugin path
Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'crates/core/tedge')
53 files changed, 4981 insertions, 0 deletions
diff --git a/crates/core/tedge/Cargo.toml b/crates/core/tedge/Cargo.toml new file mode 100644 index 00000000..1b3c1456 --- /dev/null +++ b/crates/core/tedge/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "tedge" +version = "0.4.3" +edition = "2018" +authors = ["thin-edge.io team <info@thin-edge.io>"] +license = "Apache-2.0" +readme = "README.md" +description = "tedge is the cli tool for thin-edge.io" + +[package.metadata.deb] +depends = "mosquitto" +maintainer-scripts = "configuration/debian/tedge" + +[dependencies] +anyhow = "1.0" +certificate = { path = "../../common/certificate" } +chrono = "0.4" +futures = "0.3" +hyper = { version = "0.14", default-features = false } +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls", "stream"] } +rpassword = "5.0" +rumqttc = "0.10" +rustls = "0.19" +serde = { version = "1.0", features = ["derive"] } +structopt = "0.3" +tedge_config = { path = "../../common/tedge_config" } +tedge_users = { path = "../../common/tedge_users" } +tedge_utils = { path = "../../common/tedge_utils" } +thiserror = "1.0" +toml = "0.5" +url = "2.2" +webpki = "0.21" +which = "4.2" + +[dev-dependencies] +assert_cmd = "2.0" +assert_matches = "1.5" +mockito = "0.30" +pem = "1.0" +predicates = "2.0" +tempfile = "3.2" + +[features] +integration-test = [] +mosquitto-available = [] # Enable tests requesting mosquitto installed +root-access = [] # Enable tests requesting root access +tedge-user = [] # Enable tests requesting a tedge user +openrc = [] # Enable usage of OpenRC diff --git a/crates/core/tedge/README.md b/crates/core/tedge/README.md new file mode 100644 index 00000000..af155398 --- /dev/null +++ b/crates/core/tedge/README.md @@ -0,0 +1,23 @@ +# thin-edge-cli + +tedge 0.1.0 + +USAGE: + + tedge [FLAGS] <SUBCOMMAND> + +FLAGS: + + -h, --help Prints help information + -V, --version Prints version information + -v Verbose mode (-v, -vv, -vvv, etc.) + +SUBCOMMANDS: + + config Configure Thin Edge + help Prints this message or the help of the given subcommand(s) + +Examples: + + tedge --help + tedge config --help diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs new file mode 100644 index 00000000..7ef78fb5 --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -0,0 +1,87 @@ +use super::{create::CreateCertCmd, remove::RemoveCertCmd, show::ShowCertCmd, upload::*}; + +use crate::command::{BuildCommand, BuildContext, Command}; +use crate::ConfigError; + +use structopt::StructOpt; +use tedge_config::*; + +#[derive(StructOpt, Debug)] +pub enum TEdgeCertCli { + /// Create a self-signed device certificate + Create { + /// The device identifier to be used as the common name for the certificate + #[structopt(long = "device-id")] + id: String, + }, + + /// Show the device certificate, if any + Show, + + /// Remove the device certificate + Remove, + + /// Upload root certificate + Upload(UploadCertCli), +} + +impl BuildCommand for TEdgeCertCli { + fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> { + let config = context.config_repository.load()?; + + let cmd = match self { + TEdgeCertCli::Create { id } => { + let cmd = CreateCertCmd { + id, + cert_path: config.query(DeviceCertPathSetting)?, + key_path: config.query(DeviceKeyPathSetting)?, + user_manager: context.user_manager, + }; + cmd.into_boxed() + } + + TEdgeCertCli::Show => { + let cmd = ShowCertCmd { + cert_path: config.query(DeviceCertPathSetting)?, + }; + cmd.into_boxed() + } + + TEdgeCertCli::Remove => { + let cmd = RemoveCertCmd { + cert_path: config.query(DeviceCertPathSetting)?, + key_path: config.query(DeviceKeyPathSetting)?, + user_manager: context.user_manager, + }; + cmd.into_boxed() + } + + TEdgeCertCli::Upload(cmd) => { + let cmd = match cmd { + UploadCertCli::C8y { username } => UploadCertCmd { + device_id: config.query(DeviceIdSetting)?, + path: config.query(DeviceCertPathSetting)?, + host: config.query(C8yUrlSetting)?, + username, + }, + }; + cmd.into_boxed() + } + }; + + Ok(cmd) + } +} + +#[derive(StructOpt, Debug)] +pub enum UploadCertCli { + /// Upload root certificate to Cumulocity + /// + /// The command will upload root certificate to Cumulocity. + C8y { + #[structopt(long = "user")] + /// Provided username should be a Cumulocity user with tenant management permissions. + /// The password is requested on /dev/tty, unless the $C8YPASS env var is set to the user password. + username: String, + }, +} diff --git a/crates/core/tedge/src/cli/certificate/create.rs b/crates/core/tedge/src/cli/certificate/create.rs new file mode 100644 index 00000000..560c2a8f --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/create.rs @@ -0,0 +1,188 @@ +use super::error::CertError; +use crate::command::Command; +use certificate::{KeyCertPair, NewCertificateConfig}; +use std::{ + fs::{File, OpenOptions}, + io::prelude::*, + path::Path, +}; +use tedge_config::*; +use tedge_users::UserManager; +use tedge_utils::paths::{set_permission, validate_parent_dir_exists}; + +/// Create a self-signed device certificate +pub struct CreateCertCmd { + /// The device identifier + pub id: String, + + /// The path where the device certificate will be stored + pub cert_path: FilePath, + + /// The path where the device private key will be stored + pub key_path: FilePath, + + /// The UserManager required to change effective user id + pub user_manager: UserManager, +} + +impl Command for CreateCertCmd { + fn description(&self) -> String { + format!("create a test certificate for the device {}.", self.id) + } + + fn execute(&self) -> anyhow::Result<()> { + let config = NewCertificateConfig::default(); + let () = self.create_test_certificate(&config)?; + Ok(()) + } +} + +impl CreateCertCmd { + fn create_test_certificate(&self, config: &NewCertificateConfig) -> Result<(), CertError> { + let _user_guard = self.user_manager.become_user(tedge_users::BROKER_USER)?; + + validate_parent_dir_exists(&self.cert_path).map_err(CertError::CertPathError)?; + validate_parent_dir_exists(&self.key_path).map_err(CertError::KeyPathError)?; + + let cert = KeyCertPair::new_selfsigned_certificate(config, &self.id)?; + + // Creating files with permission 644 + let mut cert_file = create_new_file(&self.cert_path) + .map_err(|err| err.cert_context(self.cert_path.clone()))?; + let mut key_file = create_new_file(&self.key_path) + .map_err(|err| err.key_context(self.key_path.clone()))?; + + let cert_pem = cert.certificate_pem_string()?; + cert_file.write_all(cert_pem.as_bytes())?; + cert_file.sync_all()?; + + // Prevent the certificate to be overwritten + set_permission(&cert_file, 0o444)?; + + { + // Make sure the key is secret, before write + set_permission(&key_file, 0o600)?; + + // Zero the private key on drop + let cert_key = cert.private_key_pem_string()?; + key_file.write_all(cert_key.as_bytes())?; + key_file.sync_all()?; + + // Prevent the key to be overwritten + set_permission(&key_file, 0o400)?; + } + + Ok(()) + } +} + +fn create_new_file(path: impl AsRef<Path>) -> Result<File, CertError> { + Ok(OpenOptions::new().write(true).create_new(true).open(path)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use std::fs; + use tedge_users::UserManager; + use tempfile::*; + + #[test] + fn basic_usage() { + let dir = tempdir().unwrap(); + let cert_path = temp_file_path(&dir, "my-device-cert.pem"); + let key_path = temp_file_path(&dir, "my-device-key.pem"); + let id = "my-device-id"; + + let cmd = CreateCertCmd { + id: String::from(id), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + user_manager: UserManager::new(), + }; + + assert_matches!( + cmd.create_test_certificate(&NewCertificateConfig::default()), + Ok(()) + ); + assert_eq!(parse_pem_file(&cert_path).unwrap().tag, "CERTIFICATE"); + assert_eq!(parse_pem_file(&key_path).unwrap().tag, "PRIVATE KEY"); + } + + #[test] + fn check_certificate_is_not_overwritten() { + let dir = tempdir().unwrap(); + + let cert_path = temp_file_path(&dir, "my-device-cert.pem"); + let key_path = temp_file_path(&dir, "my-device-key.pem"); + + let cert_content = "some cert content"; + let key_content = "some key content"; + + fs::write(&cert_path, cert_content).unwrap(); + fs::write(&key_path, key_content).unwrap(); + + let cmd = CreateCertCmd { + id: "my-device-id".into(), + cert_path: cert_path.clone(), + key_path: key_path.clone(), + user_manager: UserManager::new(), + }; + + assert!(cmd + .create_test_certificate(&NewCertificateConfig::default()) + .ok() + .is_none()); + + assert_eq!(fs::read(&cert_path).unwrap(), cert_content.as_bytes()); + assert_eq!(fs::read(&key_path).unwrap(), key_content.as_bytes()); + } + + #[test] + fn create_certificate_in_non_existent_directory() { + let dir = tempdir().unwrap(); + let key_path = temp_file_path(&dir, "my-device-key.pem"); + let cert_path = FilePath::from("/non/existent/cert/path"); + + let cmd = CreateCertCmd { + id: "my-device-id".into(), + cert_path, + key_path, + user_manager: UserManager::new(), + }; + + let cert_error = cmd + .create_test_certificate(&NewCertificateConfig::default()) + .unwrap_err(); + assert_matches!(cert_error, CertError::CertPathError { .. }); + } + + #[test] + fn create_key_in_non_existent_directory() { + let dir = tempdir().unwrap(); + let cert_path = temp_file_path(&dir, "my-device-cert.pem"); + let key_path = FilePath::from("/non/existent/key/path"); + + let cmd = CreateCertCmd { + id: "my-device-id".into(), + cert_path, + key_path, + user_manager: UserManager::new(), + }; + + let cert_error = cmd + .create_test_certificate(&NewCertificateConfig::default()) + .unwrap_err(); + assert_matches!(cert_error, CertError::KeyPathError { .. }); + } + + fn temp_file_path(dir: &TempDir, filename: &str) -> FilePath { + dir.path().join(filename).into() + } + + fn parse_pem_file(path: impl AsRef<Path>) -> Result<pem::Pem, String> { + let content = fs::read(path).map_err(|err| err.to_string())?; + pem::parse(content).map_err(|err| err.to_string()) + } +} diff --git a/crates/core/tedge/src/cli/certificate/error.rs b/crates/core/tedge/src/cli/certificate/error.rs new file mode 100644 index 00000000..41e03c9a --- /dev/null +++ b/crates/core/tedge/src/cli/certificate/error.rs @@ -0,0 +1,172 @@ +use reqwest::StatusCode; +use std::error::Error; +use tedge_config::FilePath; +use tedge_users::UserSwitchError; +use tedge_utils::paths::PathsError; + +#[derive(thiserror::Error, Debug)] +pub enum CertError { + #[error( + r#"A certificate already exists and would be overwritten. + Existing file: "{path}" + Run `tedge cert remove` first to generate a new certificate. + "# + )] + CertificateAlreadyExists { path: FilePath }, + + #[error( + r#"No certificate has been attached to that device. + Missing file: {path:?} + Run `tedge cert create` to generate a new certificate. + "# + )] + CertificateNotFound { path: FilePath }, + + #[error( + r#"No private key has been attached to that device. + Missing file: {path:?} + Run `tedge cert create` to generate a new key and certificate. + "# + )] + KeyNotFound { path: FilePath }, + + #[error( + r#"A private key already exists and would be overwritten. + Existing file: {path:?} + Run `tedge cert remove` first to generate a new certificate and private key. + "# + )] + KeyAlreadyExists { path: FilePath }, + + #[error(transparent)] + ConfigError(#[from] crate::ConfigError), + + #[error("I/O error")] + IoError(#[from] std::io::Error), + + #[error("Invalid device.cert.path path: {0}")] + CertPathError(PathsError), + + #[error("Invalid device.key.path path: {0}")] + KeyPathError(PathsError), + + #[error(transparent)] + CertificateError(#[from] certificate::CertificateError), + + #[error( + r#"Certificate read error at: {1:?} + Run `tedge cert create` if you want to create a new certificate."# + )] + CertificateReadFailed(#[source] std::io::Error, String), + + #[error(transparent)] + PathsError(#[from] PathsError), + + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + + #[error("Request returned with code: {0}")] + StatusCode(StatusCode), + + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + + #[error(transparent)] + UserSwitchError(#[from] UserSwitchError), + + #[error("HTTP Connection Problem: {msg} \nHint: {hint}")] + WebpkiValidation { hint: String, msg: String }, +} + +impl CertError { + /// Improve the error message in case the error in a IO error on the certificate file. + pub fn cert_context(self, path: FilePath) -> CertError { + match self { + CertError::IoError(ref err) => match err.kind() { + std::io::ErrorKind::AlreadyExists => CertError::CertificateAlreadyExists { path }, + std::io::ErrorKind::NotFound => CertError::CertificateNotFound { path }, + _ => self, + }, + _ => self, + } + } + + /// Improve the error message in case the error in a IO error on the private key file. + pub fn key_context(self, path: FilePath) -> CertError { + match self { + CertError::IoError(ref err) => match err.kind() { + std::io::ErrorKind::AlreadyExists => CertError::KeyAlreadyExists { path }, + std::io::ErrorKind::NotFound => CertError::KeyNotFound { path }, + _ => self, + }, + _ => self, + } + } +} + +// Our sou |