From dc2733b47b22dfe69eb5431fbb91a5db6aa18f58 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Tue, 22 Oct 2024 00:30:42 +0200 Subject: [PATCH 01/13] Add basic auth implementation to c8y_api Signed-off-by: Rina Fujino --- Cargo.lock | 3 + crates/core/c8y_api/Cargo.toml | 2 + crates/core/c8y_api/src/http_proxy.rs | 103 +++++++++++++++--- crates/core/tedge_mapper/src/c8y/mapper.rs | 26 ++++- .../c8y_http_proxy/src/credentials.rs | 79 ++++---------- plugins/c8y_firmware_plugin/Cargo.toml | 1 + plugins/c8y_firmware_plugin/src/lib.rs | 8 +- plugins/c8y_remote_access_plugin/src/auth.rs | 19 ++-- plugins/c8y_remote_access_plugin/src/lib.rs | 9 +- plugins/c8y_remote_access_plugin/src/proxy.rs | 13 ++- 10 files changed, 160 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0280882af9..581bb4c7dd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,7 @@ name = "c8y-firmware-plugin" version = "1.3.1" dependencies = [ "anyhow", + "c8y_api", "c8y_firmware_manager", "c8y_http_proxy", "clap", @@ -662,6 +663,8 @@ dependencies = [ "anyhow", "assert-json-diff", "assert_matches", + "base64 0.13.1", + "camino", "clock", "csv", "download", diff --git a/crates/core/c8y_api/Cargo.toml b/crates/core/c8y_api/Cargo.toml index edb0d72be9f..8820f28295c 100644 --- a/crates/core/c8y_api/Cargo.toml +++ b/crates/core/c8y_api/Cargo.toml @@ -10,6 +10,8 @@ repository = { workspace = true } [dependencies] anyhow = { workspace = true } +base64 = { workspace = true } +camino = { workspace = true } clock = { workspace = true } csv = { workspace = true } download = { workspace = true } diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index cfc03dfb85a..e4fb32ef279 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -1,11 +1,14 @@ use crate::smartrest::error::SmartRestDeserializerError; use crate::smartrest::smartrest_deserializer::SmartRestJwtResponse; +use camino::Utf8PathBuf; use mqtt_channel::Connection; use mqtt_channel::PubChannel; use mqtt_channel::StreamExt; use mqtt_channel::Topic; use mqtt_channel::TopicFilter; use reqwest::header::HeaderMap; +use reqwest::header::HeaderValue; +use reqwest::header::InvalidHeaderValue; use reqwest::Url; use std::collections::HashMap; use std::time::Duration; @@ -131,53 +134,117 @@ impl C8yEndPoint { } } -pub struct C8yMqttJwtTokenRetriever { - mqtt_config: mqtt_channel::Config, +pub enum C8yAuthType { + JwtToken { + mqtt_config: Box, + }, + Basic { + credentials_path: Utf8PathBuf, + }, +} + +pub struct C8yAuthRetriever { + auth: C8yAuthType, topic_prefix: TopicPrefix, } #[derive(thiserror::Error, Debug)] -pub enum JwtRetrieverError { +pub enum C8yAuthRetrieverError { #[error(transparent)] MqttConfigBuild(#[from] MqttConfigBuildError), #[error(transparent)] ConfigMulti(#[from] MultiError), + #[error(transparent)] + JwtError(#[from] JwtError), + #[error(transparent)] + InvalidHeaderValue(#[from] InvalidHeaderValue), } -impl C8yMqttJwtTokenRetriever { +impl C8yAuthRetriever { pub fn from_tedge_config( tedge_config: &TEdgeConfig, c8y_profile: Option<&str>, - ) -> Result { - let mqtt_config = tedge_config - .mqtt_config() - .map_err(MqttConfigBuildError::from)?; - - Ok(Self::new( - mqtt_config, - tedge_config + ) -> Result { + // FIXME: replace me with tedge config + let use_basic_auth = true; + let credentials_path = Utf8PathBuf::from("/etc/tedge/c8y/.password"); + + let auth = if use_basic_auth { + C8yAuthType::Basic { credentials_path } + } else { + let mqtt_config = tedge_config + .mqtt_config() + .map_err(MqttConfigBuildError::from)?; + C8yAuthType::JwtToken { + mqtt_config: Box::new(mqtt_config), + } + }; + + Ok(Self { + auth, + topic_prefix: tedge_config .c8y .try_get(c8y_profile)? .bridge .topic_prefix .clone(), - )) + }) } - pub fn new(mqtt_config: mqtt_channel::Config, topic_prefix: TopicPrefix) -> Self { + pub fn new_with_jwt_auth(mqtt_config: mqtt_channel::Config, topic_prefix: TopicPrefix) -> Self { let topic = TopicFilter::new_unchecked(&format!("{topic_prefix}/s/dat")); let mqtt_config = mqtt_config .with_no_session() // Ignore any already published tokens, possibly stale. .with_subscriptions(topic); - C8yMqttJwtTokenRetriever { - mqtt_config, + Self { + auth: C8yAuthType::JwtToken { + mqtt_config: Box::new(mqtt_config), + }, + topic_prefix, + } + } + + pub fn new_with_basic_auth(credentials_path: Utf8PathBuf, topic_prefix: TopicPrefix) -> Self { + Self { + auth: C8yAuthType::Basic { credentials_path }, topic_prefix, } } - pub async fn get_jwt_token(&mut self) -> Result { - let mut mqtt_con = Connection::new(&self.mqtt_config).await?; + pub async fn get_auth_header_value(&mut self) -> Result { + let header_value = match &self.auth { + C8yAuthType::JwtToken { .. } => { + let jwt_token = self.get_jwt_token().await?; + format!("Bearer {}", jwt_token.token()).parse()? + } + C8yAuthType::Basic { .. } => { + let (username, password) = self.get_basic_auth().await?; + format!("Basic {}", base64::encode(format!("{username}:{password}"))).parse()? + } + }; + Ok(header_value) + } + + async fn get_basic_auth(&self) -> Result<(String, String), C8yAuthRetrieverError> { + let C8yAuthType::Basic { + credentials_path: _, + } = &self.auth + else { + panic!("This method should not be called if not intended to use Basic auth (username/password)."); + }; + // FIXME: async read credentials from a file + let username = "abc"; + let password = "1234"; + + Ok((username.into(), password.into())) + } + + async fn get_jwt_token(&mut self) -> Result { + let C8yAuthType::JwtToken { mqtt_config } = &self.auth else { + panic!("This method should not be called if not intended to use JWT token."); + }; + let mut mqtt_con = Connection::new(mqtt_config).await?; let pub_topic = format!("{}/s/uat", self.topic_prefix); tokio::time::sleep(Duration::from_millis(20)).await; diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index c2a626cefe1..57cbb2c8c12 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -2,8 +2,9 @@ use crate::core::component::TEdgeComponent; use crate::core::mapper::start_basic_actors; use anyhow::Context; use async_trait::async_trait; +use c8y_api::http_proxy::C8yAuthType; use c8y_auth_proxy::actor::C8yAuthProxyBuilder; -use c8y_http_proxy::credentials::C8YJwtRetriever; +use c8y_http_proxy::credentials::C8YHeaderRetriever; use c8y_http_proxy::C8YHttpConfig; use c8y_http_proxy::C8YHttpProxyBuilder; use c8y_mapper_ext::actor::C8yMapperBuilder; @@ -14,6 +15,7 @@ use c8y_mapper_ext::config::C8yMapperConfig; use c8y_mapper_ext::converter::CumulocityConverter; use mqtt_channel::Config; use std::borrow::Cow; +use std::path::PathBuf; use tedge_api::entity_store::EntityExternalId; use tedge_api::mqtt_topics::EntityTopicId; use tedge_config::ProfileName; @@ -185,14 +187,26 @@ impl TEdgeComponent for CumulocityMapper { ) .await?; } - let mut jwt_actor = - C8YJwtRetriever::builder(mqtt_config.clone(), c8y_config.bridge.topic_prefix.clone()); + // FIXME: replace hardcoded values by tedge config + let use_basic_auth = true; + let auth_type = if use_basic_auth { + let credentials_path = PathBuf::from("/etc/tedge/c8y/.credentials"); + C8yAuthType::Basic { + credentials_path: credentials_path.try_into().unwrap(), + } + } else { + C8yAuthType::JwtToken { + mqtt_config: Box::new(mqtt_config), + } + }; + let mut header_actor = + C8YHeaderRetriever::builder(auth_type, c8y_config.bridge.topic_prefix.clone()); let mut http_actor = HttpActor::new(&tedge_config).builder(); let c8y_http_config = C8YHttpConfig::try_new(&tedge_config, c8y_profile)?; let mut c8y_http_proxy_actor = - C8YHttpProxyBuilder::new(c8y_http_config, &mut http_actor, &mut jwt_actor); + C8YHttpProxyBuilder::new(c8y_http_config, &mut http_actor, &mut header_actor); let c8y_auth_proxy_actor = - C8yAuthProxyBuilder::try_from_config(&tedge_config, c8y_profile, &mut jwt_actor)?; + C8yAuthProxyBuilder::try_from_config(&tedge_config, c8y_profile, &mut header_actor)?; let mut fs_watch_actor = FsWatchActorBuilder::new(); let mut timer_actor = TimerActor::builder(); @@ -240,7 +254,7 @@ impl TEdgeComponent for CumulocityMapper { }; runtime.spawn(mqtt_actor).await?; - runtime.spawn(jwt_actor).await?; + runtime.spawn(header_actor).await?; runtime.spawn(http_actor).await?; runtime.spawn(c8y_http_proxy_actor).await?; runtime.spawn(c8y_auth_proxy_actor).await?; diff --git a/crates/extensions/c8y_http_proxy/src/credentials.rs b/crates/extensions/c8y_http_proxy/src/credentials.rs index 3cc031c8107..1fc95b4fdf6 100644 --- a/crates/extensions/c8y_http_proxy/src/credentials.rs +++ b/crates/extensions/c8y_http_proxy/src/credentials.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; -use c8y_api::http_proxy::C8yMqttJwtTokenRetriever; +use c8y_api::http_proxy::C8yAuthRetriever; +use c8y_api::http_proxy::C8yAuthType; use http::header::AUTHORIZATION; use http::HeaderMap; use tedge_actors::ClientMessageBox; @@ -15,80 +16,42 @@ pub type HttpHeaderResult = Result; /// Retrieves HTTP headers pub type HttpHeaderRetriever = ClientMessageBox; -/// A JwtRetriever that gets JWT tokens from C8Y over MQTT and returns authorization header -pub struct C8YJwtRetriever { - mqtt_retriever: C8yMqttJwtTokenRetriever, +/// An HTTP header retriever +pub struct C8YHeaderRetriever { + auth_retriever: C8yAuthRetriever, } -impl C8YJwtRetriever { +impl C8YHeaderRetriever { pub fn builder( - mqtt_config: mqtt_channel::Config, + auth: C8yAuthType, topic_prefix: TopicPrefix, - ) -> ServerActorBuilder { - let mqtt_retriever = C8yMqttJwtTokenRetriever::new(mqtt_config, topic_prefix); - let server = C8YJwtRetriever { mqtt_retriever }; - ServerActorBuilder::new(server, &ServerConfig::default(), Sequential) - } -} - -#[async_trait] -impl Server for C8YJwtRetriever { - type Request = HttpHeaderRequest; - type Response = HttpHeaderResult; - - fn name(&self) -> &str { - "C8YJwtRetriever" - } - - async fn handle(&mut self, _request: Self::Request) -> Self::Response { - let mut heeader_map = HeaderMap::new(); - let response = self.mqtt_retriever.get_jwt_token().await?; - heeader_map.insert( - AUTHORIZATION, - format!("Bearer {}", response.token()).parse()?, - ); - Ok(heeader_map) - } -} - -/// Return base64 encoded Basic Auth header -pub struct C8YBasicAuthRetriever { - username: String, - password: String, -} - -impl C8YBasicAuthRetriever { - pub fn builder( - username: &str, - password: &str, - ) -> ServerActorBuilder { - let server = C8YBasicAuthRetriever { - username: username.into(), - password: password.into(), + ) -> ServerActorBuilder { + let auth_retriever = match auth { + C8yAuthType::JwtToken { mqtt_config } => { + C8yAuthRetriever::new_with_jwt_auth(*mqtt_config, topic_prefix) + } + C8yAuthType::Basic { credentials_path } => { + C8yAuthRetriever::new_with_basic_auth(credentials_path, topic_prefix) + } }; + let server = C8YHeaderRetriever { auth_retriever }; ServerActorBuilder::new(server, &ServerConfig::default(), Sequential) } } #[async_trait] -impl Server for C8YBasicAuthRetriever { +impl Server for C8YHeaderRetriever { type Request = HttpHeaderRequest; type Response = HttpHeaderResult; fn name(&self) -> &str { - "C8YBasicAuthRetriever" + "C8YHeaderRetriever" } async fn handle(&mut self, _request: Self::Request) -> Self::Response { let mut header_map = HeaderMap::new(); - header_map.insert( - AUTHORIZATION, - format!( - "Basic {}", - base64::encode(format!("{}:{}", self.username, self.password)) - ) - .parse()?, - ); + let auth_value = self.auth_retriever.get_auth_header_value().await?; + header_map.insert(AUTHORIZATION, auth_value); Ok(header_map) } } @@ -96,7 +59,7 @@ impl Server for C8YBasicAuthRetriever { #[derive(thiserror::Error, Debug)] pub enum HttpHeaderError { #[error(transparent)] - JwtError(#[from] c8y_api::http_proxy::JwtError), + C8yAuthRetrieverError(#[from] c8y_api::http_proxy::C8yAuthRetrieverError), #[error(transparent)] InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), diff --git a/plugins/c8y_firmware_plugin/Cargo.toml b/plugins/c8y_firmware_plugin/Cargo.toml index a32a6067ed7..639c54db206 100644 --- a/plugins/c8y_firmware_plugin/Cargo.toml +++ b/plugins/c8y_firmware_plugin/Cargo.toml @@ -11,6 +11,7 @@ repository = { workspace = true } [dependencies] anyhow = { workspace = true } +c8y_api = { workspace = true } c8y_firmware_manager = { workspace = true } c8y_http_proxy = { workspace = true } clap = { workspace = true } diff --git a/plugins/c8y_firmware_plugin/src/lib.rs b/plugins/c8y_firmware_plugin/src/lib.rs index 1b19579acc3..e0714c0a82b 100644 --- a/plugins/c8y_firmware_plugin/src/lib.rs +++ b/plugins/c8y_firmware_plugin/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::Context; use c8y_firmware_manager::FirmwareManagerBuilder; use c8y_firmware_manager::FirmwareManagerConfig; -use c8y_http_proxy::credentials::C8YJwtRetriever; +use c8y_http_proxy::credentials::C8YHeaderRetriever; use std::path::PathBuf; use tedge_actors::Runtime; use tedge_api::mqtt_topics::DeviceTopicId; @@ -94,8 +94,10 @@ async fn run_with( // Create actor instances let mqtt_config = tedge_config.mqtt_config()?; - let mut jwt_actor = C8YJwtRetriever::builder( - mqtt_config.clone(), + let mut jwt_actor = C8YHeaderRetriever::builder( + c8y_api::http_proxy::C8yAuthType::JwtToken { + mqtt_config: Box::new(mqtt_config.clone()), + }, tedge_config .c8y .try_get(c8y_profile)? diff --git a/plugins/c8y_remote_access_plugin/src/auth.rs b/plugins/c8y_remote_access_plugin/src/auth.rs index 20facf2bd0b..138834012df 100644 --- a/plugins/c8y_remote_access_plugin/src/auth.rs +++ b/plugins/c8y_remote_access_plugin/src/auth.rs @@ -1,22 +1,23 @@ -use c8y_api::http_proxy::C8yMqttJwtTokenRetriever; +use c8y_api::http_proxy::C8yAuthRetriever; +use http::HeaderValue; use miette::IntoDiagnostic; use tedge_config::TEdgeConfig; -pub struct Jwt(String); +pub struct Auth(HeaderValue); -impl Jwt { - pub fn authorization_header(&self) -> String { - format!("Bearer {}", self.0) +impl Auth { + pub fn authorization_header(&self) -> HeaderValue { + self.0.clone() } - pub async fn retrieve(config: &TEdgeConfig, c8y_profile: Option<&str>) -> miette::Result { + pub async fn retrieve(config: &TEdgeConfig, c8y_profile: Option<&str>) -> miette::Result { let mut retriever = - C8yMqttJwtTokenRetriever::from_tedge_config(config, c8y_profile).into_diagnostic()?; + C8yAuthRetriever::from_tedge_config(config, c8y_profile).into_diagnostic()?; retriever - .get_jwt_token() + .get_auth_header_value() .await - .map(|resp| Jwt(resp.token())) + .map(Auth) .into_diagnostic() } } diff --git a/plugins/c8y_remote_access_plugin/src/lib.rs b/plugins/c8y_remote_access_plugin/src/lib.rs index e2f3d4d78be..85e48dcfe7b 100644 --- a/plugins/c8y_remote_access_plugin/src/lib.rs +++ b/plugins/c8y_remote_access_plugin/src/lib.rs @@ -18,7 +18,7 @@ use tokio::io::BufReader; use tokio::net::UnixStream; use url::Url; -use crate::auth::Jwt; +use crate::auth::Auth; pub use crate::input::C8yRemoteAccessPluginOpt; use crate::input::Command; use crate::input::RemoteAccessConnect; @@ -291,13 +291,12 @@ async fn proxy( .into_diagnostic()? .to_string(); let url = build_proxy_url(host.as_str(), command.key())?; - let jwt = Jwt::retrieve(&config, c8y_profile) - .await - .context("Failed when requesting JWT from Cumulocity")?; + let auth = Auth::retrieve(&config, c8y_profile) + .await.context("Failed when requesting JWT from Cumulocity or invalid username/password credentials are given")?; let client_config = config.cloud_client_tls_config(); let proxy = - WebsocketSocketProxy::connect(&url, command.target_address(), jwt, client_config).await?; + WebsocketSocketProxy::connect(&url, command.target_address(), auth, client_config).await?; proxy.run().await; Ok(()) diff --git a/plugins/c8y_remote_access_plugin/src/proxy.rs b/plugins/c8y_remote_access_plugin/src/proxy.rs index 25e6f02ac76..73cd65ba6e4 100644 --- a/plugins/c8y_remote_access_plugin/src/proxy.rs +++ b/plugins/c8y_remote_access_plugin/src/proxy.rs @@ -1,10 +1,11 @@ -use crate::auth::Jwt; +use crate::auth::Auth; use async_compat::CompatExt; use async_tungstenite::tokio::ConnectStream; use futures::future::join; use futures::future::select; use futures_util::io::AsyncReadExt; use futures_util::io::AsyncWriteExt; +use http::HeaderValue; use miette::Context; use miette::Diagnostic; use miette::IntoDiagnostic; @@ -35,11 +36,11 @@ impl WebsocketSocketProxy { pub async fn connect( url: &Url, socket: SA, - jwt: Jwt, + auth: Auth, config: ClientConfig, ) -> miette::Result { let socket_future = TcpStream::connect(socket); - let websocket_future = Websocket::new(url, jwt.authorization_header(), config); + let websocket_future = Websocket::new(url, auth.authorization_header(), config); match join(socket_future, websocket_future).await { (Err(socket_error), _) => Err(SocketError(socket_error))?, @@ -80,7 +81,11 @@ fn generate_sec_websocket_key() -> String { } impl Websocket { - async fn new(url: &Url, authorization: String, config: ClientConfig) -> miette::Result { + async fn new( + url: &Url, + authorization: HeaderValue, + config: ClientConfig, + ) -> miette::Result { let request = http::Request::builder() .header("Authorization", authorization) .header("Sec-WebSocket-Key", generate_sec_websocket_key()) From ee50ab531388f1c55a8463a124e656baaa2dc019 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Tue, 22 Oct 2024 01:42:17 +0200 Subject: [PATCH 02/13] Add basic auth related configuration and reading a credential file Signed-off-by: Rina Fujino --- .../src/tedge_config_cli/tedge_config.rs | 11 ++ crates/core/c8y_api/src/http_proxy.rs | 142 +++++++++--------- crates/core/tedge_mapper/src/c8y/mapper.rs | 19 +-- .../c8y_http_proxy/src/credentials.rs | 26 ++-- plugins/c8y_firmware_plugin/src/lib.rs | 12 +- 5 files changed, 100 insertions(+), 110 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index b5c981b5247..4bca545fdee 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -470,6 +470,17 @@ define_tedge_config! { #[doku(as = "PathBuf")] root_cert_path: Utf8PathBuf, + // TODO: Enum or bool? If enum, what are the other items? + /// Use basic authentication (username/password) instead of device certificate based authentication + #[tedge_config(example = "true", default(value = false))] + use_basic_auth: bool, + + /// The path where Cumulocity username/password are stored + #[tedge_config(note = "The value must be the path of the credentials file.")] + #[tedge_config(example = "/etc/tedge/credentials", default(value = "/etc/tedge/credentials"))] + #[doku(as = "PathBuf")] + credentials_path: Utf8PathBuf, + smartrest: { /// Set of SmartREST template IDs the device should subscribe to #[tedge_config(example = "templateId1,templateId2", default(function = "TemplatesSet::default"))] diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index e4fb32ef279..3e007e5caea 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -1,5 +1,6 @@ use crate::smartrest::error::SmartRestDeserializerError; use crate::smartrest::smartrest_deserializer::SmartRestJwtResponse; +use camino::Utf8Path; use camino::Utf8PathBuf; use mqtt_channel::Connection; use mqtt_channel::PubChannel; @@ -11,6 +12,7 @@ use reqwest::header::HeaderValue; use reqwest::header::InvalidHeaderValue; use reqwest::Url; use std::collections::HashMap; +use std::path::PathBuf; use std::time::Duration; use tedge_config::mqtt_config::MqttConfigBuildError; use tedge_config::MultiError; @@ -134,30 +136,55 @@ impl C8yEndPoint { } } -pub enum C8yAuthType { - JwtToken { - mqtt_config: Box, - }, +pub enum C8yAuthRetriever { Basic { credentials_path: Utf8PathBuf, }, + Jwt { + mqtt_config: Box, + topic_prefix: TopicPrefix, + }, } -pub struct C8yAuthRetriever { - auth: C8yAuthType, - topic_prefix: TopicPrefix, +/// The credential file representation. e.g.: +/// ```toml +/// [c8y] +/// username = "t1234/octocat" +/// password = "abcd1234" +/// ``` +#[derive(Debug, serde::Deserialize)] +struct Credentials { + c8y: BasicCredentials, +} + +#[derive(Debug, serde::Deserialize)] +struct BasicCredentials { + username: String, + password: String, } #[derive(thiserror::Error, Debug)] pub enum C8yAuthRetrieverError { #[error(transparent)] MqttConfigBuild(#[from] MqttConfigBuildError), + #[error(transparent)] ConfigMulti(#[from] MultiError), + #[error(transparent)] JwtError(#[from] JwtError), + #[error(transparent)] InvalidHeaderValue(#[from] InvalidHeaderValue), + + #[error("{context}: {source}")] + ReadCredentialsFailed { + context: String, + source: std::io::Error, + }, + + #[error("Error while parsing credentials file: '{0}': {1}.")] + TomlError(PathBuf, #[source] toml::de::Error), } impl C8yAuthRetriever { @@ -165,87 +192,68 @@ impl C8yAuthRetriever { tedge_config: &TEdgeConfig, c8y_profile: Option<&str>, ) -> Result { - // FIXME: replace me with tedge config - let use_basic_auth = true; - let credentials_path = Utf8PathBuf::from("/etc/tedge/c8y/.password"); + let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; + let topic_prefix = c8y_config.bridge.topic_prefix.clone(); - let auth = if use_basic_auth { - C8yAuthType::Basic { credentials_path } + if c8y_config.use_basic_auth { + Ok(Self::Basic { + credentials_path: c8y_config.credentials_path.clone(), + }) } else { let mqtt_config = tedge_config .mqtt_config() .map_err(MqttConfigBuildError::from)?; - C8yAuthType::JwtToken { - mqtt_config: Box::new(mqtt_config), - } - }; - - Ok(Self { - auth, - topic_prefix: tedge_config - .c8y - .try_get(c8y_profile)? - .bridge - .topic_prefix - .clone(), - }) - } - pub fn new_with_jwt_auth(mqtt_config: mqtt_channel::Config, topic_prefix: TopicPrefix) -> Self { - let topic = TopicFilter::new_unchecked(&format!("{topic_prefix}/s/dat")); - let mqtt_config = mqtt_config - .with_no_session() // Ignore any already published tokens, possibly stale. - .with_subscriptions(topic); + let topic = TopicFilter::new_unchecked(&format!("{topic_prefix}/s/dat")); + let mqtt_config = mqtt_config + .with_no_session() // Ignore any already published tokens, possibly stale. + .with_subscriptions(topic); - Self { - auth: C8yAuthType::JwtToken { + Ok(Self::Jwt { mqtt_config: Box::new(mqtt_config), - }, - topic_prefix, - } - } - - pub fn new_with_basic_auth(credentials_path: Utf8PathBuf, topic_prefix: TopicPrefix) -> Self { - Self { - auth: C8yAuthType::Basic { credentials_path }, - topic_prefix, + topic_prefix, + }) } } pub async fn get_auth_header_value(&mut self) -> Result { - let header_value = match &self.auth { - C8yAuthType::JwtToken { .. } => { - let jwt_token = self.get_jwt_token().await?; - format!("Bearer {}", jwt_token.token()).parse()? - } - C8yAuthType::Basic { .. } => { - let (username, password) = self.get_basic_auth().await?; + let header_value = match &self { + Self::Basic { credentials_path } => { + let BasicCredentials { username, password } = + Self::get_basic_auth(credentials_path).await?; format!("Basic {}", base64::encode(format!("{username}:{password}"))).parse()? } + Self::Jwt { + mqtt_config, + topic_prefix, + } => { + let jwt_token = Self::get_jwt_token(mqtt_config, topic_prefix).await?; + format!("Bearer {}", jwt_token.token()).parse()? + } }; Ok(header_value) } - async fn get_basic_auth(&self) -> Result<(String, String), C8yAuthRetrieverError> { - let C8yAuthType::Basic { - credentials_path: _, - } = &self.auth - else { - panic!("This method should not be called if not intended to use Basic auth (username/password)."); - }; - // FIXME: async read credentials from a file - let username = "abc"; - let password = "1234"; - - Ok((username.into(), password.into())) + async fn get_basic_auth( + credentials_path: &Utf8Path, + ) -> Result { + let contents = tokio::fs::read_to_string(credentials_path) + .await + .map_err(|e| C8yAuthRetrieverError::ReadCredentialsFailed { + context: "Failed to read the basic auth credentials file.".to_string(), + source: e, + })?; + let credentials: Credentials = toml::from_str(&contents) + .map_err(|e| C8yAuthRetrieverError::TomlError(credentials_path.into(), e))?; + Ok(credentials.c8y) } - async fn get_jwt_token(&mut self) -> Result { - let C8yAuthType::JwtToken { mqtt_config } = &self.auth else { - panic!("This method should not be called if not intended to use JWT token."); - }; + async fn get_jwt_token( + mqtt_config: &mqtt_channel::Config, + topic_prefix: &TopicPrefix, + ) -> Result { let mut mqtt_con = Connection::new(mqtt_config).await?; - let pub_topic = format!("{}/s/uat", self.topic_prefix); + let pub_topic = format!("{}/s/uat", topic_prefix); tokio::time::sleep(Duration::from_millis(20)).await; for _ in 0..3 { diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 57cbb2c8c12..d8239988022 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -2,7 +2,6 @@ use crate::core::component::TEdgeComponent; use crate::core::mapper::start_basic_actors; use anyhow::Context; use async_trait::async_trait; -use c8y_api::http_proxy::C8yAuthType; use c8y_auth_proxy::actor::C8yAuthProxyBuilder; use c8y_http_proxy::credentials::C8YHeaderRetriever; use c8y_http_proxy::C8YHttpConfig; @@ -15,7 +14,6 @@ use c8y_mapper_ext::config::C8yMapperConfig; use c8y_mapper_ext::converter::CumulocityConverter; use mqtt_channel::Config; use std::borrow::Cow; -use std::path::PathBuf; use tedge_api::entity_store::EntityExternalId; use tedge_api::mqtt_topics::EntityTopicId; use tedge_config::ProfileName; @@ -50,7 +48,6 @@ impl TEdgeComponent for CumulocityMapper { let (mut runtime, mut mqtt_actor) = start_basic_actors(&c8y_mapper_name, &tedge_config).await?; - let mqtt_config = tedge_config.mqtt_config()?; let c8y_mapper_config = C8yMapperConfig::from_tedge_config(cfg_dir, &tedge_config, c8y_profile)?; if tedge_config.mqtt.bridge.built_in { @@ -187,20 +184,8 @@ impl TEdgeComponent for CumulocityMapper { ) .await?; } - // FIXME: replace hardcoded values by tedge config - let use_basic_auth = true; - let auth_type = if use_basic_auth { - let credentials_path = PathBuf::from("/etc/tedge/c8y/.credentials"); - C8yAuthType::Basic { - credentials_path: credentials_path.try_into().unwrap(), - } - } else { - C8yAuthType::JwtToken { - mqtt_config: Box::new(mqtt_config), - } - }; - let mut header_actor = - C8YHeaderRetriever::builder(auth_type, c8y_config.bridge.topic_prefix.clone()); + + let mut header_actor = C8YHeaderRetriever::try_builder(&tedge_config, c8y_profile)?; let mut http_actor = HttpActor::new(&tedge_config).builder(); let c8y_http_config = C8YHttpConfig::try_new(&tedge_config, c8y_profile)?; let mut c8y_http_proxy_actor = diff --git a/crates/extensions/c8y_http_proxy/src/credentials.rs b/crates/extensions/c8y_http_proxy/src/credentials.rs index 1fc95b4fdf6..c42d24fd98e 100644 --- a/crates/extensions/c8y_http_proxy/src/credentials.rs +++ b/crates/extensions/c8y_http_proxy/src/credentials.rs @@ -1,6 +1,5 @@ use async_trait::async_trait; use c8y_api::http_proxy::C8yAuthRetriever; -use c8y_api::http_proxy::C8yAuthType; use http::header::AUTHORIZATION; use http::HeaderMap; use tedge_actors::ClientMessageBox; @@ -8,7 +7,7 @@ use tedge_actors::Sequential; use tedge_actors::Server; use tedge_actors::ServerActorBuilder; use tedge_actors::ServerConfig; -use tedge_config::TopicPrefix; +use tedge_config::TEdgeConfig; pub type HttpHeaderRequest = (); pub type HttpHeaderResult = Result; @@ -22,20 +21,17 @@ pub struct C8YHeaderRetriever { } impl C8YHeaderRetriever { - pub fn builder( - auth: C8yAuthType, - topic_prefix: TopicPrefix, - ) -> ServerActorBuilder { - let auth_retriever = match auth { - C8yAuthType::JwtToken { mqtt_config } => { - C8yAuthRetriever::new_with_jwt_auth(*mqtt_config, topic_prefix) - } - C8yAuthType::Basic { credentials_path } => { - C8yAuthRetriever::new_with_basic_auth(credentials_path, topic_prefix) - } - }; + pub fn try_builder( + config: &TEdgeConfig, + c8y_profile: Option<&str>, + ) -> Result, HttpHeaderError> { + let auth_retriever = C8yAuthRetriever::from_tedge_config(config, c8y_profile)?; let server = C8YHeaderRetriever { auth_retriever }; - ServerActorBuilder::new(server, &ServerConfig::default(), Sequential) + Ok(ServerActorBuilder::new( + server, + &ServerConfig::default(), + Sequential, + )) } } diff --git a/plugins/c8y_firmware_plugin/src/lib.rs b/plugins/c8y_firmware_plugin/src/lib.rs index e0714c0a82b..8a2136d8a33 100644 --- a/plugins/c8y_firmware_plugin/src/lib.rs +++ b/plugins/c8y_firmware_plugin/src/lib.rs @@ -94,17 +94,7 @@ async fn run_with( // Create actor instances let mqtt_config = tedge_config.mqtt_config()?; - let mut jwt_actor = C8YHeaderRetriever::builder( - c8y_api::http_proxy::C8yAuthType::JwtToken { - mqtt_config: Box::new(mqtt_config.clone()), - }, - tedge_config - .c8y - .try_get(c8y_profile)? - .bridge - .topic_prefix - .clone(), - ); + let mut jwt_actor = C8YHeaderRetriever::try_builder(&tedge_config, c8y_profile)?; let identity = tedge_config.http.client.auth.identity()?; let cloud_root_certs = tedge_config.cloud_root_certs(); let mut downloader_actor = DownloaderActor::new(identity, cloud_root_certs).builder(); From c296b62b75ecdc8679809a23238132a5d4b0318b Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Tue, 22 Oct 2024 22:48:06 +0200 Subject: [PATCH 03/13] Support username/password auth in MQTT bridge Signed-off-by: Rina Fujino --- Cargo.lock | 1 + crates/core/c8y_api/src/http_proxy.rs | 55 +++++----- crates/core/tedge/Cargo.toml | 1 + crates/core/tedge/src/bridge/aws.rs | 3 + crates/core/tedge/src/bridge/azure.rs | 3 + crates/core/tedge/src/bridge/c8y.rs | 106 ++++++++++++++++++- crates/core/tedge/src/bridge/config.rs | 92 +++++++++++++++- crates/core/tedge/src/cli/connect/command.rs | 11 ++ crates/core/tedge/src/error.rs | 3 + 9 files changed, 242 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 581bb4c7dd6..f2cee047420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3716,6 +3716,7 @@ dependencies = [ "base64 0.13.1", "c8y-firmware-plugin", "c8y-remote-access-plugin", + "c8y_api", "camino", "cap", "certificate", diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index 3e007e5caea..b3dd6f8611e 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -177,14 +177,8 @@ pub enum C8yAuthRetrieverError { #[error(transparent)] InvalidHeaderValue(#[from] InvalidHeaderValue), - #[error("{context}: {source}")] - ReadCredentialsFailed { - context: String, - source: std::io::Error, - }, - - #[error("Error while parsing credentials file: '{0}': {1}.")] - TomlError(PathBuf, #[source] toml::de::Error), + #[error(transparent)] + CredentialsFileError(#[from] CredentialsFileError), } impl C8yAuthRetriever { @@ -219,8 +213,7 @@ impl C8yAuthRetriever { pub async fn get_auth_header_value(&mut self) -> Result { let header_value = match &self { Self::Basic { credentials_path } => { - let BasicCredentials { username, password } = - Self::get_basic_auth(credentials_path).await?; + let (username, password) = read_c8y_credentials(credentials_path)?; format!("Basic {}", base64::encode(format!("{username}:{password}"))).parse()? } Self::Jwt { @@ -234,20 +227,6 @@ impl C8yAuthRetriever { Ok(header_value) } - async fn get_basic_auth( - credentials_path: &Utf8Path, - ) -> Result { - let contents = tokio::fs::read_to_string(credentials_path) - .await - .map_err(|e| C8yAuthRetrieverError::ReadCredentialsFailed { - context: "Failed to read the basic auth credentials file.".to_string(), - source: e, - })?; - let credentials: Credentials = toml::from_str(&contents) - .map_err(|e| C8yAuthRetrieverError::TomlError(credentials_path.into(), e))?; - Ok(credentials.c8y) - } - async fn get_jwt_token( mqtt_config: &mqtt_channel::Config, topic_prefix: &TopicPrefix, @@ -295,6 +274,34 @@ impl C8yAuthRetriever { } } +pub fn read_c8y_credentials( + credentials_path: &Utf8Path, +) -> Result<(String, String), CredentialsFileError> { + let contents = std::fs::read_to_string(credentials_path).map_err(|e| { + CredentialsFileError::ReadCredentialsFailed { + context: "Failed to read the basic auth credentials file.".to_string(), + source: e, + } + })?; + let credentials: Credentials = toml::from_str(&contents) + .map_err(|e| CredentialsFileError::TomlError(credentials_path.into(), e))?; + let BasicCredentials { username, password } = credentials.c8y; + + Ok((username, password)) +} + +#[derive(thiserror::Error, Debug)] +pub enum CredentialsFileError { + #[error("{context}: {source}")] + ReadCredentialsFailed { + context: String, + source: std::io::Error, + }, + + #[error("Error while parsing credentials file: '{0}': {1}.")] + TomlError(PathBuf, #[source] toml::de::Error), +} + #[derive(thiserror::Error, Debug)] pub enum JwtError { #[error(transparent)] diff --git a/crates/core/tedge/Cargo.toml b/crates/core/tedge/Cargo.toml index 5268c8fc3d6..b6f5762a5e2 100644 --- a/crates/core/tedge/Cargo.toml +++ b/crates/core/tedge/Cargo.toml @@ -16,6 +16,7 @@ anyhow = { workspace = true } base64 = { workspace = true } c8y-firmware-plugin = { workspace = true } c8y-remote-access-plugin = { workspace = true } +c8y_api = { workspace = true } camino = { workspace = true } cap = { workspace = true } certificate = { workspace = true, features = ["reqwest-blocking"] } diff --git a/crates/core/tedge/src/bridge/aws.rs b/crates/core/tedge/src/bridge/aws.rs index 66fa3d79b12..4c6e429128e 100644 --- a/crates/core/tedge/src/bridge/aws.rs +++ b/crates/core/tedge/src/bridge/aws.rs @@ -57,6 +57,7 @@ impl From for BridgeConfig { connection: "edge_to_aws".into(), address: mqtt_host, remote_username: Some(user_name), + remote_password: None, bridge_root_cert_path, remote_clientid, local_clientid: "Aws".into(), @@ -110,6 +111,7 @@ fn test_bridge_config_from_aws_params() -> anyhow::Result<()> { connection: "edge_to_aws".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("alpha".into()), + remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), local_clientid: "Aws".into(), @@ -163,6 +165,7 @@ fn test_bridge_config_aws_custom_topic_prefix() -> anyhow::Result<()> { connection: "edge_to_aws".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("alpha".into()), + remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), local_clientid: "Aws".into(), diff --git a/crates/core/tedge/src/bridge/azure.rs b/crates/core/tedge/src/bridge/azure.rs index 2aec7fb3acb..2ca21dfc442 100644 --- a/crates/core/tedge/src/bridge/azure.rs +++ b/crates/core/tedge/src/bridge/azure.rs @@ -49,6 +49,7 @@ impl From for BridgeConfig { connection: "edge_to_az".into(), address, remote_username: Some(user_name), + remote_password: None, bridge_root_cert_path, remote_clientid, local_clientid: "Azure".into(), @@ -107,6 +108,7 @@ fn test_bridge_config_from_azure_params() -> anyhow::Result<()> { connection: "edge_to_az".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("test.test.io/alpha/?api-version=2018-06-30".into()), + remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), local_clientid: "Azure".into(), @@ -164,6 +166,7 @@ fn test_azure_bridge_config_with_custom_prefix() -> anyhow::Result<()> { connection: "edge_to_az".into(), address: HostPort::::try_from("test.test.io")?, remote_username: Some("test.test.io/alpha/?api-version=2018-06-30".into()), + remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), local_clientid: "Azure".into(), diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index 045d8cbb8c9..5f242237208 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -17,6 +17,8 @@ pub struct BridgeConfigC8yParams { pub mqtt_host: HostPort, pub config_file: Cow<'static, str>, pub remote_clientid: String, + pub remote_username: Option, + pub remote_password: Option, pub bridge_root_cert_path: Utf8PathBuf, pub bridge_certfile: Utf8PathBuf, pub bridge_keyfile: Utf8PathBuf, @@ -32,6 +34,8 @@ impl From for BridgeConfig { mqtt_host, config_file, bridge_root_cert_path, + remote_username, + remote_password, remote_clientid, bridge_certfile, bridge_keyfile, @@ -66,11 +70,17 @@ impl From for BridgeConfig { format!(r#"alarm/alarms/create out 2 {topic_prefix}/ """#), format!(r#"devicecontrol/notifications in 2 {topic_prefix}/ """#), format!(r#"error in 2 {topic_prefix}/ """#), - // c8y JWT token retrieval - format!(r#"s/uat out 0 {topic_prefix}/ """#), - format!(r#"s/dat in 0 {topic_prefix}/ """#), ]; + let use_basic_auth = remote_username.is_some() && remote_password.is_some(); + if !use_basic_auth { + topics.extend(vec![ + // c8y JWT token retrieval + format!(r#"s/uat out 0 {topic_prefix}/ """#), + format!(r#"s/dat in 0 {topic_prefix}/ """#), + ]); + } + let templates_set = smartrest_templates .0 .iter() @@ -98,7 +108,8 @@ impl From for BridgeConfig { config_file, connection: "edge_to_c8y".into(), address: mqtt_host, - remote_username: None, + remote_username, + remote_password, bridge_root_cert_path, remote_clientid, local_clientid: "Cumulocity".into(), @@ -162,6 +173,8 @@ mod tests { mqtt_host: HostPort::::try_from("test.test.io")?, config_file: "c8y-bridge.conf".into(), remote_clientid: "alpha".into(), + remote_username: None, + remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), @@ -179,6 +192,7 @@ mod tests { connection: "edge_to_c8y".into(), address: HostPort::::try_from("test.test.io")?, remote_username: None, + remote_password: None, bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), remote_clientid: "alpha".into(), local_clientid: "Cumulocity".into(), @@ -239,4 +253,88 @@ mod tests { Ok(()) } + + #[test] + fn test_bridge_config_from_c8y_params_basic_auth() -> anyhow::Result<()> { + use std::convert::TryFrom; + let params = BridgeConfigC8yParams { + mqtt_host: HostPort::::try_from("test.test.io")?, + config_file: "c8y-bridge.conf".into(), + remote_clientid: "alpha".into(), + remote_username: Some("octocat".into()), + remote_password: Some("abcd1234".into()), + bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + smartrest_templates: TemplatesSet::try_from(vec!["abc", "def"])?, + include_local_clean_session: AutoFlag::False, + bridge_location: BridgeLocation::Mosquitto, + topic_prefix: "c8y".try_into().unwrap(), + }; + + let bridge = BridgeConfig::from(params); + + let expected = BridgeConfig { + cloud_name: "c8y".into(), + config_file: "c8y-bridge.conf".into(), + connection: "edge_to_c8y".into(), + address: HostPort::::try_from("test.test.io")?, + remote_username: Some("octocat".into()), + remote_password: Some("abcd1234".into()), + bridge_root_cert_path: Utf8PathBuf::from("./test_root.pem"), + remote_clientid: "alpha".into(), + local_clientid: "Cumulocity".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: true, + use_agent: true, + topics: vec![ + // Templates + r#"s/dt in 2 c8y/ """#.into(), + r#"s/ut/# out 2 c8y/ """#.into(), + // Static templates + r#"s/us/# out 2 c8y/ """#.into(), + r#"t/us/# out 2 c8y/ """#.into(), + r#"q/us/# out 2 c8y/ """#.into(), + r#"c/us/# out 2 c8y/ """#.into(), + r#"s/ds in 2 c8y/ """#.into(), + // Debug + r#"s/e in 0 c8y/ """#.into(), + // SmartRest2 + r#"s/uc/# out 2 c8y/ """#.into(), + r#"t/uc/# out 2 c8y/ """#.into(), + r#"q/uc/# out 2 c8y/ """#.into(), + r#"c/uc/# out 2 c8y/ """#.into(), + r#"s/dc/# in 2 c8y/ """#.into(), + // c8y JSON + r#"inventory/managedObjects/update/# out 2 c8y/ """#.into(), + r#"measurement/measurements/create out 2 c8y/ """#.into(), + r#"event/events/create out 2 c8y/ """#.into(), + r#"alarm/alarms/create out 2 c8y/ """#.into(), + r#"devicecontrol/notifications in 2 c8y/ """#.into(), + r#"error in 2 c8y/ """#.into(), + // Important: no c8y JWT token topics! + // SmartRest2 custom templates + r#"s/uc/abc out 2 c8y/ """#.into(), + r#"s/dc/abc in 2 c8y/ """#.into(), + r#"s/uc/def out 2 c8y/ """#.into(), + r#"s/dc/def in 2 c8y/ """#.into(), + ], + try_private: false, + start_type: "automatic".into(), + clean_session: true, + include_local_clean_session: false, + local_clean_session: false, + notifications: true, + notifications_local_only: true, + notification_topic: C8Y_BRIDGE_HEALTH_TOPIC.into(), + bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, + connection_check_attempts: 1, + }; + + assert_eq!(bridge, expected); + + Ok(()) + } } diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index 4f37863cab5..194c4410651 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -18,6 +18,7 @@ pub struct BridgeConfig { pub connection: String, pub address: HostPort, pub remote_username: Option, + pub remote_password: Option, pub bridge_root_cert_path: Utf8PathBuf, pub remote_clientid: String, pub local_clientid: String, @@ -49,9 +50,6 @@ impl BridgeConfig { pub fn serialize(&self, writer: &mut W) -> std::io::Result<()> { writeln!(writer, "### Bridge")?; writeln!(writer, "connection {}", self.connection)?; - if let Some(name) = &self.remote_username { - writeln!(writer, "remote_username {}", name)?; - } writeln!(writer, "address {}", self.address)?; if std::fs::metadata(&self.bridge_root_cert_path)?.is_dir() { @@ -62,8 +60,20 @@ impl BridgeConfig { writeln!(writer, "remote_clientid {}", self.remote_clientid)?; writeln!(writer, "local_clientid {}", self.local_clientid)?; - writeln!(writer, "bridge_certfile {}", self.bridge_certfile)?; - writeln!(writer, "bridge_keyfile {}", self.bridge_keyfile)?; + + if let Some(name) = &self.remote_username { + writeln!(writer, "remote_username {}", name)?; + } + let use_basic_auth = self.remote_username.is_some() && self.remote_password.is_some(); + if use_basic_auth { + if let Some(password) = &self.remote_password { + writeln!(writer, "remote_password {}", password)?; + } + } else { + writeln!(writer, "bridge_certfile {}", self.bridge_certfile)?; + writeln!(writer, "bridge_keyfile {}", self.bridge_keyfile)?; + } + writeln!(writer, "try_private {}", self.try_private)?; writeln!(writer, "start_type {}", self.start_type)?; writeln!(writer, "cleansession {}", self.clean_session)?; @@ -155,6 +165,7 @@ mod test { connection: "edge_to_test".into(), address: HostPort::::try_from("test.test.io:8883")?, remote_username: None, + remote_password: None, bridge_root_cert_path: bridge_root_cert_path.to_owned(), remote_clientid: "alpha".into(), local_clientid: "test".into(), @@ -222,6 +233,7 @@ bridge_attempt_unsubscribe false connection: "edge_to_test".into(), address: HostPort::::try_from("test.test.io:8883")?, remote_username: None, + remote_password: None, bridge_root_cert_path: bridge_root_cert_path.to_owned(), remote_clientid: "alpha".into(), local_clientid: "test".into(), @@ -288,6 +300,7 @@ bridge_attempt_unsubscribe false connection: "edge_to_az".into(), address: HostPort::::try_from("test.test.io:8883")?, remote_username: Some("test.test.io/alpha/?api-version=2018-06-30".into()), + remote_password: None, bridge_root_cert_path: bridge_root_cert_path.to_owned(), remote_clientid: "alpha".into(), local_clientid: "Azure".into(), @@ -346,6 +359,74 @@ bridge_attempt_unsubscribe false Ok(()) } + #[test] + fn test_serialize_use_basic_auth() -> anyhow::Result<()> { + let file = tempfile::NamedTempFile::new()?; + let bridge_root_cert_path = Utf8Path::from_path(file.path()).unwrap(); + + let config = BridgeConfig { + cloud_name: "c8y".into(), + config_file: "c8y-bridge.conf".into(), + connection: "edge_to_c8y".into(), + address: HostPort::::try_from("test.test.io:8883")?, + remote_username: Some("octocat".into()), + remote_password: Some("pass1234".into()), + bridge_root_cert_path: bridge_root_cert_path.to_owned(), + remote_clientid: "alpha".into(), + local_clientid: "C8Y".into(), + bridge_certfile: "./test-certificate.pem".into(), + bridge_keyfile: "./test-private-key.pem".into(), + use_mapper: false, + use_agent: false, + topics: vec![ + r#"inventory/managedObjects/update/# out 2 c8y/ """#.into(), + r#"measurement/measurements/create out 2 c8y/ """#.into(), + ], + try_private: false, + start_type: "automatic".into(), + clean_session: true, + include_local_clean_session: true, + local_clean_session: true, + notifications: false, + notifications_local_only: false, + notification_topic: "test_topic".into(), + bridge_attempt_unsubscribe: false, + bridge_location: BridgeLocation::Mosquitto, + connection_check_attempts: 1, + }; + + let mut buffer = Vec::new(); + config.serialize(&mut buffer)?; + + let contents = String::from_utf8(buffer)?; + let config_set: std::collections::HashSet<&str> = contents + .lines() + .filter(|str| !str.is_empty() && !str.starts_with('#')) + .collect(); + + let mut expected = std::collections::HashSet::new(); + expected.insert("connection edge_to_c8y"); + expected.insert("remote_username octocat"); + expected.insert("remote_password pass1234"); + expected.insert("address test.test.io:8883"); + let bridge_capath = format!("bridge_cafile {}", bridge_root_cert_path); + expected.insert(&bridge_capath); + expected.insert("remote_clientid alpha"); + expected.insert("local_clientid C8Y"); + expected.insert("start_type automatic"); + expected.insert("try_private false"); + expected.insert("cleansession true"); + expected.insert("local_cleansession true"); + expected.insert("notifications false"); + expected.insert("notifications_local_only false"); + expected.insert("notification_topic test_topic"); + expected.insert("bridge_attempt_unsubscribe false"); + expected.insert(r#"topic inventory/managedObjects/update/# out 2 c8y/ """#); + expected.insert(r#"topic measurement/measurements/create out 2 c8y/ """#); + assert_eq!(config_set, expected); + Ok(()) + } + #[test] fn test_validate_ok() -> anyhow::Result<()> { let ca_file = tempfile::NamedTempFile::new()?; @@ -409,6 +490,7 @@ bridge_attempt_unsubscribe false connection: "edge_to_az/c8y".into(), address: HostPort::::from_str("test.com").unwrap(), remote_username: None, + remote_password: None, bridge_root_cert_path: "".into(), bridge_certfile: "".into(), bridge_keyfile: "".into(), diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index ea90af9d6d2..33d6243ed85 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -9,6 +9,7 @@ use crate::cli::connect::jwt_token::*; use crate::cli::connect::*; use crate::command::Command; use crate::ConfigError; +use c8y_api::http_proxy::read_c8y_credentials; use camino::Utf8PathBuf; use rumqttc::Event; use rumqttc::Incoming; @@ -263,11 +264,21 @@ pub fn bridge_config( } Cloud::C8y => { let c8y_config = config.c8y.try_get(profile)?; + + let (remote_username, remote_password) = if c8y_config.use_basic_auth { + let (username, password) = read_c8y_credentials(&c8y_config.credentials_path)?; + (Some(username), Some(password)) + } else { + (None, None) + }; + let params = BridgeConfigC8yParams { mqtt_host: c8y_config.mqtt.or_config_not_set()?.clone(), config_file: Cloud::C8y.bridge_config_filename(profile), bridge_root_cert_path: c8y_config.root_cert_path.clone(), remote_clientid: config.device.id.try_read(config)?.clone(), + remote_username, + remote_password, bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), smartrest_templates: c8y_config.smartrest.templates.clone(), diff --git a/crates/core/tedge/src/error.rs b/crates/core/tedge/src/error.rs index f9958f98757..874734db126 100644 --- a/crates/core/tedge/src/error.rs +++ b/crates/core/tedge/src/error.rs @@ -35,4 +35,7 @@ pub enum TEdgeError { #[error(transparent)] FromMultiError(#[from] MultiError), + + #[error(transparent)] + FromCredentialsFileError(#[from] c8y_api::http_proxy::CredentialsFileError), } From c9c33c6f28984f264d01973b424a84476d4b94a9 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Tue, 22 Oct 2024 23:29:18 +0200 Subject: [PATCH 04/13] Support username/password auth in MQTT connection Signed-off-by: Rina Fujino --- .../certificate/src/parse_root_certificate.rs | 11 +++++ crates/core/tedge/src/bridge/config.rs | 20 +++++---- .../src/cli/connect/c8y_direct_connection.rs | 26 +++++++++--- crates/core/tedge/src/cli/connect/command.rs | 15 ++++++- crates/core/tedge_mapper/src/c8y/mapper.rs | 41 +++++++++++++++---- .../tedge_mqtt_bridge/src/config.rs | 13 ++++++ 6 files changed, 101 insertions(+), 25 deletions(-) diff --git a/crates/common/certificate/src/parse_root_certificate.rs b/crates/common/certificate/src/parse_root_certificate.rs index 126a1a6598d..72681467359 100644 --- a/crates/common/certificate/src/parse_root_certificate.rs +++ b/crates/common/certificate/src/parse_root_certificate.rs @@ -68,6 +68,17 @@ where .with_no_client_auth()) } +pub fn create_tls_config_without_client_cert( + root_certificates: impl AsRef, +) -> Result { + let root_cert_store = new_root_store(root_certificates.as_ref())?; + + Ok(ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_cert_store) + .with_no_client_auth()) +} + pub fn add_certs_from_file( root_store: &mut RootCertStore, cert_file: impl AsRef, diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index 194c4410651..d5c11c04807 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -101,17 +101,19 @@ impl BridgeConfig { Ok(()) } - pub fn validate(&self) -> Result<(), ConnectError> { + pub fn validate(&self, use_basic_auth: bool) -> Result<(), ConnectError> { if !self.bridge_root_cert_path.exists() { return Err(ConnectError::Certificate); } - if !self.bridge_certfile.exists() { - return Err(ConnectError::Certificate); - } + if !use_basic_auth { + if !self.bridge_certfile.exists() { + return Err(ConnectError::Certificate); + } - if !self.bridge_keyfile.exists() { - return Err(ConnectError::Certificate); + if !self.bridge_keyfile.exists() { + return Err(ConnectError::Certificate); + } } Ok(()) @@ -446,7 +448,7 @@ bridge_attempt_unsubscribe false ..default_bridge_config() }; - assert!(config.validate().is_ok()); + assert!(config.validate(false).is_ok()); Ok(()) } @@ -462,7 +464,7 @@ bridge_attempt_unsubscribe false ..default_bridge_config() }; - assert!(config.validate().is_err()); + assert!(config.validate(false).is_err()); } #[test] @@ -478,7 +480,7 @@ bridge_attempt_unsubscribe false ..default_bridge_config() }; - assert!(config.validate().is_err()); + assert!(config.validate(false).is_err()); Ok(()) } diff --git a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs index ad5ace5eb77..7f556d1f56d 100644 --- a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs +++ b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs @@ -2,6 +2,7 @@ use super::ConnectError; use crate::bridge::BridgeConfig; use crate::cli::connect::CONNECTION_TIMEOUT; use certificate::parse_root_certificate::create_tls_config; +use certificate::parse_root_certificate::create_tls_config_without_client_cert; use rumqttc::tokio_rustls::rustls::AlertDescription; use rumqttc::tokio_rustls::rustls::CertificateError; use rumqttc::tokio_rustls::rustls::Error; @@ -18,6 +19,7 @@ use rumqttc::Transport; // Connect directly to the c8y cloud over mqtt and publish device create message. pub fn create_device_with_direct_connection( + use_basic_auth: bool, bridge_config: &BridgeConfig, device_type: &str, ) -> Result<(), ConnectError> { @@ -33,11 +35,25 @@ pub fn create_device_with_direct_connection( ); mqtt_options.set_keep_alive(std::time::Duration::from_secs(5)); - let tls_config = create_tls_config( - &bridge_config.bridge_root_cert_path, - &bridge_config.bridge_keyfile, - &bridge_config.bridge_certfile, - )?; + let tls_config = if use_basic_auth { + mqtt_options.set_credentials( + bridge_config + .remote_username + .clone() + .expect("username must be set to use basic auth"), + bridge_config + .remote_password + .clone() + .expect("password must be set to use basic auth"), + ); + create_tls_config_without_client_cert(&bridge_config.bridge_root_cert_path)? + } else { + create_tls_config( + &bridge_config.bridge_root_cert_path, + &bridge_config.bridge_keyfile, + &bridge_config.bridge_certfile, + )? + }; mqtt_options.set_transport(Transport::tls_with_config(tls_config.into())); let (mut client, mut connection) = Client::new(mqtt_options, 10); diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 33d6243ed85..b80d1d1f6f4 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -298,7 +298,14 @@ fn check_device_status_c8y( tedge_config: &TEdgeConfig, c8y_profile: Option<&ProfileName>, ) -> Result { - let prefix = &tedge_config.c8y.try_get(c8y_profile)?.bridge.topic_prefix; + let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; + + // TODO: Use SmartREST1 to check connection + if c8y_config.use_basic_auth { + return Ok(DeviceStatus::AlreadyExists); + } + + let prefix = c8y_config.bridge.topic_prefix.clone(); let c8y_topic_builtin_jwt_token_downstream = format!("{prefix}/s/dat"); let c8y_topic_builtin_jwt_token_upstream = format!("{prefix}/s/uat"); const CLIENT_ID: &str = "check_connection_c8y"; @@ -587,8 +594,11 @@ fn new_bridge( bridge_config_exists(config_location, bridge_config)?; } + let use_basic_auth = + bridge_config.remote_username.is_some() && bridge_config.remote_password.is_some(); + println!("Validating the bridge certificates.\n"); - bridge_config.validate()?; + bridge_config.validate(use_basic_auth)?; if bridge_config.cloud_name.eq("c8y") { if offline_mode { @@ -596,6 +606,7 @@ fn new_bridge( } else { println!("Creating the device in Cumulocity cloud.\n"); c8y_direct_connection::create_device_with_direct_connection( + use_basic_auth, bridge_config, device_type, )?; diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index d8239988022..5325cc762f7 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -2,6 +2,7 @@ use crate::core::component::TEdgeComponent; use crate::core::mapper::start_basic_actors; use anyhow::Context; use async_trait::async_trait; +use c8y_api::http_proxy::read_c8y_credentials; use c8y_auth_proxy::actor::C8yAuthProxyBuilder; use c8y_http_proxy::credentials::C8YHeaderRetriever; use c8y_http_proxy::C8YHttpConfig; @@ -22,6 +23,7 @@ use tedge_downloader_ext::DownloaderActor; use tedge_file_system_ext::FsWatchActorBuilder; use tedge_http_ext::HttpActor; use tedge_mqtt_bridge::rumqttc::LastWill; +use tedge_mqtt_bridge::use_credentials; use tedge_mqtt_bridge::use_key_and_cert; use tedge_mqtt_bridge::BridgeConfig; use tedge_mqtt_bridge::MqttBridgeActorBuilder; @@ -59,15 +61,21 @@ impl TEdgeComponent for CumulocityMapper { .map(|id| Cow::Owned(format!("s/dc/{id}"))); let cloud_topics = [ - "s/dt", - "s/dat", - "s/ds", - "s/e", - "devicecontrol/notifications", - "error", + ("s/dt", true), + ("s/ds", true), + ("s/dat", !c8y_config.use_basic_auth), + ("s/e", true), + ("devicecontrol/notifications", true), + ("error", true), ] .into_iter() - .map(Cow::Borrowed) + .filter_map(|(topic, active)| { + if active { + Some(Cow::Borrowed(topic)) + } else { + None + } + }) .chain(custom_topics); let mut tc = BridgeConfig::new(); @@ -105,7 +113,11 @@ impl TEdgeComponent for CumulocityMapper { )?; tc.forward_from_local("event/events/create/#", local_prefix.clone(), "")?; tc.forward_from_local("alarm/alarms/create/#", local_prefix.clone(), "")?; - tc.forward_from_local("s/uat", local_prefix.clone(), "")?; + + // JWT token + if !c8y_config.use_basic_auth { + tc.forward_from_local("s/uat", local_prefix.clone(), "")?; + } let c8y = c8y_config.mqtt.or_config_not_set()?; let mut cloud_config = tedge_mqtt_bridge::MqttOptions::new( @@ -116,7 +128,18 @@ impl TEdgeComponent for CumulocityMapper { // Cumulocity tells us not to not set clean session to false, so don't // https://cumulocity.com/docs/device-integration/mqtt/#mqtt-clean-session cloud_config.set_clean_session(true); - use_key_and_cert(&mut cloud_config, &c8y_config.root_cert_path, &tedge_config)?; + + if c8y_config.use_basic_auth { + let (username, password) = read_c8y_credentials(&c8y_config.credentials_path)?; + use_credentials( + &mut cloud_config, + &c8y_config.root_cert_path, + username, + password, + )?; + } else { + use_key_and_cert(&mut cloud_config, &c8y_config.root_cert_path, &tedge_config)?; + } let main_device_xid: EntityExternalId = tedge_config.device.id.try_read(&tedge_config)?.into(); diff --git a/crates/extensions/tedge_mqtt_bridge/src/config.rs b/crates/extensions/tedge_mqtt_bridge/src/config.rs index d3c882f79fa..f5e66668f7d 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/config.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/config.rs @@ -1,6 +1,7 @@ use crate::topics::matches_ignore_dollar_prefix; use crate::topics::TopicConverter; use certificate::parse_root_certificate::create_tls_config; +use certificate::parse_root_certificate::create_tls_config_without_client_cert; use rumqttc::valid_filter; use rumqttc::valid_topic; use rumqttc::MqttOptions; @@ -23,6 +24,18 @@ pub fn use_key_and_cert( Ok(()) } +pub fn use_credentials( + config: &mut MqttOptions, + root_cert_path: impl AsRef, + username: String, + password: String, +) -> anyhow::Result<()> { + let tls_config = create_tls_config_without_client_cert(root_cert_path)?; + config.set_transport(Transport::tls_with_config(tls_config.into())); + config.set_credentials(username, password); + Ok(()) +} + #[derive(Default, Debug, Clone)] pub struct BridgeConfig { local_to_remote: Vec, From 28dee964980eb48e2edf6bdfad613d855c0bd6ac Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Wed, 23 Oct 2024 00:13:34 +0200 Subject: [PATCH 05/13] Support SmartREST1.0 Signed-off-by: Rina Fujino --- .../src/tedge_config_cli/tedge_config.rs | 5 ++ crates/core/tedge/src/bridge/c8y.rs | 47 +++++++++++++++++++ crates/core/tedge/src/cli/connect/command.rs | 1 + crates/core/tedge_mapper/src/c8y/mapper.rs | 18 ++++++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index 4bca545fdee..e23df89a79e 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -491,6 +491,11 @@ define_tedge_config! { use_operation_id: bool, }, + smartrest1: { + /// Set of SmartREST 1.0 template IDs the device should subscribe to + #[tedge_config(example = "templateId1,templateId2", default(function = "TemplatesSet::default"))] + templates: TemplatesSet, + }, /// HTTP Endpoint for the Cumulocity tenant, with optional port. #[tedge_config(example = "http.your-tenant.cumulocity.com:1234")] diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index 5f242237208..3b2e143a2fc 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -23,6 +23,7 @@ pub struct BridgeConfigC8yParams { pub bridge_certfile: Utf8PathBuf, pub bridge_keyfile: Utf8PathBuf, pub smartrest_templates: TemplatesSet, + pub smartrest_one_templates: TemplatesSet, pub include_local_clean_session: AutoFlag, pub bridge_location: BridgeLocation, pub topic_prefix: TopicPrefix, @@ -40,6 +41,7 @@ impl From for BridgeConfig { bridge_certfile, bridge_keyfile, smartrest_templates, + smartrest_one_templates, include_local_clean_session, bridge_location, topic_prefix, @@ -97,6 +99,38 @@ impl From for BridgeConfig { .collect::>(); topics.extend(templates_set); + // These topics are somehow required to receive operations when using Basic auth + let use_smartrest_one = use_basic_auth; + if use_smartrest_one { + topics.extend([ + format!(r#"s/ul/# out 2 {topic_prefix}/ """#), + format!(r#"t/ul/# out 2 {topic_prefix}/ """#), + format!(r#"q/ul/# out 2 {topic_prefix}/ """#), + format!(r#"c/ul/# out 2 {topic_prefix}/ """#), + format!(r#"s/dl/# in 2 {topic_prefix}/ """#), + ]); + } + + // SmartRest1 (to support customers with existing solutions based on SmartRest 1) + // Only add the topics if at least 1 template is defined + if !smartrest_one_templates.0.is_empty() { + let templates_set = smartrest_one_templates + .0 + .iter() + .flat_map(|s| { + // SmartRest1 templates should be deserialized as: + // c8y/s/ul/template-1 (in from localhost), s/ul/template-1 + // c8y/s/dl/template-1 (out to localhost), s/dl/template-1 + [ + format!(r#"s/ul/{s} out 2 {topic_prefix}/ """#), + format!(r#"s/dl/{s} in 2 {topic_prefix}/ """#), + ] + .into_iter() + }) + .collect::>(); + topics.extend(templates_set); + } + let include_local_clean_session = match include_local_clean_session { AutoFlag::True => true, AutoFlag::False => false, @@ -179,6 +213,7 @@ mod tests { bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), smartrest_templates: TemplatesSet::try_from(vec!["abc", "def"])?, + smartrest_one_templates: TemplatesSet::default(), include_local_clean_session: AutoFlag::False, bridge_location: BridgeLocation::Mosquitto, topic_prefix: "c8y".try_into().unwrap(), @@ -267,6 +302,7 @@ mod tests { bridge_certfile: "./test-certificate.pem".into(), bridge_keyfile: "./test-private-key.pem".into(), smartrest_templates: TemplatesSet::try_from(vec!["abc", "def"])?, + smartrest_one_templates: TemplatesSet::try_from(vec!["legacy1", "legacy2"])?, include_local_clean_session: AutoFlag::False, bridge_location: BridgeLocation::Mosquitto, topic_prefix: "c8y".try_into().unwrap(), @@ -319,6 +355,17 @@ mod tests { r#"s/dc/abc in 2 c8y/ """#.into(), r#"s/uc/def out 2 c8y/ """#.into(), r#"s/dc/def in 2 c8y/ """#.into(), + // SmartREST 1.0 topics + r#"s/ul/# out 2 c8y/ """#.into(), + r#"t/ul/# out 2 c8y/ """#.into(), + r#"q/ul/# out 2 c8y/ """#.into(), + r#"c/ul/# out 2 c8y/ """#.into(), + r#"s/dl/# in 2 c8y/ """#.into(), + // SmartREST 1.0 custom templates + r#"s/ul/legacy1 out 2 c8y/ """#.into(), + r#"s/dl/legacy1 in 2 c8y/ """#.into(), + r#"s/ul/legacy2 out 2 c8y/ """#.into(), + r#"s/dl/legacy2 in 2 c8y/ """#.into(), ], try_private: false, start_type: "automatic".into(), diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index b80d1d1f6f4..57fc2b60545 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -282,6 +282,7 @@ pub fn bridge_config( bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), smartrest_templates: c8y_config.smartrest.templates.clone(), + smartrest_one_templates: c8y_config.smartrest1.templates.clone(), include_local_clean_session: c8y_config.bridge.include.local_cleansession.clone(), bridge_location, topic_prefix: c8y_config.bridge.topic_prefix.clone(), diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 5325cc762f7..a1dc4a0f742 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -53,7 +53,14 @@ impl TEdgeComponent for CumulocityMapper { let c8y_mapper_config = C8yMapperConfig::from_tedge_config(cfg_dir, &tedge_config, c8y_profile)?; if tedge_config.mqtt.bridge.built_in { - let custom_topics = c8y_config + let smartrest_1_topics = c8y_config + .smartrest1 + .templates + .0 + .iter() + .map(|id| Cow::Owned(format!("s/dl/{id}"))); + + let smartrest_2_topics = c8y_config .smartrest .templates .0 @@ -76,7 +83,8 @@ impl TEdgeComponent for CumulocityMapper { None } }) - .chain(custom_topics); + .chain(smartrest_1_topics) + .chain(smartrest_2_topics); let mut tc = BridgeConfig::new(); let local_prefix = format!("{}/", c8y_config.bridge.topic_prefix.as_str()); @@ -94,6 +102,12 @@ impl TEdgeComponent for CumulocityMapper { tc.forward_from_local("q/us/#", local_prefix.clone(), "")?; tc.forward_from_local("c/us/#", local_prefix.clone(), "")?; + // SmartREST1 + tc.forward_from_local("s/ul/#", local_prefix.clone(), "")?; + tc.forward_from_local("t/ul/#", local_prefix.clone(), "")?; + tc.forward_from_local("q/ul/#", local_prefix.clone(), "")?; + tc.forward_from_local("c/ul/#", local_prefix.clone(), "")?; + // SmartREST2 tc.forward_from_local("s/uc/#", local_prefix.clone(), "")?; tc.forward_from_local("t/uc/#", local_prefix.clone(), "")?; From 8da664750b62add34bb0a647f4557ef70e317865 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Wed, 24 Jul 2024 20:13:57 +0200 Subject: [PATCH 06/13] add SmartREST1.0 system test Signed-off-by: Reuben Miller --- .../libraries/ThinEdgeIO/ThinEdgeIO.py | 27 +++++++ .../RobotFramework/resources/common.resource | 12 ++- .../tests/cumulocity/smartrest_one/README.md | 31 ++++++++ .../smartrest_one/smartrest_one.robot | 78 +++++++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 tests/RobotFramework/tests/cumulocity/smartrest_one/README.md create mode 100644 tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot diff --git a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py index bc4765c272d..c7d10cfc008 100644 --- a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py +++ b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py @@ -935,6 +935,33 @@ def bridge_should_be_down(self, cloud: str, **kwargs) -> str: self.get_bridge_service_name(cloud), **kwargs ) + def _get_device_sn(self, name): + device = self.current + if name: + if name in self.devices: + device = self.devices.get(name) + + return name or device.get_id() + + @keyword("Delete SmartREST 1.0 Template") + def delete_smartrest_one_template(self, template_id: str): + try: + mo_id = c8y_lib.c8y.identity.get_id( + template_id, "c8y_SmartRestDeviceIdentifier" + ) + log.info( + "Deleting SmartREST 1.0 template. external_id=%s, managed_object_id=%s", + template_id, + mo_id, + ) + c8y_lib.c8y.inventory.delete(mo_id) + except Exception as ex: + log.warning( + "Could not deleted SmartREST 1.0 template. id=%s, ex=%s", + template_id, + ex, + ) + def to_date(value: relativetime_) -> datetime: if isinstance(value, datetime): diff --git a/tests/RobotFramework/resources/common.resource b/tests/RobotFramework/resources/common.resource index e9ef85cbbf7..437c4f5ee33 100644 --- a/tests/RobotFramework/resources/common.resource +++ b/tests/RobotFramework/resources/common.resource @@ -18,7 +18,15 @@ ${DEVICE_ADAPTER} %{DEVICE_ADAPTER=docker} &{LOCAL_CONFIG} skip_bootstrap=False bootstrap_script=%{LOCAL_CONFIG_BOOTSTRAP_SCRIPT= } # Cumulocity settings -&{C8Y_CONFIG} host=%{C8Y_BASEURL= } username=%{C8Y_USER= } password=%{C8Y_PASSWORD= } +&{C8Y_CONFIG} +... host=%{C8Y_BASEURL= } +... username=%{C8Y_USER= } +... password=%{C8Y_PASSWORD= } +... bootstrap_username=%{C8Y_BOOTSTRAP_USER=} +... bootstrap_password=%{C8Y_BOOTSTRAP_PASSWORD=} # AWS settings -&{AWS_CONFIG} access_key_id=%{AWS_ACCESS_KEY_ID= } access_key=%{AWS_SECRET_ACCESS_KEY= } region=%{AWS_REGION= } +&{AWS_CONFIG} +... access_key_id=%{AWS_ACCESS_KEY_ID= } +... access_key=%{AWS_SECRET_ACCESS_KEY= } +... region=%{AWS_REGION= } diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/README.md b/tests/RobotFramework/tests/cumulocity/smartrest_one/README.md new file mode 100644 index 00000000000..2820fa2746e --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/README.md @@ -0,0 +1,31 @@ +#### PUT - works (at least in PUT mode) + +```sh +curl -XPOST --user "$C8Y_TENANT/${C8Y_USER}:${C8Y_PASSWORD}" -H "Accept: application/json" -H "X-Id: templateXIDexample11" -d '10,107,PUT,/inventory/managedObjects/%%,application/json,application/json,%%,UNSIGNED UNSIGNED,"{""value"":""%%""}"' "https://$(tedge config get c8y.http)/s" +``` + +```sh +tedge mqtt pub c8y/s/ul/templateXIDexample11 "$(printf '107,%s,%s' "9238352676" "1")" +``` + + +### c8y-bridge.conf + +```sh +topic s/ul/# out 2 c8y/ "" +topic t/ul/# out 2 c8y/ "" +topic q/ul/# out 2 c8y/ "" +topic c/ul/# out 2 c8y/ "" +topic s/dl/# in 2 c8y/ "" +topic s/ul/templateXIDexample10 out 2 c8y/ "" +topic s/dl/templateXIDexample10 in 2 c8y/ "" +topic s/ol/templateXIDexample10 in 2 c8y/ "" +``` + + +### Problem with cert based device user when using SmartREST 1.0 + +```sh +[c8y/s/ul/templateXIDexample11] 107,9238352676,3333 +[c8y/s/dl/templateXIDexample11] 50,1,401,Unauthorized +``` diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot new file mode 100644 index 00000000000..f841ca342fd --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot @@ -0,0 +1,78 @@ +*** Settings *** +Resource ../../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Test Teardown Custom Teardown + +Test Tags theme:c8y theme:operation + + +*** Variables *** +${SMART_REST_ONE_TEMPLATES}= +... SEPARATOR=\n +... 10,339,GET,/identity/externalIds/c8y_Serial/%%,,application/vnd.com.nsn.cumulocity.externalId+json,%%,STRING, +... 10,311,GET,/alarm/alarms?source\=%%&status\=%%&pageSize\=100,,,%%,UNSIGNED STRING, +... 11,800,$.managedObject,,$.id +... 11,808,$.alarms,,$.id,$.type + + +*** Test Cases *** +Supports SmartREST 1.0 Templates + [Template] Register and Use SmartREST 1.0. Templates + use_builtin_bridge=true + use_builtin_bridge=false + + +*** Keywords *** +Register and Use SmartREST 1.0. Templates + [Arguments] ${use_builtin_bridge} + Custom Setup use_builtin_bridge=${use_builtin_bridge} + + ${TEMPLATE_XID}= Get Random Name prefix=TST_Template + Set Test Variable $TEMPLATE_XID + Execute Command tedge config set c8y.smartrest1.templates "${TEMPLATE_XID}" + Execute Command tedge connect c8y timeout=10 + ${mo}= Device Should Exist ${DEVICE_SN} + + # register templates + Execute Command + ... curl --max-time 15 -sf -XPOST http://127.0.0.1:8001/c8y/s -H "Content-Type: plain/text" -H "X-Id: ${TEMPLATE_XID}" --data "${SMART_REST_ONE_TEMPLATES}" + + # Use templates + # Get managed object id + Execute Command cmd=tedge mqtt pub c8y/s/ul/${TEMPLATE_XID} '339,${DEVICE_SN}' + Should Have MQTT Messages c8y/s/dl/${TEMPLATE_XID} message_pattern=^800,\\d+,${mo["id"]} timeout=10 + + Execute Command cmd=tedge mqtt pub te/device/main///a/test '{"text":"test alarm","severity":"major"}' -r + Device Should Have Alarm/s type=test expected_text=test alarm + + # Get alarms + Execute Command cmd=tedge mqtt pub c8y/s/ul/${TEMPLATE_XID} '311,${mo["id"]},ACTIVE' + Should Have MQTT Messages c8y/s/dl/${TEMPLATE_XID} message_pattern=^808,\\d+,\\d+,test timeout=10 + + # Operations + ${OPERATION}= Get Configuration tedge-configuration-plugin + Operation Should Be SUCCESSFUL ${OPERATION} + +Register Device + [Arguments] ${SERIAL} + ${CREDENTIALS}= Cumulocity.Bulk Register Device With Basic Auth external_id=${SERIAL} + + Execute Command + ... cmd=printf '[c8y]\nusername = "%s"\npassword = "%s"\n' "${CREDENTIALS.username}" "${CREDENTIALS.password}" > /etc/tedge/credentials + +Custom Setup + [Arguments] ${use_builtin_bridge} + ${DEVICE_SN}= Setup skip_bootstrap=${True} + Execute Command test -f ./bootstrap.sh && ./bootstrap.sh --no-connect || true + Execute Command tedge config set mqtt.bridge.built_in ${use_builtin_bridge} + Execute Command tedge config set c8y.use_basic_auth true + + Set Suite Variable $DEVICE_SN + + Register Device ${DEVICE_SN} + +Custom Teardown + Get Logs + IF $TEMPLATE_XID Delete SmartREST 1.0 Template ${TEMPLATE_XID} From d8305b3d499026e9cb378484406c786e569e00ac Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Wed, 23 Oct 2024 20:59:28 +0200 Subject: [PATCH 07/13] Add c8y.registration_mode enum instead of c8y.use_basic_auth bool Signed-off-by: Rina Fujino --- .../tedge_config_cli/models/auth_method.rs | 59 +++++++++++++++++++ .../src/tedge_config_cli/models/mod.rs | 1 + .../src/tedge_config_cli/tedge_config.rs | 12 ++-- crates/core/c8y_api/src/http_proxy.rs | 36 +++++------ crates/core/tedge/src/cli/connect/command.rs | 21 ++++--- crates/core/tedge_mapper/src/c8y/mapper.rs | 13 ++-- .../smartrest_one/smartrest_one.robot | 2 +- 7 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 crates/common/tedge_config/src/tedge_config_cli/models/auth_method.rs diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/auth_method.rs b/crates/common/tedge_config/src/tedge_config_cli/models/auth_method.rs new file mode 100644 index 00000000000..aad2daf154f --- /dev/null +++ b/crates/common/tedge_config/src/tedge_config_cli/models/auth_method.rs @@ -0,0 +1,59 @@ +use camino::Utf8Path; +use std::str::FromStr; +use strum_macros::Display; + +#[derive( + Debug, Display, Clone, Copy, Eq, PartialEq, doku::Document, serde::Serialize, serde::Deserialize, +)] +#[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +pub enum AuthMethod { + Certificate, + Basic, + Auto, +} + +#[derive(thiserror::Error, Debug)] +#[error("Failed to parse flag: {input}. Supported values are: 'certificate', 'basic' or 'auto'")] +pub struct InvalidRegistrationMode { + input: String, +} + +impl FromStr for AuthMethod { + type Err = InvalidRegistrationMode; + + fn from_str(input: &str) -> Result { + match input { + "certificate" => Ok(AuthMethod::Certificate), + "basic" => Ok(AuthMethod::Basic), + "auto" => Ok(AuthMethod::Auto), + _ => Err(InvalidRegistrationMode { + input: input.to_string(), + }), + } + } +} + +pub enum AuthType { + Certificate, + Basic, +} + +impl AuthMethod { + pub fn is_basic(self, credentials_path: &Utf8Path) -> bool { + matches!(self.to_type(credentials_path), AuthType::Basic) + } + + pub fn is_certificate(self, credentials_path: &Utf8Path) -> bool { + matches!(self.to_type(credentials_path), AuthType::Certificate) + } + + pub fn to_type(self, credentials_path: &Utf8Path) -> AuthType { + match self { + AuthMethod::Certificate => AuthType::Certificate, + AuthMethod::Basic => AuthType::Basic, + AuthMethod::Auto if credentials_path.exists() => AuthType::Basic, + AuthMethod::Auto => AuthType::Certificate, + } + } +} diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/mod.rs b/crates/common/tedge_config/src/tedge_config_cli/models/mod.rs index dc2c59b119c..7fb154ddf92 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/models/mod.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/models/mod.rs @@ -1,4 +1,5 @@ pub mod apt_config; +pub mod auth_method; pub mod auto; pub mod c8y_software_management; pub mod connect_url; diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index e23df89a79e..ca1b7f97190 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -1,4 +1,5 @@ use super::models::timestamp::TimeFormat; +use crate::auth_method::AuthMethod; use crate::AptConfig; use crate::AutoFlag; use crate::AutoLogUpload; @@ -379,7 +380,8 @@ impl_append_remove_for_single_value!( SecondsOrHumanTime, u32, AptConfig, - MqttPayloadLimit + MqttPayloadLimit, + AuthMethod ); impl AppendRemoveItem for TemplatesSet { @@ -470,10 +472,10 @@ define_tedge_config! { #[doku(as = "PathBuf")] root_cert_path: Utf8PathBuf, - // TODO: Enum or bool? If enum, what are the other items? - /// Use basic authentication (username/password) instead of device certificate based authentication - #[tedge_config(example = "true", default(value = false))] - use_basic_auth: bool, + /// The authentication method used to connect Cumulocity + #[tedge_config(note = "In the auto mode, basic auth is used if c8y.credentials_path is set")] + #[tedge_config(example = "certificate", example = "basic", example = "auto", default(variable = AuthMethod::Certificate))] + auth_method: AuthMethod, /// The path where Cumulocity username/password are stored #[tedge_config(note = "The value must be the path of the credentials file.")] diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index b3dd6f8611e..788162171c2 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -14,6 +14,7 @@ use reqwest::Url; use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration; +use tedge_config::auth_method::AuthType; use tedge_config::mqtt_config::MqttConfigBuildError; use tedge_config::MultiError; use tedge_config::TEdgeConfig; @@ -189,24 +190,25 @@ impl C8yAuthRetriever { let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; let topic_prefix = c8y_config.bridge.topic_prefix.clone(); - if c8y_config.use_basic_auth { - Ok(Self::Basic { + match c8y_config.auth_method.to_type(&c8y_config.credentials_path) { + AuthType::Basic => Ok(Self::Basic { credentials_path: c8y_config.credentials_path.clone(), - }) - } else { - let mqtt_config = tedge_config - .mqtt_config() - .map_err(MqttConfigBuildError::from)?; - - let topic = TopicFilter::new_unchecked(&format!("{topic_prefix}/s/dat")); - let mqtt_config = mqtt_config - .with_no_session() // Ignore any already published tokens, possibly stale. - .with_subscriptions(topic); - - Ok(Self::Jwt { - mqtt_config: Box::new(mqtt_config), - topic_prefix, - }) + }), + AuthType::Certificate => { + let mqtt_config = tedge_config + .mqtt_config() + .map_err(MqttConfigBuildError::from)?; + + let topic = TopicFilter::new_unchecked(&format!("{topic_prefix}/s/dat")); + let mqtt_config = mqtt_config + .with_no_session() // Ignore any already published tokens, possibly stale. + .with_subscriptions(topic); + + Ok(Self::Jwt { + mqtt_config: Box::new(mqtt_config), + topic_prefix, + }) + } } } diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 57fc2b60545..b4d14c31e60 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -19,6 +19,7 @@ use rumqttc::QoS::AtLeastOnce; use std::path::Path; use std::sync::Arc; use std::time::Duration; +use tedge_config::auth_method::AuthType; use tedge_config::system_services::*; use tedge_config::TEdgeConfig; use tedge_config::*; @@ -265,12 +266,15 @@ pub fn bridge_config( Cloud::C8y => { let c8y_config = config.c8y.try_get(profile)?; - let (remote_username, remote_password) = if c8y_config.use_basic_auth { - let (username, password) = read_c8y_credentials(&c8y_config.credentials_path)?; - (Some(username), Some(password)) - } else { - (None, None) - }; + let (remote_username, remote_password) = + match c8y_config.auth_method.to_type(&c8y_config.credentials_path) { + AuthType::Certificate => (None, None), + AuthType::Basic => { + let (username, password) = + read_c8y_credentials(&c8y_config.credentials_path)?; + (Some(username), Some(password)) + } + }; let params = BridgeConfigC8yParams { mqtt_host: c8y_config.mqtt.or_config_not_set()?.clone(), @@ -302,7 +306,10 @@ fn check_device_status_c8y( let c8y_config = tedge_config.c8y.try_get(c8y_profile)?; // TODO: Use SmartREST1 to check connection - if c8y_config.use_basic_auth { + if c8y_config + .auth_method + .is_basic(&c8y_config.credentials_path) + { return Ok(DeviceStatus::AlreadyExists); } diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index a1dc4a0f742..b1c249528a9 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -67,10 +67,13 @@ impl TEdgeComponent for CumulocityMapper { .iter() .map(|id| Cow::Owned(format!("s/dc/{id}"))); + let use_certificate = c8y_config + .auth_method + .is_certificate(&c8y_config.credentials_path); let cloud_topics = [ ("s/dt", true), ("s/ds", true), - ("s/dat", !c8y_config.use_basic_auth), + ("s/dat", use_certificate), ("s/e", true), ("devicecontrol/notifications", true), ("error", true), @@ -129,7 +132,7 @@ impl TEdgeComponent for CumulocityMapper { tc.forward_from_local("alarm/alarms/create/#", local_prefix.clone(), "")?; // JWT token - if !c8y_config.use_basic_auth { + if use_certificate { tc.forward_from_local("s/uat", local_prefix.clone(), "")?; } @@ -143,7 +146,9 @@ impl TEdgeComponent for CumulocityMapper { // https://cumulocity.com/docs/device-integration/mqtt/#mqtt-clean-session cloud_config.set_clean_session(true); - if c8y_config.use_basic_auth { + if use_certificate { + use_key_and_cert(&mut cloud_config, &c8y_config.root_cert_path, &tedge_config)?; + } else { let (username, password) = read_c8y_credentials(&c8y_config.credentials_path)?; use_credentials( &mut cloud_config, @@ -151,8 +156,6 @@ impl TEdgeComponent for CumulocityMapper { username, password, )?; - } else { - use_key_and_cert(&mut cloud_config, &c8y_config.root_cert_path, &tedge_config)?; } let main_device_xid: EntityExternalId = diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot index f841ca342fd..613bc31839e 100644 --- a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot @@ -67,7 +67,7 @@ Custom Setup ${DEVICE_SN}= Setup skip_bootstrap=${True} Execute Command test -f ./bootstrap.sh && ./bootstrap.sh --no-connect || true Execute Command tedge config set mqtt.bridge.built_in ${use_builtin_bridge} - Execute Command tedge config set c8y.use_basic_auth true + Execute Command tedge config set c8y.auth_method basic Set Suite Variable $DEVICE_SN From 562b072d4b174eda5dc44b5bcd8a312f32fc84be Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Thu, 24 Oct 2024 15:28:30 +0200 Subject: [PATCH 08/13] Add log messages to indicate which authentication method is used Signed-off-by: Rina Fujino --- crates/core/c8y_api/src/http_proxy.rs | 3 +++ crates/core/tedge/src/cli/connect/command.rs | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index 788162171c2..7cfd1b1c724 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -19,6 +19,7 @@ use tedge_config::mqtt_config::MqttConfigBuildError; use tedge_config::MultiError; use tedge_config::TEdgeConfig; use tedge_config::TopicPrefix; +use tracing::debug; use tracing::error; use tracing::info; @@ -215,6 +216,7 @@ impl C8yAuthRetriever { pub async fn get_auth_header_value(&mut self) -> Result { let header_value = match &self { Self::Basic { credentials_path } => { + debug!("Using basic authentication."); let (username, password) = read_c8y_credentials(credentials_path)?; format!("Basic {}", base64::encode(format!("{username}:{password}"))).parse()? } @@ -222,6 +224,7 @@ impl C8yAuthRetriever { mqtt_config, topic_prefix, } => { + debug!("Using JWT token bearer authentication."); let jwt_token = Self::get_jwt_token(mqtt_config, topic_prefix).await?; format!("Bearer {}", jwt_token.token()).parse()? } diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index b4d14c31e60..8475302b6a1 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -268,8 +268,12 @@ pub fn bridge_config( let (remote_username, remote_password) = match c8y_config.auth_method.to_type(&c8y_config.credentials_path) { - AuthType::Certificate => (None, None), + AuthType::Certificate => { + println!("Using device certificate authentication.\n"); + (None, None) + } AuthType::Basic => { + println!("Using basic authentication.\n"); let (username, password) = read_c8y_credentials(&c8y_config.credentials_path)?; (Some(username), Some(password)) From efc3eea1a051c7c77d1d3b50d57ddb6bc7b164c5 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Thu, 24 Oct 2024 14:26:53 +0000 Subject: [PATCH 09/13] Fix bridge configuration for smartrest1.0 Signed-off-by: Rina Fujino --- crates/core/tedge/src/bridge/c8y.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index 3b2e143a2fc..e96c320bcf4 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -99,9 +99,9 @@ impl From for BridgeConfig { .collect::>(); topics.extend(templates_set); - // These topics are somehow required to receive operations when using Basic auth - let use_smartrest_one = use_basic_auth; - if use_smartrest_one { + // SmartRest1 (to support customers with existing solutions based on SmartRest 1) + // Only add the topics if at least 1 template is defined + if !smartrest_one_templates.0.is_empty() { topics.extend([ format!(r#"s/ul/# out 2 {topic_prefix}/ """#), format!(r#"t/ul/# out 2 {topic_prefix}/ """#), @@ -109,11 +109,7 @@ impl From for BridgeConfig { format!(r#"c/ul/# out 2 {topic_prefix}/ """#), format!(r#"s/dl/# in 2 {topic_prefix}/ """#), ]); - } - // SmartRest1 (to support customers with existing solutions based on SmartRest 1) - // Only add the topics if at least 1 template is defined - if !smartrest_one_templates.0.is_empty() { let templates_set = smartrest_one_templates .0 .iter() From f4352e0eafa772ea550ac9cd8e49873554230e18 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Thu, 24 Oct 2024 16:24:28 +0000 Subject: [PATCH 10/13] Exclude SmartREST1 topics from subscriptions when auth is cert Signed-off-by: Rina Fujino --- crates/core/tedge_mapper/src/c8y/mapper.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index b1c249528a9..ad975bf9158 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -106,10 +106,12 @@ impl TEdgeComponent for CumulocityMapper { tc.forward_from_local("c/us/#", local_prefix.clone(), "")?; // SmartREST1 - tc.forward_from_local("s/ul/#", local_prefix.clone(), "")?; - tc.forward_from_local("t/ul/#", local_prefix.clone(), "")?; - tc.forward_from_local("q/ul/#", local_prefix.clone(), "")?; - tc.forward_from_local("c/ul/#", local_prefix.clone(), "")?; + if !use_certificate { + tc.forward_from_local("s/ul/#", local_prefix.clone(), "")?; + tc.forward_from_local("t/ul/#", local_prefix.clone(), "")?; + tc.forward_from_local("q/ul/#", local_prefix.clone(), "")?; + tc.forward_from_local("c/ul/#", local_prefix.clone(), "")?; + } // SmartREST2 tc.forward_from_local("s/uc/#", local_prefix.clone(), "")?; From b1ace6761ea49393fc25716d9d7c34fa090d9884 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Thu, 24 Oct 2024 19:46:00 +0200 Subject: [PATCH 11/13] Skip tenant URL check in tedge connect c8y for basic authentication Signed-off-by: Rina Fujino --- crates/core/tedge/src/cli/connect/command.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 8475302b6a1..b4c489b57e9 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -137,13 +137,18 @@ impl Command for ConnectCommand { } if let Cloud::C8y = self.cloud { - if !self.offline_mode { + let c8y_config = config.c8y.try_get(self.profile.as_deref())?; + + let use_basic_auth = c8y_config + .auth_method + .is_basic(&c8y_config.credentials_path); + if use_basic_auth { + println!("Skipped tenant URL check due to basic authentication.\n"); + } else if !self.offline_mode { check_connected_c8y_tenant_as_configured( config, self.profile.as_deref(), - &config - .c8y - .try_get(self.profile.as_deref())? + &c8y_config .mqtt .or_none() .map(|u| u.host().to_string()) @@ -831,7 +836,9 @@ fn check_connected_c8y_tenant_as_configured( configured_url: &str, ) { match get_connected_c8y_url(tedge_config, c8y_prefix) { - Ok(url) if url == configured_url => {} + Ok(url) if url == configured_url => { + println!("Tenant URL check is successful.\n") + } Ok(url) => println!( "Warning: Connecting to {}, but the configured URL is {}.\n\ The device certificate has to be removed from the former tenant.\n", From 39f1354ff1ee6c5f19d25de6f1a33eb6834367d4 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Fri, 25 Oct 2024 14:20:16 +0000 Subject: [PATCH 12/13] test: improve stability of the smartrest 1 system tests Signed-off-by: Rina Fujino --- .../smartrest_one/smartrest_one.robot | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot index 613bc31839e..983556bce2c 100644 --- a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot @@ -18,10 +18,11 @@ ${SMART_REST_ONE_TEMPLATES}= *** Test Cases *** -Supports SmartREST 1.0 Templates - [Template] Register and Use SmartREST 1.0. Templates - use_builtin_bridge=true - use_builtin_bridge=false +Supports SmartREST 1.0 Templates - builtin + Register and Use SmartREST 1.0. Templates use_builtin_bridge=true + +Supports SmartREST 1.0 Templates - mosquitto + Register and Use SmartREST 1.0. Templates use_builtin_bridge=false *** Keywords *** @@ -32,17 +33,22 @@ Register and Use SmartREST 1.0. Templates ${TEMPLATE_XID}= Get Random Name prefix=TST_Template Set Test Variable $TEMPLATE_XID Execute Command tedge config set c8y.smartrest1.templates "${TEMPLATE_XID}" - Execute Command tedge connect c8y timeout=10 + Execute Command tedge connect c8y ${mo}= Device Should Exist ${DEVICE_SN} # register templates Execute Command ... curl --max-time 15 -sf -XPOST http://127.0.0.1:8001/c8y/s -H "Content-Type: plain/text" -H "X-Id: ${TEMPLATE_XID}" --data "${SMART_REST_ONE_TEMPLATES}" + SmartREST1 Template Should Exist ${TEMPLATE_XID} + + # Since we create a SmartREST template after initial connection, reconnect is required to subscribe the template properly. + Execute Command tedge reconnect c8y + # Use templates # Get managed object id - Execute Command cmd=tedge mqtt pub c8y/s/ul/${TEMPLATE_XID} '339,${DEVICE_SN}' - Should Have MQTT Messages c8y/s/dl/${TEMPLATE_XID} message_pattern=^800,\\d+,${mo["id"]} timeout=10 + Execute Command cmd=tedge mqtt pub --qos 1 c8y/s/ul/${TEMPLATE_XID} '339,${DEVICE_SN}' + Should Have MQTT Messages c8y/s/dl/${TEMPLATE_XID} message_pattern=^800,\\d+,${mo["id"]} Execute Command cmd=tedge mqtt pub te/device/main///a/test '{"text":"test alarm","severity":"major"}' -r Device Should Have Alarm/s type=test expected_text=test alarm @@ -60,7 +66,12 @@ Register Device ${CREDENTIALS}= Cumulocity.Bulk Register Device With Basic Auth external_id=${SERIAL} Execute Command - ... cmd=printf '[c8y]\nusername = "%s"\npassword = "%s"\n' "${CREDENTIALS.username}" "${CREDENTIALS.password}" > /etc/tedge/credentials + ... cmd=printf '[c8y]\nusername = "%s"\npassword = "%s"\n' '${CREDENTIALS.username}' '${CREDENTIALS.password}' > /etc/tedge/credentials + +SmartREST1 Template Should Exist + [Arguments] ${name} + Execute Command + ... cmd=curl --max-time 15 -sf -X GET http://127.0.0.1:8001/c8y/identity/externalIds/c8y_SmartRestDeviceIdentifier/${name} Custom Setup [Arguments] ${use_builtin_bridge} From c078d4b469178f78252f6d7e33f1590676d35f96 Mon Sep 17 00:00:00 2001 From: Rina Fujino Date: Wed, 30 Oct 2024 12:35:03 +0000 Subject: [PATCH 13/13] test: add retry tag to smartrest1 test Signed-off-by: Rina Fujino --- .../tests/cumulocity/smartrest_one/smartrest_one.robot | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot index 983556bce2c..7205bb76fba 100644 --- a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot @@ -22,6 +22,7 @@ Supports SmartREST 1.0 Templates - builtin Register and Use SmartREST 1.0. Templates use_builtin_bridge=true Supports SmartREST 1.0 Templates - mosquitto + [Tags] test:retry(1) workaround # rarely no message arrives on c8y/s/dl/template Register and Use SmartREST 1.0. Templates use_builtin_bridge=false