Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: check compatibility of thin-edge.io with Cumulocity IoT basic auth device users and SmartREST 1.0 #3039

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions crates/common/certificate/src/parse_root_certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ where
.with_no_client_auth())
}

pub fn create_tls_config_without_client_cert(
root_certificates: impl AsRef<Path>,
) -> Result<ClientConfig, CertificateError> {
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<Path>,
Expand Down
8 changes: 8 additions & 0 deletions crates/common/download/src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,17 @@ impl DownloadInfo {
pub enum Auth {
/// HTTP Bearer authentication
Bearer(String),
Basic(Option<String>, Option<String>),
}

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.
Expand Down Expand Up @@ -409,6 +414,9 @@ 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 {
Expand Down
19 changes: 19 additions & 0 deletions crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ impl TEdgeConfig {
Self(TEdgeConfigReader::from_dto(dto, location))
}

pub fn use_legacy_auth(&self) -> bool {
!self.c8y.username.is_empty() && !self.c8y.password.is_empty()
}

pub fn mqtt_config(&self) -> Result<mqtt_channel::Config, CertificateError> {
let host = self.mqtt.client.host.as_str();
let port = u16::from(self.mqtt.client.port);
Expand Down Expand Up @@ -465,12 +469,27 @@ 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"))]
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")]
Expand Down
6 changes: 6 additions & 0 deletions crates/core/c8y_api/src/http_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ impl C8yMqttJwtTokenRetriever {
}

pub async fn get_jwt_token(&mut self) -> Result<SmartRestJwtResponse, JwtError> {
// TODO: Remove this hack
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);

Expand Down
2 changes: 2 additions & 0 deletions crates/core/tedge/src/bridge/aws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl From<BridgeConfigAwsParams> 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(),
Expand Down Expand Up @@ -106,6 +107,7 @@ fn test_bridge_config_from_aws_params() -> anyhow::Result<()> {
connection: "edge_to_aws".into(),
address: HostPort::<MQTT_TLS_PORT>::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(),
Expand Down
2 changes: 2 additions & 0 deletions crates/core/tedge/src/bridge/azure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ impl From<BridgeConfigAzureParams> 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(),
Expand Down Expand Up @@ -103,6 +104,7 @@ fn test_bridge_config_from_azure_params() -> anyhow::Result<()> {
connection: "edge_to_az".into(),
address: HostPort::<MQTT_TLS_PORT>::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(),
Expand Down
65 changes: 61 additions & 4 deletions crates/core/tedge/src/bridge/c8y.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ pub struct BridgeConfigC8yParams {
pub mqtt_host: HostPort<MQTT_TLS_PORT>,
pub config_file: String,
pub remote_clientid: String,
pub remote_username: Option<String>,
pub remote_password: Option<String>,
pub bridge_root_cert_path: Utf8PathBuf,
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,
}
Expand All @@ -30,10 +33,13 @@ impl From<BridgeConfigC8yParams> for BridgeConfig {
config_file,
bridge_root_cert_path,
remote_clientid,
remote_username,
remote_password,
bridge_certfile,
bridge_keyfile,
smartrest_templates,
include_local_clean_session,
smartrest_one_templates,
bridge_location,
} = params;

Expand Down Expand Up @@ -62,11 +68,17 @@ impl From<BridgeConfigC8yParams> 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic should be opposite. If use_legacy_auth == true, not adding JWT token topics to bridge.

Suggested change
if use_legacy_auth {
if !use_legacy_auth {

Question: Do we even have to change the bridge file as per auth method? If mapper doesn't subscribe to JWT token, isn't it enough? I mean the bridge still maps the JWT tokens c8y/, but no actor subscribes to the JWT topic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think it still needs to be conditional because otherwise an error message is published on the s/e topic

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()
Expand All @@ -83,6 +95,35 @@ impl From<BridgeConfigC8yParams> for BridgeConfig {
.collect::<Vec<String>>();
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::<Vec<String>>();
topics.extend(templates_set);
}

let include_local_clean_session = match include_local_clean_session {
AutoFlag::True => true,
AutoFlag::False => false,
Expand All @@ -94,7 +135,8 @@ impl From<BridgeConfigC8yParams> 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(),
Expand Down Expand Up @@ -159,10 +201,13 @@ mod tests {
mqtt_host: HostPort::<MQTT_TLS_PORT>::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(),
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,
};
Expand All @@ -175,6 +220,7 @@ mod tests {
connection: "edge_to_c8y".into(),
address: HostPort::<MQTT_TLS_PORT>::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(),
Expand Down Expand Up @@ -217,6 +263,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(),
Expand Down
20 changes: 18 additions & 2 deletions crates/core/tedge/src/bridge/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct BridgeConfig {
pub connection: String,
pub address: HostPort<MQTT_TLS_PORT>,
pub remote_username: Option<String>,
pub remote_password: Option<String>,
pub bridge_root_cert_path: Utf8PathBuf,
pub remote_clientid: String,
pub local_clientid: String,
Expand Down Expand Up @@ -63,8 +64,19 @@ 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)?;

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)?;
Expand Down Expand Up @@ -156,6 +168,7 @@ mod test {
connection: "edge_to_test".into(),
address: HostPort::<MQTT_TLS_PORT>::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(),
Expand Down Expand Up @@ -223,6 +236,7 @@ bridge_attempt_unsubscribe false
connection: "edge_to_test".into(),
address: HostPort::<MQTT_TLS_PORT>::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(),
Expand Down Expand Up @@ -289,6 +303,7 @@ bridge_attempt_unsubscribe false
connection: "edge_to_az".into(),
address: HostPort::<MQTT_TLS_PORT>::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(),
Expand Down Expand Up @@ -410,6 +425,7 @@ bridge_attempt_unsubscribe false
connection: "edge_to_az/c8y".into(),
address: HostPort::<MQTT_TLS_PORT>::from_str("test.com").unwrap(),
remote_username: None,
remote_password: None,
bridge_root_cert_path: "".into(),
bridge_certfile: "".into(),
bridge_keyfile: "".into(),
Expand Down
25 changes: 20 additions & 5 deletions crates/core/tedge/src/cli/connect/c8y_direct_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading