diff --git a/Cargo.lock b/Cargo.lock index d4a722891..16115f448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,7 +333,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "agent" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-debug-echo", "akri-discovery-utils", @@ -402,7 +402,7 @@ dependencies = [ [[package]] name = "akri-debug-echo" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "akri-shared", @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "akri-discovery-utils" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-shared", "anyhow", @@ -444,13 +444,15 @@ dependencies = [ [[package]] name = "akri-onvif" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "akri-shared", "anyhow", "async-trait", + "base64 0.13.1", "bytes 1.4.0", + "chrono", "env_logger", "futures-util", "hyper", @@ -460,6 +462,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_yaml", + "sha1 0.6.1", "sxd-document", "sxd-xpath", "tokio 1.26.0", @@ -472,7 +475,7 @@ dependencies = [ [[package]] name = "akri-opcua" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "akri-shared", @@ -496,7 +499,7 @@ dependencies = [ [[package]] name = "akri-shared" -version = "0.12.1" +version = "0.12.2" dependencies = [ "anyhow", "async-trait", @@ -525,7 +528,7 @@ dependencies = [ [[package]] name = "akri-udev" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "anyhow", @@ -1043,7 +1046,7 @@ checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" [[package]] name = "controller" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-shared", "anyhow", @@ -1243,7 +1246,7 @@ dependencies = [ [[package]] name = "debug-echo-discovery-handler" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-debug-echo", "akri-discovery-utils", @@ -2540,7 +2543,7 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "onvif-discovery-handler" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "akri-onvif", @@ -2590,7 +2593,7 @@ dependencies = [ [[package]] name = "opcua-discovery-handler" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "akri-opcua", @@ -4189,7 +4192,7 @@ dependencies = [ [[package]] name = "udev-discovery-handler" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-discovery-utils", "akri-udev", @@ -4200,7 +4203,7 @@ dependencies = [ [[package]] name = "udev-video-broker" -version = "0.12.1" +version = "0.12.2" dependencies = [ "akri-shared", "env_logger", @@ -4477,7 +4480,7 @@ dependencies = [ [[package]] name = "webhook-configuration" -version = "0.12.1" +version = "0.12.2" dependencies = [ "actix", "actix-rt 2.7.0", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 58d7c370c..10b6b4a27 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring ", ""] edition = "2018" rust-version = "1.68.1" diff --git a/controller/Cargo.toml b/controller/Cargo.toml index c4cf90db9..b2d6c1222 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "controller" -version = "0.12.1" +version = "0.12.2" authors = ["", ""] edition = "2018" rust-version = "1.68.1" diff --git a/deployment/helm/Chart.yaml b/deployment/helm/Chart.yaml index 4f98ce22a..1b008c5c0 100644 --- a/deployment/helm/Chart.yaml +++ b/deployment/helm/Chart.yaml @@ -16,9 +16,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.12.1 +version: 0.12.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 0.12.1 +appVersion: 0.12.2 diff --git a/deployment/helm/templates/onvif-configuration.yaml b/deployment/helm/templates/onvif-configuration.yaml index 59b37757d..27d7c74a5 100644 --- a/deployment/helm/templates/onvif-configuration.yaml +++ b/deployment/helm/templates/onvif-configuration.yaml @@ -40,6 +40,42 @@ spec: items: [] {{- end }} discoveryTimeoutSeconds: {{ .Values.onvif.configuration.discoveryDetails.discoveryTimeoutSeconds }} + {{- if .Values.onvif.configuration.discoveryProperties}} + discoveryProperties: + {{- range $property := .Values.onvif.configuration.discoveryProperties }} + - name: {{ $property.name }} + {{- if $property.valueFrom }} + valueFrom: + {{- if $property.valueFrom.secretKeyRef }} + secretKeyRef: + name: {{ $property.valueFrom.secretKeyRef.name }} + {{- if $property.valueFrom.secretKeyRef.namespace }} + namespace: {{ $property.valueFrom.secretKeyRef.namespace }} + {{- end }} + {{- if $property.valueFrom.secretKeyRef.key }} + key: {{ $property.valueFrom.secretKeyRef.key }} + {{- end }} + {{- if hasKey $property.valueFrom.secretKeyRef "optional" }} + optional: {{ $property.valueFrom.secretKeyRef.optional }} + {{- end }} + {{- else if $property.valueFrom.configMapKeyRef}} + configMapKeyRef: + name: {{ $property.valueFrom.configMapKeyRef.name }} + {{- if $property.valueFrom.configMapKeyRef.namespace }} + namespace: {{ $property.valueFrom.configMapKeyRef.namespace }} + {{- end }} + {{- if $property.valueFrom.configMapKeyRef.key }} + key: {{ $property.valueFrom.configMapKeyRef.key }} + {{- end }} + {{- if hasKey $property.valueFrom.configMapKeyRef "optional" }} + optional: {{ $property.configMapKeyRef.optional }} + {{- end }} + {{- end }} + {{- else }} + value: {{ $property.value | quote }} + {{- end }} + {{- end }} + {{- end }} {{- if or .Values.onvif.configuration.brokerPod.image.repository .Values.onvif.configuration.brokerJob.image.repository }} {{- /* Only add brokerSpec if a broker image is provided */}} brokerSpec: diff --git a/deployment/helm/values.yaml b/deployment/helm/values.yaml index 56f9a0437..94a97eab0 100644 --- a/deployment/helm/values.yaml +++ b/deployment/helm/values.yaml @@ -428,6 +428,9 @@ onvif: action: Exclude items: [] discoveryTimeoutSeconds: 1 + # discoveryProperties is a map of properties fthat will be passed to discovery handler, + # the properties can be direct specified or read from Secret or ConfigMap + discoveryProperties: # capacity is the capacity for any instances created as a result of # applying this onvif configuration capacity: 1 diff --git a/discovery-handler-modules/debug-echo-discovery-handler/Cargo.toml b/discovery-handler-modules/debug-echo-discovery-handler/Cargo.toml index ccd80f191..769a0b5a5 100644 --- a/discovery-handler-modules/debug-echo-discovery-handler/Cargo.toml +++ b/discovery-handler-modules/debug-echo-discovery-handler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "debug-echo-discovery-handler" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-handler-modules/onvif-discovery-handler/Cargo.toml b/discovery-handler-modules/onvif-discovery-handler/Cargo.toml index 100a206f1..82fd9f6de 100644 --- a/discovery-handler-modules/onvif-discovery-handler/Cargo.toml +++ b/discovery-handler-modules/onvif-discovery-handler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "onvif-discovery-handler" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-handler-modules/opcua-discovery-handler/Cargo.toml b/discovery-handler-modules/opcua-discovery-handler/Cargo.toml index dcf8a1190..71187890f 100644 --- a/discovery-handler-modules/opcua-discovery-handler/Cargo.toml +++ b/discovery-handler-modules/opcua-discovery-handler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opcua-discovery-handler" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-handler-modules/udev-discovery-handler/Cargo.toml b/discovery-handler-modules/udev-discovery-handler/Cargo.toml index 78b00e98a..88c22ed1c 100644 --- a/discovery-handler-modules/udev-discovery-handler/Cargo.toml +++ b/discovery-handler-modules/udev-discovery-handler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "udev-discovery-handler" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-handlers/debug-echo/Cargo.toml b/discovery-handlers/debug-echo/Cargo.toml index e8134c8d2..25a36185b 100644 --- a/discovery-handlers/debug-echo/Cargo.toml +++ b/discovery-handlers/debug-echo/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akri-debug-echo" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-handlers/onvif/Cargo.toml b/discovery-handlers/onvif/Cargo.toml index eee67558f..e627701bf 100644 --- a/discovery-handlers/onvif/Cargo.toml +++ b/discovery-handlers/onvif/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akri-onvif" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" @@ -12,7 +12,9 @@ akri-discovery-utils = { path = "../../discovery-utils" } akri-shared = { path = "../../shared" } anyhow = "1.0.38" async-trait = "0.1.0" +base64 = "0.13.1" bytes = "1.0.1" +chrono = "0.4.10" env_logger = "0.10.0" futures-util = "0.3" hyper = { version = "0.14.11", package = "hyper" } @@ -21,6 +23,7 @@ serde = "1.0.104" serde_json = "1.0.45" serde_yaml = "0.8.11" serde_derive = "1.0.104" +sha1 = "0.6.1" sxd-document = "0.3.0" sxd-xpath = "0.4.0" tokio = { version = "1.0", features = ["time", "net", "sync"] } diff --git a/discovery-handlers/onvif/src/credential_store.rs b/discovery-handlers/onvif/src/credential_store.rs new file mode 100644 index 000000000..a9f99fad0 --- /dev/null +++ b/discovery-handlers/onvif/src/credential_store.rs @@ -0,0 +1,714 @@ +use akri_discovery_utils::discovery::v0::ByteData; +use std::collections::HashMap; + +/// Key name of device credential list in discoveryProperties +pub const DEVICE_CREDENTIAL_LIST: &str = "device_credential_list"; +/// Key name of device credential ref list in discoveryProperties +pub const DEVICE_CREDENTIAL_REF_LIST: &str = "device_credential_ref_list"; +/// Key name prefix of username credential list in discoveryProperties +pub const DEVICE_CREDENTIAL_USERNAME_PREFIX: &str = "username_"; +/// Key name prefix of password credential list in discoveryProperties +pub const DEVICE_CREDENTIAL_PASSWORD_PREFIX: &str = "password_"; +/// Key name of default username +pub const DEVICE_CREDENTIAL_DEFAULT_USERNAME: &str = "username_default"; +/// Key name of default password +pub const DEVICE_CREDENTIAL_DEFAULT_PASSWORD: &str = "password_default"; +/// Name of default credential for querying CredentialStore +pub const DEFAULT_CREDENTIAL_ID: &str = "default"; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CredentialData { + username: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + password: Option, + #[serde(default)] + base64encoded: bool, +} + +impl CredentialData { + fn get_username(&self) -> String { + self.username.clone() + } + + fn get_password(&self) -> Option { + if self.base64encoded { + self.password + .as_ref() + .and_then(|encoded_data| decode_base64_str(encoded_data)) + } else { + self.password.clone() + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CredentialRefData { + username_ref: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + password_ref: Option, +} + +#[derive(Default)] +pub struct CredentialStore { + credentials: HashMap)>, +} + +impl CredentialStore { + pub fn new(credential_data: &HashMap) -> Self { + let mut store = Self::default(); + store.process_credential_list(credential_data); + store.process_credential_ref_list(credential_data); + store.process_username_password(credential_data); + store.process_default_username_password(credential_data); + store + } + + pub fn get(&self, uuid: &str) -> Option<(String, Option)> { + self.credentials + .get(uuid) + .or_else(|| self.credentials.get(DEFAULT_CREDENTIAL_ID)) + .map(|(n, p)| (n.to_string(), p.as_ref().map(|p| p.to_string()))) + } + + fn process_credential_list(&mut self, credential_data: &HashMap) { + let result = self.process_list_data( + DEVICE_CREDENTIAL_LIST, + credential_data, + |list_content, _credential_data| parse_credential_list(list_content), + ); + self.credentials.extend(result); + } + + fn process_credential_ref_list(&mut self, credential_data: &HashMap) { + let result = self.process_list_data( + DEVICE_CREDENTIAL_REF_LIST, + credential_data, + parse_credential_ref_list, + ); + self.credentials.extend(result); + } + + fn process_list_data( + &mut self, + key: &str, + credential_data: &HashMap, + parse: F, + ) -> HashMap)> + where + F: Fn(&str, &HashMap) -> HashMap)>, + { + parse_list_data(key, credential_data) + .unwrap_or_default() + .into_iter() + .filter_map(|list| credential_data.get(&list).and_then(byte_data_to_str)) + .flat_map(|list_content| parse(list_content, credential_data)) + .collect() + } + + fn process_username_password(&mut self, credential_data: &HashMap) { + let username_list = credential_data + .iter() + .filter_map(|(key, value)| { + key.strip_prefix(DEVICE_CREDENTIAL_USERNAME_PREFIX) + .and_then(|device_uuid| { + let username = byte_data_to_str(value)?; + if !device_uuid.is_empty() { + Some((device_uuid, username)) + } else { + None + } + }) + }) + .collect::>(); + if username_list.is_empty() { + return; + } + + let mut password_list = credential_data + .iter() + .filter_map(|(key, value)| { + key.strip_prefix(DEVICE_CREDENTIAL_PASSWORD_PREFIX) + .and_then(|device_uuid| { + let password = byte_data_to_str(value); + if !device_uuid.is_empty() { + Some((device_uuid, password)) + } else { + None + } + }) + }) + .collect::>>(); + + let result = username_list + .into_iter() + .map(|(device_uuid, username)| { + let password = password_list + .remove(&device_uuid) + .and_then(|opt_s| opt_s.map(|s| s.to_string())); + // Credential data key is in C_IDENTIFIER format + // convert it back to uuid string format by replacing "_" with "-" + ( + device_uuid.replace('_', "-"), + (username.to_string(), password), + ) + }) + .collect::)>>(); + self.credentials.extend(result); + } + + fn process_default_username_password(&mut self, credential_data: &HashMap) { + let default_credential = credential_data + .get(DEVICE_CREDENTIAL_DEFAULT_USERNAME) + .and_then(byte_data_to_str) + .map(|username| username.to_string()) + .map(|username| { + let password = credential_data + .get(DEVICE_CREDENTIAL_DEFAULT_PASSWORD) + .and_then(byte_data_to_str) + .map(|password| password.to_string()); + (username, password) + }); + if let Some(credential) = default_credential { + self.credentials + .insert(DEFAULT_CREDENTIAL_ID.to_string(), credential); + } + } +} + +fn parse_credential_list(credential_list: &str) -> HashMap)> { + serde_json::from_str::>(credential_list) + .unwrap_or_default() + .into_iter() + .map(|(id, cred_data)| (id, (cred_data.get_username(), cred_data.get_password()))) + .collect() +} + +fn parse_credential_ref_list( + credential_ref_list: &str, + credential_data: &HashMap, +) -> HashMap)> { + serde_json::from_str::>(credential_ref_list) + .unwrap_or_default() + .into_iter() + .filter_map(|(id, cred_ref)| { + let username = credential_data + .get(&cred_ref.username_ref) + .and_then(byte_data_to_str) + .map(|n| n.to_string())?; + if username.is_empty() { + return None; + } + + let password = cred_ref + .password_ref + .map(|pwd| { + credential_data + .get(&pwd) + .and_then(byte_data_to_str) + .map(|p| p.to_string()) + }) + .unwrap_or_default(); + Some((id, (username, password))) + }) + .collect() +} + +fn parse_list_data(key: &str, credential_data: &HashMap) -> Option> { + credential_data + .get(key) + .and_then(byte_data_to_str) + .and_then(|list_json_str| serde_json::from_str::>(list_json_str).ok()) +} + +fn byte_data_to_str(byte_data: &ByteData) -> Option<&str> { + byte_data + .vec + .as_ref() + .and_then(|s| std::str::from_utf8(s).ok()) +} + +fn decode_base64_str(encoded_data: &str) -> Option { + let decoded_data = base64::decode(encoded_data).ok()?; + std::str::from_utf8(&decoded_data) + .map(|s| s.to_string()) + .ok() +} + +#[cfg(test)] +mod tests { + use super::*; + struct DeviceCredentialData<'a> { + pub id: String, + pub username: Option<&'a [u8]>, + pub password: Option<&'a [u8]>, + } + + fn generate_list_credential_data( + list_name: &str, + entries: Vec<(&str, Option<&[u8]>)>, + ) -> HashMap { + let credential_list_value = format!( + "[{}]", + entries + .iter() + .map(|(k, _v)| format!(r#""{}""#, k)) + .collect::>() + .join(", ") + ); + + let list_data = HashMap::from([generate_credential_data_entry( + list_name, + Some(credential_list_value.as_bytes()), + )]); + + entries + .into_iter() + .map(|(k, v)| generate_credential_data_entry(k, v)) + .chain(list_data.into_iter()) + .collect::>() + } + + fn generate_username_password_credential_data( + entries: Vec, + ) -> HashMap { + entries + .into_iter() + .flat_map(|entry| { + let username_key = format!("{}{}", DEVICE_CREDENTIAL_USERNAME_PREFIX, entry.id); + let password_key = format!("{}{}", DEVICE_CREDENTIAL_PASSWORD_PREFIX, entry.id); + HashMap::from([ + generate_credential_data_entry(&username_key, entry.username), + generate_credential_data_entry(&password_key, entry.password), + ]) + }) + .collect::>() + } + + fn generate_credential_data_entry(key: &str, value: Option<&[u8]>) -> (String, ByteData) { + ( + key.to_string(), + ByteData { + vec: value.map(|p| p.to_vec()), + }, + ) + } + + #[test] + fn test_credential_store_empty() { + let _ = env_logger::builder().is_test(true).try_init(); + let credential_data = HashMap::new(); + + let credential_store = CredentialStore::new(&credential_data); + assert!(credential_store.credentials.is_empty()); + } + + #[test] + fn test_credential_store_non_utf8_username() { + let _ = env_logger::builder().is_test(true).try_init(); + let test_data = vec![("deviceid-1", vec![200u8, 200u8, 200u8], "password_1")]; + let test_entries = test_data + .iter() + .map(|(id, uname, pwd)| DeviceCredentialData { + id: id.replace('-', "_"), + username: Some(uname as &[u8]), + password: Some(pwd.as_bytes()), + }) + .collect::>(); + let credential_data = generate_username_password_credential_data(test_entries); + + let credential_store = CredentialStore::new(&credential_data); + assert!(credential_store.credentials.is_empty()); + } + + #[test] + fn test_credential_store_non_utf8_password() { + let _ = env_logger::builder().is_test(true).try_init(); + let test_data = vec![("deviceid-1", "username_1", vec![200u8, 200u8, 200u8])]; + let expected_result = test_data + .iter() + .map(|(id, uname, pwd)| { + ( + id.to_string(), + ( + uname.to_string(), + std::str::from_utf8(pwd).map(|s| s.to_string()).ok(), + ), + ) + }) + .collect::)>>(); + let test_entries = test_data + .iter() + .map(|(id, uname, pwd)| DeviceCredentialData { + id: id.replace('-', "_"), + username: Some(uname.as_bytes()), + password: Some(pwd as &[u8]), + }) + .collect::>(); + let credential_data = generate_username_password_credential_data(test_entries); + + let credential_store = CredentialStore::new(&credential_data); + assert_eq!(credential_store.credentials, expected_result); + } + + fn build_default_username_password_data() -> HashMap { + let secret_data = vec![("default", "default_username", "default_password")]; + let secret_test_data = secret_data + .iter() + .map(|(id, uname, pwd)| DeviceCredentialData { + id: id.replace('-', "_"), + username: Some(uname.as_bytes()), + password: Some(pwd.as_bytes()), + }) + .collect::>(); + generate_username_password_credential_data(secret_test_data) + } + + fn build_username_password_data() -> HashMap { + let secret_data = vec![( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc", + "username_5f", + "password_5f", + )]; + let secret_test_data = secret_data + .iter() + .map(|(id, uname, pwd)| DeviceCredentialData { + id: id.replace('-', "_"), + username: Some(uname.as_bytes()), + password: Some(pwd.as_bytes()), + }) + .collect::>(); + generate_username_password_credential_data(secret_test_data) + } + + fn build_device_credential_list_data() -> HashMap { + let credential_list_key = "device_credential_list"; + let secret_list1_key = "secret_list1"; + let secret_list1_value = r#" + { + "5f5a69c2-e0ae-504f-829b-00fcdab169cc" : + { + "username" : "uname_1", + "password" : "password_1" + }, + "6a67158b-42b1-400b-8afe-1bec9a5d7909": + { + "username" : "uname_3", + "password" : "YWRtaW4=", + "base64encoded": true + } + }"#; + + let secret_list2_key = "secret_list2"; + let secret_list2_value = r#" + { + "7a21dc67-8438-5588-1547-4d1349048438" : + { + "username" : "uname_2", + "password" : "password_2" + } + } + "#; + let secret_list_test_data = vec![ + (secret_list1_key, Some(secret_list1_value.as_bytes())), + (secret_list2_key, Some(secret_list2_value.as_bytes())), + ]; + generate_list_credential_data(credential_list_key, secret_list_test_data) + } + + fn build_device_credential_ref_list_data() -> HashMap { + let credential_ref_list_key = "device_credential_ref_list"; + let secret_ref_list1_key = "secret_ref_list1"; + let secret_ref_list1_value = r#" + { + "5f5a69c2-e0ae-504f-829b-00fcdab169cc" : + { + "username_ref" : "device_1_username", + "password_ref" : "device_1_password" + } + } + "#; + + let secret_ref_list2_key = "secret_ref_list2"; + let secret_ref_list2_value = r#" + { + "7a21dc67-8438-5588-1547-4d1349048438" : + { + "username_ref" : "device_2_username", + "password_ref" : "device_2_password" + } + } + "#; + let secret_ref_username_password = vec![ + ("device_1_username", "user_foo"), + ("device_1_password", "password_foo"), + ("device_2_username", "user_bar"), + ("device_2_password", "password_bar"), + ]; + + let secret_ref_list_test_data = vec![ + ( + secret_ref_list1_key, + Some(secret_ref_list1_value.as_bytes()), + ), + ( + secret_ref_list2_key, + Some(secret_ref_list2_value.as_bytes()), + ), + ]; + let credential_ref_list_data = + generate_list_credential_data(credential_ref_list_key, secret_ref_list_test_data); + secret_ref_username_password + .iter() + .map(|(k, v)| generate_credential_data_entry(k, Some(v.as_bytes()))) + .chain(credential_ref_list_data.into_iter()) + .collect::>() + } + + #[test] + fn test_credential_store_default_username_password() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = ( + "default_username".to_string(), + Some("default_password".to_string()), + ); + let credential_data = build_default_username_password_data(); + + let credential_store = CredentialStore::new(&credential_data); + assert_eq!(credential_store.get("any_id"), Some(expected_result)); + } + + #[test] + fn test_credential_store_username_password() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + )]); + let credential_data = build_username_password_data(); + + let credential_store = CredentialStore::new(&credential_data); + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_credential_store_device_credential_list() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([ + ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("uname_1".to_string(), Some("password_1".to_string())), + ), + ( + "6a67158b-42b1-400b-8afe-1bec9a5d7909".to_string(), + ("uname_3".to_string(), Some("admin".to_string())), + ), + ( + "7a21dc67-8438-5588-1547-4d1349048438".to_string(), + ("uname_2".to_string(), Some("password_2".to_string())), + ), + ]); + let credential_data = build_device_credential_list_data(); + + let credential_store = CredentialStore::new(&credential_data); + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_credential_store_device_credential_ref_list() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([ + ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("user_foo".to_string(), Some("password_foo".to_string())), + ), + ( + "7a21dc67-8438-5588-1547-4d1349048438".to_string(), + ("user_bar".to_string(), Some("password_bar".to_string())), + ), + ]); + let credential_data = build_device_credential_ref_list_data(); + let credential_store = CredentialStore::new(&credential_data); + + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_credential_store_device_credential_list_and_username_password() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([ + ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ), + ( + "6a67158b-42b1-400b-8afe-1bec9a5d7909".to_string(), + ("uname_3".to_string(), Some("admin".to_string())), + ), + ( + "7a21dc67-8438-5588-1547-4d1349048438".to_string(), + ("uname_2".to_string(), Some("password_2".to_string())), + ), + ]); + let credential_list_data = build_device_credential_list_data(); + let username_password_data = build_username_password_data(); + let credential_data = HashMap::new() + .into_iter() + .chain(credential_list_data.into_iter()) + .chain(username_password_data.into_iter()) + .collect(); + let credential_store = CredentialStore::new(&credential_data); + + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_credential_store_device_credential_list_and_credential_ref_list() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([ + ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("user_foo".to_string(), Some("password_foo".to_string())), + ), + ( + "6a67158b-42b1-400b-8afe-1bec9a5d7909".to_string(), + ("uname_3".to_string(), Some("admin".to_string())), + ), + ( + "7a21dc67-8438-5588-1547-4d1349048438".to_string(), + ("user_bar".to_string(), Some("password_bar".to_string())), + ), + ]); + let credential_list_data = build_device_credential_list_data(); + let credential_ref_list_data = build_device_credential_ref_list_data(); + let credential_data = HashMap::new() + .into_iter() + .chain(credential_list_data.into_iter()) + .chain(credential_ref_list_data.into_iter()) + .collect(); + let credential_store = CredentialStore::new(&credential_data); + + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_credential_store_device_credential_ref_list_and_username_password() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([ + ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ), + ( + "7a21dc67-8438-5588-1547-4d1349048438".to_string(), + ("user_bar".to_string(), Some("password_bar".to_string())), + ), + ]); + let credential_ref_list_data = build_device_credential_ref_list_data(); + let username_password_data = build_username_password_data(); + let credential_data = HashMap::new() + .into_iter() + .chain(credential_ref_list_data.into_iter()) + .chain(username_password_data.into_iter()) + .collect(); + let credential_store = CredentialStore::new(&credential_data); + + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_credential_store_device_credential_all() { + let _ = env_logger::builder().is_test(true).try_init(); + let expected_result = HashMap::from([ + ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ), + ( + "6a67158b-42b1-400b-8afe-1bec9a5d7909".to_string(), + ("uname_3".to_string(), Some("admin".to_string())), + ), + ( + "7a21dc67-8438-5588-1547-4d1349048438".to_string(), + ("user_bar".to_string(), Some("password_bar".to_string())), + ), + ]); + let credential_list_data = build_device_credential_list_data(); + let credential_ref_list_data = build_device_credential_ref_list_data(); + let username_password_data = build_username_password_data(); + let credential_data = HashMap::new() + .into_iter() + .chain(credential_list_data.into_iter()) + .chain(credential_ref_list_data.into_iter()) + .chain(username_password_data.into_iter()) + .collect(); + let credential_store = CredentialStore::new(&credential_data); + + assert_eq!(credential_store.credentials, expected_result); + } + + #[test] + fn test_get_credential_found_no_default() { + let _ = env_logger::builder().is_test(true).try_init(); + let credential = ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ); + let credentials = HashMap::from([credential.clone()]); + let credential_store = CredentialStore { credentials }; + let result = credential_store.get(&credential.0); + assert_eq!(result, Some(credential.1)); + } + + #[test] + fn test_get_credential_not_found_no_default() { + let _ = env_logger::builder().is_test(true).try_init(); + let credential = ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ); + let credentials = HashMap::from([credential]); + let credential_store = CredentialStore { credentials }; + let result = credential_store.get("not-exist-uuid"); + assert!(result.is_none()); + } + + #[test] + fn test_get_credential_found_with_default() { + let _ = env_logger::builder().is_test(true).try_init(); + let credential = ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ); + let default_credential = ( + "default".to_string(), + ( + "default_username".to_string(), + Some("default_password".to_string()), + ), + ); + let credentials = HashMap::from([credential.clone(), default_credential]); + let credential_store = CredentialStore { credentials }; + let result = credential_store.get(&credential.0); + assert_eq!(result, Some(credential.1)); + } + + #[test] + fn test_get_credential_not_found_with_default() { + let _ = env_logger::builder().is_test(true).try_init(); + let credential = ( + "5f5a69c2-e0ae-504f-829b-00fcdab169cc".to_string(), + ("username_5f".to_string(), Some("password_5f".to_string())), + ); + let default_credential = ( + "default".to_string(), + ( + "default_username".to_string(), + Some("default_password".to_string()), + ), + ); + let credentials = HashMap::from([credential, default_credential.clone()]); + let credential_store = CredentialStore { credentials }; + let result = credential_store.get("not-exist-uuid"); + assert_eq!(result, Some(default_credential.1)); + } +} diff --git a/discovery-handlers/onvif/src/discovery_handler.rs b/discovery-handlers/onvif/src/discovery_handler.rs index eeaef2f82..472adfffa 100644 --- a/discovery-handlers/onvif/src/discovery_handler.rs +++ b/discovery-handlers/onvif/src/discovery_handler.rs @@ -1,3 +1,4 @@ +use super::credential_store::CredentialStore; use super::discovery_impl::util; use super::discovery_utils::{ OnvifQuery, OnvifQueryImpl, ONVIF_DEVICE_IP_ADDRESS_LABEL_ID, @@ -75,6 +76,8 @@ impl DiscoveryHandler for DiscoveryHandlerImpl { let discovery_handler_config: OnvifDiscoveryDetails = deserialize_discovery_details(&discover_request.discovery_details) .map_err(|e| tonic::Status::new(tonic::Code::InvalidArgument, format!("{}", e)))?; + let credential_store = CredentialStore::new(&discover_request.discovery_properties); + let onvif_query = OnvifQueryImpl::new(credential_store); tokio::spawn(async move { let mut previous_cameras = HashMap::new(); let mut filtered_camera_devices = HashMap::new(); @@ -88,7 +91,6 @@ impl DiscoveryHandler for DiscoveryHandlerImpl { break; } let mut changed_camera_list = false; - let onvif_query = OnvifQueryImpl {}; trace!("discover - filters:{:?}", &discovery_handler_config,); let mut socket = util::get_discovery_response_socket().await.unwrap(); @@ -172,7 +174,7 @@ async fn apply_filters( } let (ip_address, mac_address) = match onvif_query - .get_device_ip_and_mac_address(device_service_uri) + .get_device_ip_and_mac_address(device_service_uri, device_uuid) .await { Ok(ip_and_mac) => ip_and_mac, @@ -254,8 +256,8 @@ mod tests { ) { mock.expect_get_device_ip_and_mac_address() .times(1) - .withf(move |u| u == uri) - .returning(move |_| Ok((ip.to_string(), mac.to_string()))); + .withf(move |u, _uuid| u == uri) + .returning(move |_, _| Ok((ip.to_string(), mac.to_string()))); } fn expected_device(uri: &str, uuid: &str, ip: &str, mac: &str) -> (String, Device) { diff --git a/discovery-handlers/onvif/src/discovery_impl.rs b/discovery-handlers/onvif/src/discovery_impl.rs index 6812babc2..bff8bf906 100644 --- a/discovery-handlers/onvif/src/discovery_impl.rs +++ b/discovery-handlers/onvif/src/discovery_impl.rs @@ -610,7 +610,7 @@ pub mod util { "simple_onvif_discover - uris after filtering by scopes {:?}", filtered_uris ); - let devices = get_responsive_uris(filtered_uris, &OnvifQueryImpl {}).await; + let devices = get_responsive_uris(filtered_uris, &OnvifQueryImpl::default()).await; info!("simple_onvif_discover - devices: {:?}", devices); Ok(devices) } diff --git a/discovery-handlers/onvif/src/discovery_utils.rs b/discovery-handlers/onvif/src/discovery_utils.rs index 7dd9ee172..b94f58b01 100644 --- a/discovery-handlers/onvif/src/discovery_utils.rs +++ b/discovery-handlers/onvif/src/discovery_utils.rs @@ -1,3 +1,5 @@ +use super::credential_store::CredentialStore; +use super::username_token::UsernameToken; use async_trait::async_trait; use futures_util::stream::TryStreamExt; use hyper::Request; @@ -24,6 +26,7 @@ pub trait OnvifQuery { async fn get_device_ip_and_mac_address( &self, service_url: &str, + device_uuid: &str, ) -> Result<(String, String), anyhow::Error>; async fn get_device_service_uri( &self, @@ -39,7 +42,16 @@ pub trait OnvifQuery { async fn is_device_responding(&self, url: &str) -> Result; } -pub struct OnvifQueryImpl {} +#[derive(Default)] +pub struct OnvifQueryImpl { + credential_store: CredentialStore, +} + +impl OnvifQueryImpl { + pub fn new(credential_store: CredentialStore) -> Self { + Self { credential_store } + } +} #[async_trait] impl OnvifQuery for OnvifQueryImpl { @@ -47,9 +59,11 @@ impl OnvifQuery for OnvifQueryImpl { async fn get_device_ip_and_mac_address( &self, service_url: &str, + device_uuid: &str, ) -> Result<(String, String), anyhow::Error> { + let credential = self.credential_store.get(device_uuid); let http = HttpRequest {}; - inner_get_device_ip_and_mac_address(service_url, &http).await + inner_get_device_ip_and_mac_address(service_url, credential, &http).await } /// Gets specific service, like media, from a given ONVIF camera @@ -173,13 +187,18 @@ fn get_action(wsdl: &str, function: &str) -> String { /// Gets the ip and mac address for a given ONVIF camera async fn inner_get_device_ip_and_mac_address( service_url: &str, + credential: Option<(String, Option)>, http: &impl Http, ) -> Result<(String, String), anyhow::Error> { + let username_token = credential.map(|(uname, passwd)| { + UsernameToken::new(uname.as_str(), passwd.unwrap_or_default().as_str()) + }); + let message = get_network_interfaces_message(&username_token); let network_interfaces_xml = match http .post( service_url, &get_action(DEVICE_WSDL, "GetNetworkInterfaces"), - GET_NETWORK_INTERFACES_TEMPLATE, + message.as_str(), ) .await { @@ -229,13 +248,49 @@ async fn inner_get_device_ip_and_mac_address( Ok((ip_address, mac_address)) } +fn get_soap_security_header(username_token: &UsernameToken) -> String { + format!( + r#" + + + {} + {} + {} + {} + + + "#, + username_token.username, + username_token.digest, + username_token.nonce, + username_token.created + ) +} + /// SOAP request body for getting the network interfaces for an ONVIF camera -const GET_NETWORK_INTERFACES_TEMPLATE: &str = r#" - - - - - "#; +fn get_network_interfaces_message(username_token: &Option) -> String { + let security_header = if let Some(username_token) = username_token { + get_soap_security_header(username_token) + } else { + "".to_string() + }; + + format!( + r#" + + + {} + + + + +"#, + security_header + ) +} /// Gets a specific service (like media) uri from an ONVIF camera async fn inner_get_device_service_uri( @@ -285,10 +340,10 @@ async fn inner_get_device_service_uri( /// SOAP request body for getting the supported services' uris for an ONVIF camera const GET_SERVICES_TEMPLATE: &str = r#" - - - - "#; + + + +"#; /// Gets list of media profiles for a given ONVIF camera async fn inner_get_device_profiles( @@ -378,18 +433,18 @@ fn get_stream_uri_message(profile: &str) -> String { format!( r#" - - - - RTP-Unicast - - RTSP - - - {} - - - ;"#, + + + + RTP-Unicast + + RTSP + + + {} + + +;"#, profile ) } @@ -397,10 +452,10 @@ fn get_stream_uri_message(profile: &str) -> String { /// SOAP request body for getting the media profiles for an ONVIF camera const GET_PROFILES_TEMPLATE: &str = r#" - - - - "#; + + + +"#; // const GET_DEVICE_INFORMATION_TEMPLATE: &str = r#" // @@ -411,10 +466,10 @@ const GET_PROFILES_TEMPLATE: &str = r#" - - - - "#; + + + +"#; #[cfg(test)] mod tests { @@ -443,17 +498,20 @@ mod tests { let mut mock = MockHttp::new(); let response = "\ntrueeth000:12:41:5c:a1:a51500false10Fullfalse10Full0true192.168.1.3624false"; + let username_token = None; + let message = get_network_interfaces_message(&username_token); configure_post( &mut mock, "test_inner_get_device_ip_and_mac_address-url", &get_action(DEVICE_WSDL, "GetNetworkInterfaces"), - GET_NETWORK_INTERFACES_TEMPLATE, + &message, response, ); assert_eq!( ("192.168.1.36".to_string(), "00:12:41:5c:a1:a5".to_string()), inner_get_device_ip_and_mac_address( "test_inner_get_device_ip_and_mac_address-url", + None, &mock ) .await @@ -467,11 +525,13 @@ mod tests { let mut mock = MockHttp::new(); let response = "\ntrueeth000:FC:DA:B1:69:CC1500true10.137.185.208010.137.185.20823true\r\n"; + let username_token = None; + let message = get_network_interfaces_message(&username_token); configure_post( &mut mock, "test_inner_get_device_ip_and_mac_address-url", &get_action(DEVICE_WSDL, "GetNetworkInterfaces"), - GET_NETWORK_INTERFACES_TEMPLATE, + &message, response, ); assert_eq!( @@ -481,6 +541,7 @@ mod tests { ), inner_get_device_ip_and_mac_address( "test_inner_get_device_ip_and_mac_address-url", + None, &mock ) .await diff --git a/discovery-handlers/onvif/src/lib.rs b/discovery-handlers/onvif/src/lib.rs index 2fe844b10..228140ed0 100644 --- a/discovery-handlers/onvif/src/lib.rs +++ b/discovery-handlers/onvif/src/lib.rs @@ -1,6 +1,8 @@ +mod credential_store; pub mod discovery_handler; mod discovery_impl; mod discovery_utils; +mod username_token; #[macro_use] extern crate serde_derive; diff --git a/discovery-handlers/onvif/src/username_token.rs b/discovery-handlers/onvif/src/username_token.rs new file mode 100644 index 000000000..3cbea0538 --- /dev/null +++ b/discovery-handlers/onvif/src/username_token.rs @@ -0,0 +1,53 @@ +/// This implements the Username token profile described in ONVIF Core Spec 5.9.4 +/// which is based on [WS-UsernameToken]: https://docs.oasis-open.org/wss/v1.1/wss-v1.1-spec-pr-UsernameTokenProfile-01.htm +#[derive(Default, Debug, Clone)] +pub struct UsernameToken { + pub username: String, + pub nonce: String, + pub digest: String, + pub created: String, +} + +impl UsernameToken { + pub fn new(username: &str, password: &str) -> UsernameToken { + let nonce = uuid::Uuid::new_v4().to_string(); + let created = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); + Self::generate_token(username, password, &nonce, &created) + } + + fn generate_token(username: &str, password: &str, nonce: &str, created: &str) -> UsernameToken { + let concat = format!("{}{}{}", nonce, created, password); + let digest = { + let mut hasher = sha1::Sha1::new(); + hasher.update(concat.as_bytes()); + hasher.digest().bytes() + }; + + UsernameToken { + username: username.to_string(), + nonce: base64::encode(nonce), + digest: base64::encode(digest), + created: created.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_token() { + let username = "abcdefe"; + let password = "1234567"; + let nonce = "nonce"; + let created = "2000-01-01T12:34:56:789Z"; + + let username_token = UsernameToken::generate_token(username, password, nonce, created); + + assert_eq!(username_token.username, username); + assert_eq!(username_token.created, created); + assert_eq!(username_token.nonce, "bm9uY2U="); + assert_eq!(username_token.digest, "AGsoQQ+qNJu6Ha7h/QAPoQvYcV0="); + } +} diff --git a/discovery-handlers/opcua/Cargo.toml b/discovery-handlers/opcua/Cargo.toml index 84e299930..fb3201194 100644 --- a/discovery-handlers/opcua/Cargo.toml +++ b/discovery-handlers/opcua/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akri-opcua" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-handlers/udev/Cargo.toml b/discovery-handlers/udev/Cargo.toml index cdc1599c1..1ac2c6cc1 100644 --- a/discovery-handlers/udev/Cargo.toml +++ b/discovery-handlers/udev/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akri-udev" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/discovery-utils/Cargo.toml b/discovery-utils/Cargo.toml index a6f3e6a04..3ecbd385d 100644 --- a/discovery-utils/Cargo.toml +++ b/discovery-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akri-discovery-utils" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring "] edition = "2018" rust-version = "1.68.1" diff --git a/samples/brokers/udev-video-broker/Cargo.toml b/samples/brokers/udev-video-broker/Cargo.toml index 3ff422ab3..85103f07a 100644 --- a/samples/brokers/udev-video-broker/Cargo.toml +++ b/samples/brokers/udev-video-broker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "udev-video-broker" -version = "0.12.1" +version = "0.12.2" authors = ["Kate Goldenring ", ""] edition = "2018" rust-version = "1.68.1" diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 7843534e3..08f5ef379 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "akri-shared" -version = "0.12.1" +version = "0.12.2" authors = [""] edition = "2018" rust-version = "1.68.1" diff --git a/version.txt b/version.txt index 34a83616b..26acbf080 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.12.1 +0.12.2 diff --git a/webhooks/validating/configuration/Cargo.toml b/webhooks/validating/configuration/Cargo.toml index a5c7fee99..6009f7a69 100644 --- a/webhooks/validating/configuration/Cargo.toml +++ b/webhooks/validating/configuration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "webhook-configuration" -version = "0.12.1" +version = "0.12.2" authors = ["DazWilkin "] edition = "2018" rust-version = "1.68.1"