From 4509a90e1732563875a922a3260a01bdb4dfd255 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Mon, 22 Jul 2024 10:34:21 +0200 Subject: [PATCH 1/7] add support for c8y smartrest one topics Signed-off-by: Reuben Miller --- .../src/tedge_config_cli/tedge_config.rs | 5 ++ crates/core/tedge/src/bridge/c8y.rs | 49 +++++++++++++++++++ crates/core/tedge/src/cli/connect/command.rs | 1 + 3 files changed, 55 insertions(+) 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 64407269e7e..2ad58848fd3 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 @@ -471,6 +471,11 @@ define_tedge_config! { templates: TemplatesSet, }, + 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 91eb8e8b4ad..b43ba001cb5 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -19,6 +19,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, } @@ -34,6 +35,7 @@ impl From for BridgeConfig { bridge_keyfile, smartrest_templates, include_local_clean_session, + smartrest_one_templates, bridge_location, } = params; @@ -49,6 +51,12 @@ impl From for BridgeConfig { r#"s/ds in 2 c8y/ """#.into(), // Debug r#"s/e in 0 c8y/ """#.into(), + // SmartRest1 (to support customers with existing solutions based on SmartRest 1) + // 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(), // SmartRest2 r#"s/uc/# out 2 c8y/ """#.into(), r#"t/uc/# out 2 c8y/ """#.into(), @@ -83,6 +91,35 @@ impl From for BridgeConfig { .collect::>(); topics.extend(templates_set); + // 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([ + 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(), + ]); + + // TODO: Add support for smartrest one topics + let templates_set = smartrest_one_templates + .0 + .iter() + .flat_map(|s| { + // Smartrest 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 c8y/ """#), + format!(r#"s/dl/{s} in 2 c8y/ """#), + ] + .into_iter() + }) + .collect::>(); + topics.extend(templates_set); + } + let include_local_clean_session = match include_local_clean_session { AutoFlag::True => true, AutoFlag::False => false, @@ -163,6 +200,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, }; @@ -217,6 +255,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 963fb2aa047..38ee34d3fe9 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -258,6 +258,7 @@ pub fn bridge_config( bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), smartrest_templates: config.c8y.smartrest.templates.clone(), + smartrest_one_templates: config.c8y.smartrest1.templates.clone(), include_local_clean_session: config.c8y.bridge.include.local_cleansession.clone(), bridge_location, }; From 6e81a7b4b57aeabcb93e96ca9ce2748ca74b323d Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Wed, 24 Jul 2024 20:13:57 +0200 Subject: [PATCH 2/7] add system test Signed-off-by: Reuben Miller --- .../tests/cumulocity/smartrest_one/README.md | 31 +++++++++++++++++++ .../smartrest_one/smartrest_one.robot | 25 +++++++++++++++ 2 files changed, 56 insertions(+) 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/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..e6549bf09da --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot @@ -0,0 +1,25 @@ +*** Settings *** +Resource ../../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Test Tags theme:c8y theme:operation +Test Setup Custom Setup +Test Teardown Get Logs + +*** Test Cases *** + +Register SmartREST 1 templates + ${TEMPLATE_XID}= Set Variable templateXIDexample01 + # Execute Command cmd=curl -XPOST -H "X-Id: templateXIDexample01" -d "10,107,GET,/inventory/managedObjects/%%/childDevices?pageSize=100,,,%%,," http://127.0.0.1:8001/c8y/s + Execute Command cmd=tedge mqtt pub c8y/s/ul "15,${TEMPLATE_XID}\n10,107,GET,/inventory/managedObjects/%%/childDevices?pageSize=100,,,%%,,\n" + Log debug + # 10,107,GET,/inventory/managedObjects/%%/childDevices?pageSize=100,,,%%,,\n + # Should Have MQTT Messages c8y/s/us message_pattern=114,c8y_DownloadConfigFile,c8y_LogfileRequest,c8y_RemoteAccessConnect,c8y_Restart,c8y_SoftwareUpdate,c8y_UploadConfigFile minimum=1 maximum=1 + + +*** Keywords *** +Custom Setup + ${DEVICE_SN}= Setup + Set Suite Variable $DEVICE_SN + Device Should Exist ${DEVICE_SN} From a895b1bb631e40cb45cfbd78b6d489464913fde7 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Wed, 24 Jul 2024 20:20:43 +0200 Subject: [PATCH 3/7] add legacy c8y mqtt device registration script Signed-off-by: Reuben Miller --- .../smartrest_one/register-device.sh | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100755 tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh b/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh new file mode 100755 index 00000000000..4d5d3762c6f --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh @@ -0,0 +1,70 @@ +#!/bin/sh +set -e + +install_dependencies() { + if ! command -V c8y >/dev/null 2>&1; then + curl https://reubenmiller.github.io/go-c8y-cli-repo/debian/PUBLIC.KEY | gpg --dearmor | sudo tee /usr/share/keyrings/go-c8y-cli-archive-keyring.gpg >/dev/null + sudo sh -c "echo 'deb [signed-by=/usr/share/keyrings/go-c8y-cli-archive-keyring.gpg] http://reubenmiller.github.io/go-c8y-cli-repo/debian stable main' >> /etc/apt/sources.list" + sudo apt-get update + sudo apt-get install -y --no-install-recommends go-c8y-cli + fi +} + +register_device() { + DEVICE_ID=$(tedge config get device.id) + C8Y_HOST=$(tedge config get c8y.url) + export C8Y_HOST + export CI=true + + while :; do + echo "Device registration loop: $DEVICE_ID" + CREDS=$(c8y deviceregistration getCredentials --id "$DEVICE_ID" --sessionUsername "$C8Y_BOOTSTRAP_USER" --sessionPassword "$C8Y_BOOTSTRAP_PASSWORD" --select tenantid,username,password -o csv ||:) + if [ -n "$CREDS" ]; then + break + fi + sleep 5 + done + + DEVICE_TENANT=$(echo "$CREDS" | cut -d, -f1) + DEVICE_USERNAME=$(echo "$CREDS" | cut -d, -f2) + DEVICE_PASSWORD=$(echo "$CREDS" | cut -d, -f3) + + # Save credentials + cat << EOT > /etc/tedge/c8y-mqtt.env +DEVICE_TENANT="$DEVICE_TENANT" +DEVICE_USERNAME="$DEVICE_USERNAME" +DEVICE_PASSWORD="$DEVICE_PASSWORD" +EOT + + # Show banner + # echo + # echo "--------------- device credentials --------------" + # echo "DEVICE_TENANT: $DEVICE_TENANT" + # echo "DEVICE_USERNAME: $DEVICE_USERNAME" + # echo "DEVICE_PASSWORD: $DEVICE_PASSWORD" + # echo "-------------------------------------------------" +} + +install_dependencies + +if [ ! -f /etc/tedge/c8y-mqtt.env ]; then + register_device +fi + +# shellcheck disable=SC1091 +. /etc/tedge/c8y-mqtt.env + +if [ -f /etc/tedge/mosquitto-conf/c8y-bridge.conf ]; then + echo "Updating c8y bridge username/password" + if ! grep -q remote_username /etc/tedge/mosquitto-conf/c8y-bridge.conf; then + sed -i 's|bridge_certfile .*|remote_username '"$DEVICE_TENANT/$DEVICE_USERNAME"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf + sed -i 's|bridge_keyfile .*|remote_password '"$DEVICE_PASSWORD"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf + else + sed -i 's|remote_username .*|remote_username '"$DEVICE_TENANT/$DEVICE_USERNAME"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf + sed -i 's|remote_password .*|remote_password '"$DEVICE_PASSWORD"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf + fi + + # TODO delete JWT topics + # topic s/uat out 0 c8y/ "" + # topic s/dat in 0 c8y/ "" +fi From f0e9028f70f68bf21f03d8ec281e9df09f214a70 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Thu, 25 Jul 2024 23:49:03 +0200 Subject: [PATCH 4/7] enable basic auth Signed-off-by: Reuben Miller --- Cargo.lock | 3 + crates/common/download/src/download.rs | 7 +++ .../src/tedge_config_cli/tedge_config.rs | 14 +++++ crates/core/c8y_api/src/http_proxy.rs | 5 ++ crates/core/tedge/src/bridge/aws.rs | 2 + crates/core/tedge/src/bridge/azure.rs | 2 + crates/core/tedge/src/bridge/c8y.rs | 23 ++++++-- crates/core/tedge/src/bridge/config.rs | 21 ++++++- crates/core/tedge/src/cli/connect/command.rs | 6 +- crates/core/tedge_mapper/src/c8y/mapper.rs | 5 +- crates/extensions/c8y_auth_proxy/Cargo.toml | 1 + .../extensions/c8y_auth_proxy/src/server.rs | 55 ++++++++++++++++--- crates/extensions/c8y_http_proxy/src/actor.rs | 14 +++-- .../c8y_http_proxy/src/credentials.rs | 1 + crates/extensions/c8y_http_proxy/src/lib.rs | 6 ++ crates/extensions/tedge_http_ext/Cargo.toml | 2 + .../extensions/tedge_http_ext/src/messages.rs | 21 +++++++ plugins/c8y_remote_access_plugin/src/auth.rs | 7 ++- 18 files changed, 173 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48eaa6fc313..07f8eea4351 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,7 @@ dependencies = [ "axum 0.6.20", "axum-server", "axum_tls", + "base64 0.13.1", "c8y_http_proxy", "camino", "env_logger", @@ -4056,10 +4057,12 @@ name = "tedge_http_ext" version = "1.1.1" dependencies = [ "async-trait", + "base64 0.13.1", "futures", "http 0.2.11", "hyper 0.14.28", "hyper-rustls", + "log", "mockito", "rustls 0.21.11", "serde", diff --git a/crates/common/download/src/download.rs b/crates/common/download/src/download.rs index a9108fc34fd..e3dd7cdcc4a 100644 --- a/crates/common/download/src/download.rs +++ b/crates/common/download/src/download.rs @@ -94,12 +94,17 @@ impl DownloadInfo { pub enum Auth { /// HTTP Bearer authentication Bearer(String), + Basic(Option, Option), } impl Auth { pub fn new_bearer(token: &str) -> Self { Self::Bearer(token.into()) } + + pub fn new_basic(username: &str, password: &str) -> Self { + Self::Basic(Some(username.to_string()), Some(password.to_string())) + } } /// A struct which manages file downloads. @@ -409,6 +414,8 @@ impl Downloader { let mut request = self.client.get(url.url()); if let Some(Auth::Bearer(token)) = &url.auth { request = request.bearer_auth(token) + } else if let Some(Auth::Basic(username, password)) = &url.auth { + request = request.basic_auth(username.clone().unwrap(), Some(password.clone().unwrap())) } if range_start != 0 { 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 2ad58848fd3..4f6384da9a8 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 @@ -74,6 +74,10 @@ impl TEdgeConfig { Self(TEdgeConfigReader::from_dto(dto, location)) } + pub fn use_legacy_auth(&self) -> bool { + return std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); + } + pub fn mqtt_config(&self) -> Result { let host = self.mqtt.client.host.as_str(); let port = u16::from(self.mqtt.client.port); @@ -465,6 +469,16 @@ define_tedge_config! { #[doku(as = "PathBuf")] root_cert_path: Utf8PathBuf, + /// Cumulocity Username + #[tedge_config(note = "The value can be a directory path as well as the path of the certificate file.")] + #[tedge_config(example = "t12345/device_tedge001", default(variable = "DEFAULT_ROOT_CERT_PATH"))] + username: String, + + /// Cumulocity Password + #[tedge_config(note = "The value can be a directory path as well as the path of the certificate file.")] + #[tedge_config(example = "d8aj1d8j1.81", default(variable = "DEFAULT_ROOT_CERT_PATH"))] + password: String, + 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 e799c373898..c8c54d5bac1 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -157,6 +157,11 @@ impl C8yMqttJwtTokenRetriever { } pub async fn get_jwt_token(&mut self) -> Result { + // TODO: Remove this hack + if std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok() { + return Ok(SmartRestJwtResponse::try_new("71,111111")?); + } + let mut mqtt_con = Connection::new(&self.mqtt_config).await?; let pub_topic = format!("{}/s/uat", self.topic_prefix); diff --git a/crates/core/tedge/src/bridge/aws.rs b/crates/core/tedge/src/bridge/aws.rs index 0e1030c9bb2..a39f4111615 100644 --- a/crates/core/tedge/src/bridge/aws.rs +++ b/crates/core/tedge/src/bridge/aws.rs @@ -52,6 +52,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(), @@ -106,6 +107,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(), diff --git a/crates/core/tedge/src/bridge/azure.rs b/crates/core/tedge/src/bridge/azure.rs index d8713379859..34466e65706 100644 --- a/crates/core/tedge/src/bridge/azure.rs +++ b/crates/core/tedge/src/bridge/azure.rs @@ -46,6 +46,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(), @@ -103,6 +104,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(), diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index b43ba001cb5..a12e999357d 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -15,6 +15,8 @@ pub struct BridgeConfigC8yParams { pub mqtt_host: HostPort, pub config_file: String, 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, @@ -31,6 +33,8 @@ impl From for BridgeConfig { config_file, bridge_root_cert_path, remote_clientid, + remote_username, + remote_password, bridge_certfile, bridge_keyfile, smartrest_templates, @@ -70,11 +74,17 @@ impl From for BridgeConfig { r#"alarm/alarms/create out 2 c8y/ """#.into(), r#"devicecontrol/notifications in 2 c8y/ """#.into(), r#"error in 2 c8y/ """#.into(), - // c8y JWT token retrieval - r#"s/uat out 0 c8y/ """#.into(), - r#"s/dat in 0 c8y/ """#.into(), ]; + let use_legacy_auth = remote_username.is_some() && remote_password.is_some(); + if use_legacy_auth { + topics.extend(vec![ + // c8y JWT token retrieval + r#"s/uat out 0 c8y/ """#.into(), + r#"s/dat in 0 c8y/ """#.into(), + ]) + } + let templates_set = smartrest_templates .0 .iter() @@ -100,6 +110,7 @@ impl From for BridgeConfig { r#"q/ul/# out 2 c8y/ """#.into(), r#"c/ul/# out 2 c8y/ """#.into(), r#"s/dl/# in 2 c8y/ """#.into(), + r#"s/ol/# in 2 c8y/ """#.into(), ]); // TODO: Add support for smartrest one topics @@ -131,7 +142,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(), @@ -196,6 +208,8 @@ mod tests { mqtt_host: HostPort::::try_from("test.test.io")?, config_file: C8Y_CONFIG_FILENAME.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(), @@ -213,6 +227,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(), diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index a73ca828f24..c5f275ba7dc 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -16,6 +16,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, @@ -63,8 +64,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)?; + + // TODO: + let use_legacy_auth = self.remote_username.is_some() && self.remote_password.is_some(); + if use_legacy_auth { + match &self.remote_password { + Some(value) => { + writeln!(writer, "remote_password {}", value)?; + } + None => {} + } + } 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)?; @@ -156,6 +169,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(), @@ -223,6 +237,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(), @@ -289,6 +304,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(), @@ -410,6 +426,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 38ee34d3fe9..29544e140c0 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -254,6 +254,9 @@ pub fn bridge_config( mqtt_host: config.c8y.mqtt.or_config_not_set()?.clone(), config_file: C8Y_CONFIG_FILENAME.into(), bridge_root_cert_path: config.c8y.root_cert_path.clone(), + // TODO: now + remote_username: if config.c8y.username.is_empty() { None } else { Some(config.c8y.username.clone()) }, + remote_password: if config.c8y.password.is_empty() { None } else { Some(config.c8y.password.clone()) }, remote_clientid: config.device.id.try_read(config)?.clone(), bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), @@ -578,7 +581,8 @@ fn new_bridge( if let Err(err) = write_bridge_config_to_file(config_location, bridge_config) { // We want to preserve previous errors and therefore discard result of this function. - let _ = clean_up(config_location, bridge_config); + // TODO: Debugging bridge configuration + // let _ = clean_up(config_location, bridge_config); return Err(err); } } else { diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index a2acd65e5ce..b0324d2eaed 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -105,7 +105,10 @@ 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(), "")?; + + if tedge_config.use_legacy_auth() { + tc.forward_from_local("s/uat", local_prefix.clone(), "")?; + } let c8y = tedge_config.c8y.mqtt.or_config_not_set()?; let mut cloud_config = tedge_mqtt_bridge::MqttOptions::new( diff --git a/crates/extensions/c8y_auth_proxy/Cargo.toml b/crates/extensions/c8y_auth_proxy/Cargo.toml index 34c40a2d89a..753b1ed6a4d 100644 --- a/crates/extensions/c8y_auth_proxy/Cargo.toml +++ b/crates/extensions/c8y_auth_proxy/Cargo.toml @@ -14,6 +14,7 @@ anyhow = { workspace = true } axum = { workspace = true, features = ["macros", "ws", "headers"] } axum-server = { workspace = true } axum_tls = { workspace = true } +base64 = { workspace = true } c8y_http_proxy = { workspace = true } camino = { workspace = true } futures = { workspace = true } diff --git a/crates/extensions/c8y_auth_proxy/src/server.rs b/crates/extensions/c8y_auth_proxy/src/server.rs index 24d907bbf7e..0ec7db2d3d0 100644 --- a/crates/extensions/c8y_auth_proxy/src/server.rs +++ b/crates/extensions/c8y_auth_proxy/src/server.rs @@ -235,7 +235,16 @@ async fn connect_to_websocket( for (name, value) in headers { req = req.header(name.as_str(), value); } - req = req.header("Authorization", format!("Bearer {token}")); + + let use_legacy_auth = std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); + let header_value = if use_legacy_auth { + format!("Basic {}", base64::encode(format!("{}/{}:{}", std::env::var("C8Y_DEVICE_TENANT").unwrap(), std::env::var("C8Y_DEVICE_USER").unwrap(), std::env::var("C8Y_DEVICE_PASSWORD").unwrap()))) + } else { + format!("Bearer {token}") + }; + info!("Using header | Authorization: {header_value}"); + + req = req.header("Authorization", header_value); let req = req .uri(uri) .header(HOST, host.without_scheme.as_ref()) @@ -378,6 +387,10 @@ where Ok(None) } +const C8Y_DEVICE_TENANT_ENV: &str = "C8Y_DEVICE_TENANT"; +const C8Y_DEVICE_USER_ENV: &str = "C8Y_DEVICE_USER"; +const C8Y_DEVICE_PASSWORD_ENV: &str = "C8Y_DEVICE_PASSWORD"; + #[allow(clippy::too_many_arguments)] async fn respond_to( State(host): State, @@ -394,11 +407,26 @@ async fn respond_to( Some(Path(p)) => p.as_str(), None => "", }; + let use_legacy_auth = std::env::var(C8Y_DEVICE_TENANT_ENV).is_ok() && std::env::var(C8Y_DEVICE_USER_ENV).is_ok() && std::env::var(C8Y_DEVICE_PASSWORD_ENV).is_ok(); + // let username = format!("{:?}/{:?}", std::env::var(C8Y_DEVICE_TENANT_ENV).unwrap(), std::env::var(C8Y_DEVICE_USER_ENV).unwrap()); + // let password = Some(std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap()); let auth: fn(reqwest::RequestBuilder, &str) -> reqwest::RequestBuilder = if headers.contains_key("Authorization") { |req, _token| req } else { - |req, token| req.bearer_auth(token) + if use_legacy_auth { + |req: reqwest::RequestBuilder, _token| { + let username = format!("{}/{}", std::env::var(C8Y_DEVICE_TENANT_ENV).unwrap(), std::env::var(C8Y_DEVICE_USER_ENV).unwrap()); + let password = std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap(); + info!("Using basic auth: username={username}, password={password}"); + req.basic_auth(username, Some(password)) + } + } else { + |req, token| { + info!("Using bearer auth: token={token}"); + req.bearer_auth(token) + } + } }; headers.remove(HOST); @@ -422,12 +450,23 @@ async fn respond_to( let (body, body_clone) = small_body.try_clone(); if body_clone.is_none() { let destination = format!("{}/tenant/currentTenant", host.http); - let response = client - .head(&destination) - .bearer_auth(&token) - .send() - .await - .with_context(|| format!("making HEAD request to {destination}"))?; + let response = if use_legacy_auth { + info!("Making head request with basic auth"); + client + .head(&destination) + .basic_auth(format!("{:?}/{:?}", Some(std::env::var(C8Y_DEVICE_TENANT_ENV)).unwrap(), Some(std::env::var(C8Y_DEVICE_USER_ENV)).unwrap()), Some(std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap_or_default())) + .send() + .await + .with_context(|| format!("making HEAD request to {destination}"))? + } else { + info!("Making head request with bearer auth"); + client + .head(&destination) + .bearer_auth(&token) + .send() + .await + .with_context(|| format!("making HEAD request to {destination}"))? + }; if response.status() == StatusCode::UNAUTHORIZED { token = retrieve_token.not_matching(Some(&token)).await; } diff --git a/crates/extensions/c8y_http_proxy/src/actor.rs b/crates/extensions/c8y_http_proxy/src/actor.rs index 3257f3e150e..08c4beea21a 100644 --- a/crates/extensions/c8y_http_proxy/src/actor.rs +++ b/crates/extensions/c8y_http_proxy/src/actor.rs @@ -207,7 +207,7 @@ impl C8YHttpProxyActor { loop { attempt += 1; let request = HttpRequestBuilder::get(&url_get_id) - .bearer_auth(self.end_point.token.clone().unwrap_or_default()) + .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) .build()?; let endpoint = request.uri().path().to_owned(); let method = request.method().to_owned(); @@ -265,7 +265,7 @@ impl C8YHttpProxyActor { let request_builder = build_request(&self.end_point); let request = request_builder .await? - .bearer_auth(self.end_point.token.clone().unwrap_or_default()) + .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) .build()?; let endpoint = request.uri().path().to_owned(); let method = request.method().to_owned(); @@ -311,7 +311,7 @@ impl C8YHttpProxyActor { let request_builder = build_request(&self.end_point); let request = request_builder .await? - .bearer_auth(self.end_point.token.clone().unwrap_or_default()) + .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) .build()?; // retry the request Ok(self.peers.http.await_response(request).await?) @@ -330,7 +330,7 @@ impl C8YHttpProxyActor { let request_builder = build_request(&self.end_point); let request = request_builder .await? - .bearer_auth(self.end_point.token.clone().unwrap_or_default()) + .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) .build()?; Ok(self.peers.http.await_response(request).await?) } @@ -509,7 +509,11 @@ impl C8YHttpProxyActor { .is_some() { let token = self.get_and_set_jwt_token().await?; - download_info.auth = Some(Auth::new_bearer(token.as_str())); + if self.config.c8y_username.is_some() && self.config.c8y_password.is_some() { + download_info.auth = Some(Auth::new_basic(self.config.c8y_username.clone().unwrap_or_default().as_str(), self.config.c8y_password.clone().unwrap_or_default().as_str())); + } else { + download_info.auth = Some(Auth::new_bearer(token.as_str())); + } } info!(target: self.name(), "Downloading from: {:?}", download_info.url()); diff --git a/crates/extensions/c8y_http_proxy/src/credentials.rs b/crates/extensions/c8y_http_proxy/src/credentials.rs index f12bbb5abde..e174811ee7d 100644 --- a/crates/extensions/c8y_http_proxy/src/credentials.rs +++ b/crates/extensions/c8y_http_proxy/src/credentials.rs @@ -40,6 +40,7 @@ impl Server for C8YJwtRetriever { } async fn handle(&mut self, _request: Self::Request) -> Self::Response { + // TODO: Support skipping when using legacy auth let response = self.mqtt_retriever.get_jwt_token().await?; Ok(response.token()) } diff --git a/crates/extensions/c8y_http_proxy/src/lib.rs b/crates/extensions/c8y_http_proxy/src/lib.rs index 49f90edb98d..a30a474cba9 100644 --- a/crates/extensions/c8y_http_proxy/src/lib.rs +++ b/crates/extensions/c8y_http_proxy/src/lib.rs @@ -37,6 +37,8 @@ mod tests; pub struct C8YHttpConfig { pub c8y_http_host: String, pub c8y_mqtt_host: String, + pub c8y_username: Option, + pub c8y_password: Option, pub device_id: String, pub tmp_dir: PathBuf, identity: Option, @@ -50,6 +52,8 @@ impl TryFrom<&TEdgeConfig> for C8YHttpConfig { fn try_from(tedge_config: &TEdgeConfig) -> Result { let c8y_http_host = tedge_config.c8y.http.or_config_not_set()?.to_string(); let c8y_mqtt_host = tedge_config.c8y.mqtt.or_config_not_set()?.to_string(); + let c8y_username = Some(tedge_config.c8y.username.clone()); + let c8y_password = Some(tedge_config.c8y.password.clone()); let device_id = tedge_config.device.id.try_read(tedge_config)?.to_string(); let tmp_dir = tedge_config.tmp.path.as_std_path().to_path_buf(); let identity = tedge_config.http.client.auth.identity()?; @@ -59,6 +63,8 @@ impl TryFrom<&TEdgeConfig> for C8YHttpConfig { Ok(Self { c8y_http_host, c8y_mqtt_host, + c8y_username, + c8y_password, device_id, tmp_dir, identity, diff --git a/crates/extensions/tedge_http_ext/Cargo.toml b/crates/extensions/tedge_http_ext/Cargo.toml index 4f92d361057..f4ec491bd56 100644 --- a/crates/extensions/tedge_http_ext/Cargo.toml +++ b/crates/extensions/tedge_http_ext/Cargo.toml @@ -16,6 +16,7 @@ test_helpers = [] [dependencies] async-trait = { workspace = true } +base64 = { workspace = true } futures = { workspace = true } http = { workspace = true } hyper = { workspace = true, default-features = false, features = [ @@ -25,6 +26,7 @@ hyper = { workspace = true, default-features = false, features = [ "tcp", ] } hyper-rustls = { workspace = true } +log = { workspace = true } rustls = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/extensions/tedge_http_ext/src/messages.rs b/crates/extensions/tedge_http_ext/src/messages.rs index 96c9f9209ab..004a31ab592 100644 --- a/crates/extensions/tedge_http_ext/src/messages.rs +++ b/crates/extensions/tedge_http_ext/src/messages.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use http::header::HeaderName; use http::header::HeaderValue; use http::Method; +use log::info; use serde::de::DeserializeOwned; use thiserror::Error; @@ -131,6 +132,26 @@ impl HttpRequestBuilder { let header_value = format!("Bearer {}", token); self.header(http::header::AUTHORIZATION, header_value) } + + /// Add basic authentication (e.g. a JWT token) + pub fn basic_auth(self, username: T, password: T) -> Self + where + T: std::fmt::Display, + { + let header_value = format!("Basic {}", base64::encode(format!("{username}:{password}"))); + self.header(http::header::AUTHORIZATION, header_value) + } + + pub fn with_auth(self, token: Option, username: Option, password: Option) -> Self + { + let header_value = if username.is_some() && password.is_some() { + format!("Basic {}", base64::encode(format!("{}:{}", username.unwrap_or_default(), password.unwrap_or_default()))) + } else { + format!("Bearer {}", token.unwrap_or_default()) + }; + info!("Using header | Authorization: {header_value}"); + self.header(http::header::AUTHORIZATION, header_value) + } } #[async_trait] diff --git a/plugins/c8y_remote_access_plugin/src/auth.rs b/plugins/c8y_remote_access_plugin/src/auth.rs index 547c2df4b63..9c24629d20c 100644 --- a/plugins/c8y_remote_access_plugin/src/auth.rs +++ b/plugins/c8y_remote_access_plugin/src/auth.rs @@ -6,7 +6,12 @@ pub struct Jwt(String); impl Jwt { pub fn authorization_header(&self) -> String { - format!("Bearer {}", self.0) + let use_legacy_auth = std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); + if use_legacy_auth { + format!("Basic {}", base64::encode(format!("{}/{}:{}", std::env::var("C8Y_DEVICE_TENANT").unwrap(), std::env::var("C8Y_DEVICE_USER").unwrap(), std::env::var("C8Y_DEVICE_PASSWORD").unwrap()))) + } else { + format!("Bearer {}", self.0) + } } pub async fn retrieve(config: &TEdgeConfig) -> miette::Result { From efe95489a83b809f393956e394104c27a7ebf618 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Fri, 26 Jul 2024 00:09:00 +0200 Subject: [PATCH 5/7] support direct connection using credentials Signed-off-by: Reuben Miller --- .../certificate/src/parse_root_certificate.rs | 11 ++++ crates/common/download/src/download.rs | 3 +- crates/core/c8y_api/src/http_proxy.rs | 5 +- crates/core/tedge/src/bridge/c8y.rs | 7 --- crates/core/tedge/src/bridge/config.rs | 1 - .../src/cli/connect/c8y_direct_connection.rs | 25 ++++++-- crates/core/tedge/src/cli/connect/command.rs | 16 +++-- crates/core/tedge_mapper/src/c8y/mapper.rs | 63 ++++++++++++++----- .../extensions/c8y_auth_proxy/src/server.rs | 45 +++++++------ crates/extensions/c8y_http_proxy/src/actor.rs | 37 +++++++++-- crates/extensions/c8y_http_proxy/src/tests.rs | 6 ++ crates/extensions/tedge_http_ext/Cargo.toml | 2 +- .../extensions/tedge_http_ext/src/messages.rs | 17 ++++- .../tedge_mqtt_bridge/src/config.rs | 13 ++++ plugins/c8y_remote_access_plugin/src/auth.rs | 12 +++- 15 files changed, 197 insertions(+), 66 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/common/download/src/download.rs b/crates/common/download/src/download.rs index e3dd7cdcc4a..85dcac1255f 100644 --- a/crates/common/download/src/download.rs +++ b/crates/common/download/src/download.rs @@ -415,7 +415,8 @@ impl Downloader { if let Some(Auth::Bearer(token)) = &url.auth { request = request.bearer_auth(token) } else if let Some(Auth::Basic(username, password)) = &url.auth { - request = request.basic_auth(username.clone().unwrap(), Some(password.clone().unwrap())) + request = + request.basic_auth(username.clone().unwrap(), Some(password.clone().unwrap())) } if range_start != 0 { diff --git a/crates/core/c8y_api/src/http_proxy.rs b/crates/core/c8y_api/src/http_proxy.rs index c8c54d5bac1..1c599aa8ea0 100644 --- a/crates/core/c8y_api/src/http_proxy.rs +++ b/crates/core/c8y_api/src/http_proxy.rs @@ -158,10 +158,11 @@ impl C8yMqttJwtTokenRetriever { pub async fn get_jwt_token(&mut self) -> Result { // TODO: Remove this hack - if std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok() { + if std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok() + { return Ok(SmartRestJwtResponse::try_new("71,111111")?); } - + let mut mqtt_con = Connection::new(&self.mqtt_config).await?; let pub_topic = format!("{}/s/uat", self.topic_prefix); diff --git a/crates/core/tedge/src/bridge/c8y.rs b/crates/core/tedge/src/bridge/c8y.rs index a12e999357d..0fd90b57a25 100644 --- a/crates/core/tedge/src/bridge/c8y.rs +++ b/crates/core/tedge/src/bridge/c8y.rs @@ -55,12 +55,6 @@ impl From for BridgeConfig { r#"s/ds in 2 c8y/ """#.into(), // Debug r#"s/e in 0 c8y/ """#.into(), - // SmartRest1 (to support customers with existing solutions based on SmartRest 1) - // 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(), // SmartRest2 r#"s/uc/# out 2 c8y/ """#.into(), r#"t/uc/# out 2 c8y/ """#.into(), @@ -110,7 +104,6 @@ impl From for BridgeConfig { r#"q/ul/# out 2 c8y/ """#.into(), r#"c/ul/# out 2 c8y/ """#.into(), r#"s/dl/# in 2 c8y/ """#.into(), - r#"s/ol/# in 2 c8y/ """#.into(), ]); // TODO: Add support for smartrest one topics diff --git a/crates/core/tedge/src/bridge/config.rs b/crates/core/tedge/src/bridge/config.rs index c5f275ba7dc..13871565c98 100644 --- a/crates/core/tedge/src/bridge/config.rs +++ b/crates/core/tedge/src/bridge/config.rs @@ -65,7 +65,6 @@ impl BridgeConfig { writeln!(writer, "remote_clientid {}", self.remote_clientid)?; writeln!(writer, "local_clientid {}", self.local_clientid)?; - // TODO: let use_legacy_auth = self.remote_username.is_some() && self.remote_password.is_some(); if use_legacy_auth { match &self.remote_password { 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..0cc1fe8c30e 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; @@ -33,11 +34,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 use_legacy_auth = + bridge_config.remote_username.is_some() && bridge_config.remote_password.is_some(); + if use_legacy_auth { + mqtt_options.set_credentials( + bridge_config.remote_username.clone().unwrap_or_default(), + bridge_config.remote_password.clone().unwrap_or_default(), + ); + } + + let tls_config = if use_legacy_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 29544e140c0..2a0a495db58 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -254,9 +254,16 @@ pub fn bridge_config( mqtt_host: config.c8y.mqtt.or_config_not_set()?.clone(), config_file: C8Y_CONFIG_FILENAME.into(), bridge_root_cert_path: config.c8y.root_cert_path.clone(), - // TODO: now - remote_username: if config.c8y.username.is_empty() { None } else { Some(config.c8y.username.clone()) }, - remote_password: if config.c8y.password.is_empty() { None } else { Some(config.c8y.password.clone()) }, + remote_username: if config.c8y.username.is_empty() { + None + } else { + Some(config.c8y.username.clone()) + }, + remote_password: if config.c8y.password.is_empty() { + None + } else { + Some(config.c8y.password.clone()) + }, remote_clientid: config.device.id.try_read(config)?.clone(), bridge_certfile: config.device.cert_path.clone(), bridge_keyfile: config.device.key_path.clone(), @@ -581,8 +588,7 @@ fn new_bridge( if let Err(err) = write_bridge_config_to_file(config_location, bridge_config) { // We want to preserve previous errors and therefore discard result of this function. - // TODO: Debugging bridge configuration - // let _ = clean_up(config_location, bridge_config); + let _ = clean_up(config_location, bridge_config); return Err(err); } } else { diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index b0324d2eaed..6cc99508b8a 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -20,6 +20,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; @@ -49,7 +50,15 @@ impl TEdgeComponent for CumulocityMapper { let mqtt_config = tedge_config.mqtt_config()?; let c8y_mapper_config = C8yMapperConfig::from_tedge_config(cfg_dir, &tedge_config)?; if tedge_config.mqtt.bridge.built_in { - let custom_topics = tedge_config + let smartrest_1_topics = tedge_config + .c8y + .smartrest1 + .templates + .0 + .iter() + .map(|id| Cow::Owned(format!("s/dl/{id}"))); + + let smartrest_2_topics = tedge_config .c8y .smartrest .templates @@ -57,17 +66,26 @@ impl TEdgeComponent for CumulocityMapper { .iter() .map(|id| Cow::Owned(format!("s/dc/{id}"))); + // Topics are defined as a tuple to make it easier to include/exclude topics. + // Tuple format is: (topic, should_include) let cloud_topics = [ - "s/dt", - "s/dat", - "s/ds", - "s/e", - "devicecontrol/notifications", - "error", + ("s/dt", true), + ("s/ds", true), + ("s/dat", !tedge_config.use_legacy_auth()), + ("s/e", true), + ("devicecontrol/notifications", true), + ("error", true), ] .into_iter() - .map(Cow::Borrowed) - .chain(custom_topics); + .filter_map(|(topic, active)| { + if active { + Some(Cow::Borrowed(topic)) + } else { + None + } + }) + .chain(smartrest_1_topics) + .chain(smartrest_2_topics); let mut tc = BridgeConfig::new(); let local_prefix = format!("{}/", tedge_config.c8y.bridge.topic_prefix.as_str()); @@ -86,6 +104,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(), "")?; @@ -105,7 +129,7 @@ impl TEdgeComponent for CumulocityMapper { )?; tc.forward_from_local("event/events/create/#", local_prefix.clone(), "")?; tc.forward_from_local("alarm/alarms/create/#", local_prefix.clone(), "")?; - + if tedge_config.use_legacy_auth() { tc.forward_from_local("s/uat", local_prefix.clone(), "")?; } @@ -119,11 +143,20 @@ 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, - &tedge_config.c8y.root_cert_path, - &tedge_config, - )?; + if tedge_config.use_legacy_auth() { + use_credentials( + &mut cloud_config, + &tedge_config.c8y.root_cert_path, + tedge_config.c8y.username.clone(), + tedge_config.c8y.password.clone(), + )?; + } else { + use_key_and_cert( + &mut cloud_config, + &tedge_config.c8y.root_cert_path, + &tedge_config, + )?; + } let main_device_xid: EntityExternalId = tedge_config.device.id.try_read(&tedge_config)?.into(); diff --git a/crates/extensions/c8y_auth_proxy/src/server.rs b/crates/extensions/c8y_auth_proxy/src/server.rs index 0ec7db2d3d0..59d6a23f57b 100644 --- a/crates/extensions/c8y_auth_proxy/src/server.rs +++ b/crates/extensions/c8y_auth_proxy/src/server.rs @@ -236,9 +236,17 @@ async fn connect_to_websocket( req = req.header(name.as_str(), value); } - let use_legacy_auth = std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); + let use_legacy_auth = + std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); let header_value = if use_legacy_auth { - format!("Basic {}", base64::encode(format!("{}/{}:{}", std::env::var("C8Y_DEVICE_TENANT").unwrap(), std::env::var("C8Y_DEVICE_USER").unwrap(), std::env::var("C8Y_DEVICE_PASSWORD").unwrap()))) + format!( + "Basic {}", + base64::encode(format!( + "{}:{}", + std::env::var("C8Y_DEVICE_USER").unwrap(), + std::env::var("C8Y_DEVICE_PASSWORD").unwrap() + )) + ) } else { format!("Bearer {token}") }; @@ -387,7 +395,6 @@ where Ok(None) } -const C8Y_DEVICE_TENANT_ENV: &str = "C8Y_DEVICE_TENANT"; const C8Y_DEVICE_USER_ENV: &str = "C8Y_DEVICE_USER"; const C8Y_DEVICE_PASSWORD_ENV: &str = "C8Y_DEVICE_PASSWORD"; @@ -407,25 +414,22 @@ async fn respond_to( Some(Path(p)) => p.as_str(), None => "", }; - let use_legacy_auth = std::env::var(C8Y_DEVICE_TENANT_ENV).is_ok() && std::env::var(C8Y_DEVICE_USER_ENV).is_ok() && std::env::var(C8Y_DEVICE_PASSWORD_ENV).is_ok(); - // let username = format!("{:?}/{:?}", std::env::var(C8Y_DEVICE_TENANT_ENV).unwrap(), std::env::var(C8Y_DEVICE_USER_ENV).unwrap()); - // let password = Some(std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap()); + let use_legacy_auth = std::env::var(C8Y_DEVICE_USER_ENV).is_ok() + && std::env::var(C8Y_DEVICE_PASSWORD_ENV).is_ok(); let auth: fn(reqwest::RequestBuilder, &str) -> reqwest::RequestBuilder = if headers.contains_key("Authorization") { |req, _token| req + } else if use_legacy_auth { + |req: reqwest::RequestBuilder, _token| { + let username = std::env::var(C8Y_DEVICE_USER_ENV).unwrap(); + let password = std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap(); + info!("Using basic auth: username={username}, password={password}"); + req.basic_auth(username, Some(password)) + } } else { - if use_legacy_auth { - |req: reqwest::RequestBuilder, _token| { - let username = format!("{}/{}", std::env::var(C8Y_DEVICE_TENANT_ENV).unwrap(), std::env::var(C8Y_DEVICE_USER_ENV).unwrap()); - let password = std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap(); - info!("Using basic auth: username={username}, password={password}"); - req.basic_auth(username, Some(password)) - } - } else { - |req, token| { - info!("Using bearer auth: token={token}"); - req.bearer_auth(token) - } + |req, token| { + info!("Using bearer auth: token={token}"); + req.bearer_auth(token) } }; headers.remove(HOST); @@ -454,7 +458,10 @@ async fn respond_to( info!("Making head request with basic auth"); client .head(&destination) - .basic_auth(format!("{:?}/{:?}", Some(std::env::var(C8Y_DEVICE_TENANT_ENV)).unwrap(), Some(std::env::var(C8Y_DEVICE_USER_ENV)).unwrap()), Some(std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap_or_default())) + .basic_auth( + std::env::var(C8Y_DEVICE_USER_ENV).unwrap_or_default(), + Some(std::env::var(C8Y_DEVICE_PASSWORD_ENV).unwrap_or_default()), + ) .send() .await .with_context(|| format!("making HEAD request to {destination}"))? diff --git a/crates/extensions/c8y_http_proxy/src/actor.rs b/crates/extensions/c8y_http_proxy/src/actor.rs index 08c4beea21a..01eecb8ca97 100644 --- a/crates/extensions/c8y_http_proxy/src/actor.rs +++ b/crates/extensions/c8y_http_proxy/src/actor.rs @@ -207,7 +207,11 @@ impl C8YHttpProxyActor { loop { attempt += 1; let request = HttpRequestBuilder::get(&url_get_id) - .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) + .with_auth( + self.end_point.token.clone(), + self.config.c8y_username.clone(), + self.config.c8y_password.clone(), + ) .build()?; let endpoint = request.uri().path().to_owned(); let method = request.method().to_owned(); @@ -265,7 +269,11 @@ impl C8YHttpProxyActor { let request_builder = build_request(&self.end_point); let request = request_builder .await? - .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) + .with_auth( + self.end_point.token.clone(), + self.config.c8y_username.clone(), + self.config.c8y_password.clone(), + ) .build()?; let endpoint = request.uri().path().to_owned(); let method = request.method().to_owned(); @@ -311,7 +319,11 @@ impl C8YHttpProxyActor { let request_builder = build_request(&self.end_point); let request = request_builder .await? - .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) + .with_auth( + self.end_point.token.clone(), + self.config.c8y_username.clone(), + self.config.c8y_password.clone(), + ) .build()?; // retry the request Ok(self.peers.http.await_response(request).await?) @@ -330,7 +342,11 @@ impl C8YHttpProxyActor { let request_builder = build_request(&self.end_point); let request = request_builder .await? - .with_auth(self.end_point.token.clone(), self.config.c8y_username.clone(), self.config.c8y_password.clone()) + .with_auth( + self.end_point.token.clone(), + self.config.c8y_username.clone(), + self.config.c8y_password.clone(), + ) .build()?; Ok(self.peers.http.await_response(request).await?) } @@ -510,7 +526,18 @@ impl C8YHttpProxyActor { { let token = self.get_and_set_jwt_token().await?; if self.config.c8y_username.is_some() && self.config.c8y_password.is_some() { - download_info.auth = Some(Auth::new_basic(self.config.c8y_username.clone().unwrap_or_default().as_str(), self.config.c8y_password.clone().unwrap_or_default().as_str())); + download_info.auth = Some(Auth::new_basic( + self.config + .c8y_username + .clone() + .unwrap_or_default() + .as_str(), + self.config + .c8y_password + .clone() + .unwrap_or_default() + .as_str(), + )); } else { download_info.auth = Some(Auth::new_bearer(token.as_str())); } diff --git a/crates/extensions/c8y_http_proxy/src/tests.rs b/crates/extensions/c8y_http_proxy/src/tests.rs index a1a6fa5f56c..2b343a10106 100644 --- a/crates/extensions/c8y_http_proxy/src/tests.rs +++ b/crates/extensions/c8y_http_proxy/src/tests.rs @@ -362,6 +362,8 @@ async fn retry_internal_id_on_expired_jwt_with_mock() { let config = C8YHttpConfig { c8y_http_host: target_url.clone(), c8y_mqtt_host: target_url.clone(), + c8y_username: None, + c8y_password: None, device_id: external_id.into(), tmp_dir: tmp_dir.into(), identity: None, @@ -431,6 +433,8 @@ async fn retry_create_event_on_expired_jwt_with_mock() { let config = C8YHttpConfig { c8y_http_host: target_url.clone(), c8y_mqtt_host: target_url.clone(), + c8y_username: None, + c8y_password: None, device_id: external_id.into(), tmp_dir: tmp_dir.into(), identity: None, @@ -676,6 +680,8 @@ async fn spawn_c8y_http_proxy( let config = C8YHttpConfig { c8y_http_host: c8y_host.clone(), c8y_mqtt_host: c8y_host, + c8y_username: None, + c8y_password: None, device_id, tmp_dir, identity: None, diff --git a/crates/extensions/tedge_http_ext/Cargo.toml b/crates/extensions/tedge_http_ext/Cargo.toml index f4ec491bd56..299b05fe31b 100644 --- a/crates/extensions/tedge_http_ext/Cargo.toml +++ b/crates/extensions/tedge_http_ext/Cargo.toml @@ -16,7 +16,7 @@ test_helpers = [] [dependencies] async-trait = { workspace = true } -base64 = { workspace = true } +base64 = { workspace = true } futures = { workspace = true } http = { workspace = true } hyper = { workspace = true, default-features = false, features = [ diff --git a/crates/extensions/tedge_http_ext/src/messages.rs b/crates/extensions/tedge_http_ext/src/messages.rs index 004a31ab592..7eac15077d3 100644 --- a/crates/extensions/tedge_http_ext/src/messages.rs +++ b/crates/extensions/tedge_http_ext/src/messages.rs @@ -142,10 +142,21 @@ impl HttpRequestBuilder { self.header(http::header::AUTHORIZATION, header_value) } - pub fn with_auth(self, token: Option, username: Option, password: Option) -> Self - { + pub fn with_auth( + self, + token: Option, + username: Option, + password: Option, + ) -> Self { let header_value = if username.is_some() && password.is_some() { - format!("Basic {}", base64::encode(format!("{}:{}", username.unwrap_or_default(), password.unwrap_or_default()))) + format!( + "Basic {}", + base64::encode(format!( + "{}:{}", + username.unwrap_or_default(), + password.unwrap_or_default() + )) + ) } else { format!("Bearer {}", token.unwrap_or_default()) }; 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, diff --git a/plugins/c8y_remote_access_plugin/src/auth.rs b/plugins/c8y_remote_access_plugin/src/auth.rs index 9c24629d20c..9dcaf596fcf 100644 --- a/plugins/c8y_remote_access_plugin/src/auth.rs +++ b/plugins/c8y_remote_access_plugin/src/auth.rs @@ -6,9 +6,17 @@ pub struct Jwt(String); impl Jwt { pub fn authorization_header(&self) -> String { - let use_legacy_auth = std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); + let use_legacy_auth = std::env::var("C8Y_DEVICE_USER").is_ok() + && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); if use_legacy_auth { - format!("Basic {}", base64::encode(format!("{}/{}:{}", std::env::var("C8Y_DEVICE_TENANT").unwrap(), std::env::var("C8Y_DEVICE_USER").unwrap(), std::env::var("C8Y_DEVICE_PASSWORD").unwrap()))) + format!( + "Basic {}", + base64::encode(format!( + "{}:{}", + std::env::var("C8Y_DEVICE_USER").unwrap(), + std::env::var("C8Y_DEVICE_PASSWORD").unwrap() + )) + ) } else { format!("Bearer {}", self.0) } From 53c607eff6d05b82de02d5f8845f0abdd310d3d6 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Fri, 26 Jul 2024 00:22:52 +0200 Subject: [PATCH 6/7] bypass connection check Signed-off-by: Reuben Miller --- .../tedge_config/src/tedge_config_cli/tedge_config.rs | 2 +- crates/core/tedge/src/cli/connect/command.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 4f6384da9a8..95924e2146d 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 @@ -75,7 +75,7 @@ impl TEdgeConfig { } pub fn use_legacy_auth(&self) -> bool { - return std::env::var("C8Y_DEVICE_TENANT").is_ok() && std::env::var("C8Y_DEVICE_USER").is_ok() && std::env::var("C8Y_DEVICE_PASSWORD").is_ok(); + !self.c8y.username.is_empty() && !self.c8y.password.is_empty() } pub fn mqtt_config(&self) -> Result { diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 2a0a495db58..df890146728 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -286,6 +286,12 @@ fn check_device_status_c8y(tedge_config: &TEdgeConfig) -> Result Date: Fri, 26 Jul 2024 15:57:54 +0200 Subject: [PATCH 7/7] add smartrest 1.0 system tests Signed-off-by: Reuben Miller --- .../libraries/ThinEdgeIO/ThinEdgeIO.py | 27 ++++++ .../requirements/requirements.txt | 2 +- .../RobotFramework/resources/common.resource | 2 +- .../cumulocity/smartrest_one/override.conf | 2 + .../smartrest_one/register-device.service | 14 +++ .../smartrest_one/register-device.sh | 74 ++++++--------- .../smartrest_one/smartrest_one.robot | 94 ++++++++++++++++--- .../debian-systemd/debian-systemd.dockerfile | 4 +- 8 files changed, 160 insertions(+), 59 deletions(-) create mode 100644 tests/RobotFramework/tests/cumulocity/smartrest_one/override.conf create mode 100644 tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.service diff --git a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py index 6dc4e730739..fc41f39011b 100644 --- a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py +++ b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py @@ -893,6 +893,33 @@ def get_bridge_service_name(self, cloud: str) -> str: # Legacy mosquitto bridge return f"mosquitto-{cloud}-bridge" + 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/requirements/requirements.txt b/tests/RobotFramework/requirements/requirements.txt index 274d79cfdce..2291d5ae8dd 100644 --- a/tests/RobotFramework/requirements/requirements.txt +++ b/tests/RobotFramework/requirements/requirements.txt @@ -2,7 +2,7 @@ dateparser~=1.2.0 paho-mqtt~=1.6.1 python-dotenv~=1.0.0 robotframework~=7.0.0 -robotframework-c8y @ git+https://github.com/reubenmiller/robotframework-c8y.git@0.35.2 +robotframework-c8y @ git+https://github.com/reubenmiller/robotframework-c8y.git@0.36.1 robotframework-debuglibrary~=2.5.0 robotframework-jsonlibrary~=0.5 robotframework-pabot~=2.18.0 diff --git a/tests/RobotFramework/resources/common.resource b/tests/RobotFramework/resources/common.resource index 4a039f5308f..9784b0e7bac 100644 --- a/tests/RobotFramework/resources/common.resource +++ b/tests/RobotFramework/resources/common.resource @@ -7,7 +7,7 @@ ${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} host=%{AWS_URL= } diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/override.conf b/tests/RobotFramework/tests/cumulocity/smartrest_one/override.conf new file mode 100644 index 00000000000..89df8bcf3e9 --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/override.conf @@ -0,0 +1,2 @@ +[Service] +EnvironmentFile=-/etc/tedge/c8y-mqtt.env diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.service b/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.service new file mode 100644 index 00000000000..702e8cb7dc7 --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.service @@ -0,0 +1,14 @@ +[Unit] +Description=Cumulocity device registration service +After=syslog.target network.target mosquitto.service + +[Service] +User=tedge +EnvironmentFile=-/etc/tedge/c8y-bootstrap.env +ExecStart=/usr/bin/register-device.sh +Restart=on-failure +RestartPreventExitStatus=255 +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh b/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh index 4d5d3762c6f..b27c6809d2b 100755 --- a/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/register-device.sh @@ -1,70 +1,56 @@ #!/bin/sh set -e -install_dependencies() { - if ! command -V c8y >/dev/null 2>&1; then - curl https://reubenmiller.github.io/go-c8y-cli-repo/debian/PUBLIC.KEY | gpg --dearmor | sudo tee /usr/share/keyrings/go-c8y-cli-archive-keyring.gpg >/dev/null - sudo sh -c "echo 'deb [signed-by=/usr/share/keyrings/go-c8y-cli-archive-keyring.gpg] http://reubenmiller.github.io/go-c8y-cli-repo/debian stable main' >> /etc/apt/sources.list" - sudo apt-get update - sudo apt-get install -y --no-install-recommends go-c8y-cli - fi -} - register_device() { DEVICE_ID=$(tedge config get device.id) C8Y_HOST=$(tedge config get c8y.url) export C8Y_HOST export CI=true - + RESP= + + echo "-----------------------------------------------------" + echo "Device registration: $DEVICE_ID" + echo "" + echo "Please register the above device id in Cumulocity IoT" + echo "-----------------------------------------------------" + echo "Waiting..." while :; do - echo "Device registration loop: $DEVICE_ID" - CREDS=$(c8y deviceregistration getCredentials --id "$DEVICE_ID" --sessionUsername "$C8Y_BOOTSTRAP_USER" --sessionPassword "$C8Y_BOOTSTRAP_PASSWORD" --select tenantid,username,password -o csv ||:) - if [ -n "$CREDS" ]; then - break + RESP=$( + curl -sf -XPOST "https://$C8Y_HOST/devicecontrol/deviceCredentials" \ + --user "${C8Y_BOOTSTRAP_USER}:${C8Y_BOOTSTRAP_PASSWORD}" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "{\"id\":\"$DEVICE_ID\"}" || : + ) + if [ -n "$RESP" ]; then + HAS_TENANT=$(echo "$RESP" | jq -r 'has("tenantId") and has("username") and has("password")' 2>/dev/null ||:) + if [ "$HAS_TENANT" = "true" ]; then + break + fi fi sleep 5 done + echo "Received device credentials" - DEVICE_TENANT=$(echo "$CREDS" | cut -d, -f1) - DEVICE_USERNAME=$(echo "$CREDS" | cut -d, -f2) - DEVICE_PASSWORD=$(echo "$CREDS" | cut -d, -f3) + C8Y_DEVICE_USER=$(echo "$RESP" | jq -r '[.tenantId, .username] | join("/")') + C8Y_DEVICE_PASSWORD=$(echo "$RESP" | jq -r '.password') # Save credentials + PASSWORD_ESCAPED=$(echo "$C8Y_DEVICE_PASSWORD" | sed 's|\$|\\$|g') cat << EOT > /etc/tedge/c8y-mqtt.env -DEVICE_TENANT="$DEVICE_TENANT" -DEVICE_USERNAME="$DEVICE_USERNAME" -DEVICE_PASSWORD="$DEVICE_PASSWORD" +C8Y_DEVICE_USER="$C8Y_DEVICE_USER" +C8Y_DEVICE_PASSWORD="$PASSWORD_ESCAPED" EOT - - # Show banner - # echo - # echo "--------------- device credentials --------------" - # echo "DEVICE_TENANT: $DEVICE_TENANT" - # echo "DEVICE_USERNAME: $DEVICE_USERNAME" - # echo "DEVICE_PASSWORD: $DEVICE_PASSWORD" - # echo "-------------------------------------------------" } -install_dependencies - if [ ! -f /etc/tedge/c8y-mqtt.env ]; then register_device +else + echo "Device is already registered" fi # shellcheck disable=SC1091 . /etc/tedge/c8y-mqtt.env -if [ -f /etc/tedge/mosquitto-conf/c8y-bridge.conf ]; then - echo "Updating c8y bridge username/password" - if ! grep -q remote_username /etc/tedge/mosquitto-conf/c8y-bridge.conf; then - sed -i 's|bridge_certfile .*|remote_username '"$DEVICE_TENANT/$DEVICE_USERNAME"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf - sed -i 's|bridge_keyfile .*|remote_password '"$DEVICE_PASSWORD"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf - else - sed -i 's|remote_username .*|remote_username '"$DEVICE_TENANT/$DEVICE_USERNAME"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf - sed -i 's|remote_password .*|remote_password '"$DEVICE_PASSWORD"'|' /etc/tedge/mosquitto-conf/c8y-bridge.conf - fi - - # TODO delete JWT topics - # topic s/uat out 0 c8y/ "" - # topic s/dat in 0 c8y/ "" -fi +tedge config set c8y.username "$C8Y_DEVICE_USER" +tedge config set c8y.password "$C8Y_DEVICE_PASSWORD" diff --git a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot index e6549bf09da..00f2ec1e764 100644 --- a/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot +++ b/tests/RobotFramework/tests/cumulocity/smartrest_one/smartrest_one.robot @@ -4,22 +4,92 @@ Library Cumulocity Library ThinEdgeIO Test Tags theme:c8y theme:operation -Test Setup Custom Setup -Test Teardown Get Logs +Test Teardown Custom Teardown + +*** 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 *** -Register SmartREST 1 templates - ${TEMPLATE_XID}= Set Variable templateXIDexample01 - # Execute Command cmd=curl -XPOST -H "X-Id: templateXIDexample01" -d "10,107,GET,/inventory/managedObjects/%%/childDevices?pageSize=100,,,%%,," http://127.0.0.1:8001/c8y/s - Execute Command cmd=tedge mqtt pub c8y/s/ul "15,${TEMPLATE_XID}\n10,107,GET,/inventory/managedObjects/%%/childDevices?pageSize=100,,,%%,,\n" - Log debug - # 10,107,GET,/inventory/managedObjects/%%/childDevices?pageSize=100,,,%%,,\n - # Should Have MQTT Messages c8y/s/us message_pattern=114,c8y_DownloadConfigFile,c8y_LogfileRequest,c8y_RemoteAccessConnect,c8y_Restart,c8y_SoftwareUpdate,c8y_UploadConfigFile minimum=1 maximum=1 - +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 tedge config set c8y.username "${CREDENTIALS.username}" log_output=${False} + Execute Command tedge config set c8y.password "${CREDENTIALS.password}" log_output=${False} + Execute Command cmd=printf 'C8Y_DEVICE_USER="%s"\nC8Y_DEVICE_PASSWORD="%s"\n' "${CREDENTIALS.username}" "${CREDENTIALS.password}" > /etc/tedge/c8y-mqtt.env + +Register Device Using Bootstrap Credentials + [Arguments] ${SERIAL} + + # setup registration service + Transfer To Device ${CURDIR}/register-device.sh /usr/bin/register-device.sh + Transfer To Device ${CURDIR}/register-device.service /lib/systemd/system/register-device.service + Execute Command cmd=printf 'C8Y_BOOTSTRAP_USER=%s\nC8Y_BOOTSTRAP_PASSWORD=%s\n' '${C8Y_CONFIG.bootstrap_username}' '${C8Y_CONFIG.bootstrap_password}' > /etc/tedge/c8y-bootstrap.env log_output=${False} + Execute Command systemctl daemon-reload + + # Start background registration service + Execute Command systemctl start register-device.service + + # Register device in the platform and then approve it (after the background service connects as well) + Cumulocity.Register Device With Basic Auth external_id=${SERIAL} + Custom Setup - ${DEVICE_SN}= 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} + + # Allow mapper to read env variable from file + Transfer To Device ${CURDIR}/override.conf /etc/systemd/system/tedge-mapper-c8y.service.d/override.conf + Execute Command systemctl daemon-reload + Set Suite Variable $DEVICE_SN - Device Should Exist ${DEVICE_SN} + + Register Device ${DEVICE_SN} + +Custom Teardown + Get Logs + IF $TEMPLATE_XID + Delete SmartREST 1.0 Template ${TEMPLATE_XID} + END diff --git a/tests/images/debian-systemd/debian-systemd.dockerfile b/tests/images/debian-systemd/debian-systemd.dockerfile index 9596a50eafe..9187a83f721 100644 --- a/tests/images/debian-systemd/debian-systemd.dockerfile +++ b/tests/images/debian-systemd/debian-systemd.dockerfile @@ -17,7 +17,9 @@ RUN apt-get -y update \ nginx \ netcat-openbsd \ iputils-ping \ - net-tools + net-tools \ + jq \ + jo # Install more recent version of mosquitto >= 2.0.18 from debian backports to avoid mosquitto following bugs: # The mosquitto repo can't be used as it does not included builds for arm64/aarch64 (only amd64 and armhf)