diff --git a/Cargo.lock b/Cargo.lock index e0ef31dc..ea7bd058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -556,11 +556,13 @@ dependencies = [ "axum", "chrono", "clap", + "common", "cucumber", "databroker-proto", "futures", "jemallocator", "jsonwebtoken", + "kuksa", "lazy_static", "prost", "prost-types", diff --git a/kuksa_databroker/databroker/Cargo.toml b/kuksa_databroker/databroker/Cargo.toml index d48df296..cde54975 100644 --- a/kuksa_databroker/databroker/Cargo.toml +++ b/kuksa_databroker/databroker/Cargo.toml @@ -23,6 +23,8 @@ name = "databroker" path = "src/lib.rs" [dependencies] +common = { path = "../lib"} +kuksa = { path = "../lib/kuksa"} databroker-proto = { workspace = true } tonic = { workspace = true, features = ["transport", "channel", "prost"] } prost = { workspace = true } @@ -78,6 +80,7 @@ vergen = { version = "8", features = [ [dev-dependencies] anyhow = "1.0" +chrono = "^0.4" cucumber = { version = "0.20", default-features = false, features = ["libtest", "macros"] } [[test]] diff --git a/kuksa_databroker/databroker/tests/features/read_write_values.feature b/kuksa_databroker/databroker/tests/features/read_write_values.feature index 14f4ede7..a49a8581 100644 --- a/kuksa_databroker/databroker/tests/features/read_write_values.feature +++ b/kuksa_databroker/databroker/tests/features/read_write_values.feature @@ -1,9 +1,65 @@ Feature: Reading and writing values of a VSS Data Entry + Rule: Access with right permissions succeeds and fails with wrong/no permissions + + Background: + Given a running Databroker server with authorization enabled with the following Data Entries registered + | path | data type | change type | type | + | Vehicle.Speed | float | Static | Sensor | + | Vehicle.ADAS.ABS.IsEnabled | bool | Static | Actuator | + + Scenario: Writing the current value of an unset Data Entry without authenticating fails + When a client sets the current value of Vehicle.Width of type float to 13.4 + Then the operation fails with status code 16 + + Scenario: Read the current value of an unset Data Entry without authenticating fails + When a client gets the current value of Vehicle.Width + Then the operation fails with status code 16 + + Scenario: Writing the current value of a Data Entry without right permissions fails + When a client uses a token with scope read + And a client sets the current value of Vehicle.Speed of type float to 13.4 + Then setting the value for Vehicle.Speed fails with error code 403 + + Scenario: Writing the current value of a Data Entry without right permissions fails + When a client uses a token with scope actuate + And a client sets the current value of Vehicle.Speed of type float to 13.4 + Then setting the value for Vehicle.Speed fails with error code 403 + + Scenario: Writing the current value of a Data Entry without right permissions fails + When a client uses a token with scope provide:Vehicle.ADAS.ABS.IsEnabled + And a client sets the current value of Vehicle.Speed of type float to 13.4 + Then setting the value for Vehicle.Speed fails with error code 403 + + Scenario: Writing the current value of a Data Entry with right permissions succeeds + When a client uses a token with scope provide:Vehicle.Speed + And a client sets the current value of Vehicle.Speed of type float to 13.4 + Then the set operation succeeds + + Scenario: Writing the target value of a Data Entry without right permissions fails + When a client uses a token with scope read + And a client sets the target value of Vehicle.ADAS.ABS.IsEnabled of type bool to true + Then setting the value for Vehicle.Speed fails with error code 403 + + Scenario: Writing the target value of a Data Entry without right permissions fails + When a client uses a token with scope provide + And a client sets the target value of Vehicle.ADAS.ABS.IsEnabled of type bool to true + Then setting the value for Vehicle.Speed fails with error code 403 + + Scenario: Writing the target value of a Data Entry without right permissions fails + When a client uses a token with scope actuate:Vehicle.Speed + And a client sets the target value of Vehicle.ADAS.ABS.IsEnabled of type bool to true + Then setting the value for Vehicle.Speed fails with error code 403 + + Scenario: Writing the target value of a Data Entry with right permissions succeeds + When a client uses a token with scope actuate:Vehicle.ADAS.ABS.IsEnabled + And a client sets the target value of Vehicle.ADAS.ABS.IsEnabled of type bool to true + Then the set operation succeeds + Rule: Accessing unregistered Data Entries fails Background: - Given a running Databroker server + Given a running Databroker server with authorization disabled Scenario: Setting the current value of an unregistered Data Entry fails When a client sets the current value of No.Such.Path of type float to 13.4 @@ -24,7 +80,7 @@ Feature: Reading and writing values of a VSS Data Entry Rule: Target values can only be set on Actuators Background: - Given a running Databroker server with the following Data Entries registered + Given a running Databroker server with authorization disabled with the following Data Entries registered | path | data type | change type | type | | Vehicle.Powertrain.Range | uint32 | Continuous | Sensor | | Vehicle.Width | uint16 | Static | Attribute | @@ -40,7 +96,7 @@ Feature: Reading and writing values of a VSS Data Entry Rule: Accessing registered Data Entries works Background: - Given a running Databroker server with the following Data Entries registered + Given a running Databroker server with authorization disabled with the following Data Entries registered | path | data type | change type | type | | Vehicle.Cabin.Lights.AmbientLight | uint8 | OnChange | Actuator | | Vehicle.Cabin.Sunroof.Position | int8 | OnChange | Actuator | diff --git a/kuksa_databroker/databroker/tests/read_write_values.rs b/kuksa_databroker/databroker/tests/read_write_values.rs index 88243ecf..b127d673 100644 --- a/kuksa_databroker/databroker/tests/read_write_values.rs +++ b/kuksa_databroker/databroker/tests/read_write_values.rs @@ -12,14 +12,11 @@ ********************************************************************************/ use core::panic; -use std::{future, time::SystemTime, vec}; +use std::{collections::HashMap, future, time::SystemTime, vec}; use cucumber::{cli, gherkin::Step, given, then, when, writer, World as _}; use databroker::broker; -use databroker_proto::kuksa::val::v1::{ - datapoint::Value, DataEntry, DataType, Datapoint, EntryRequest, EntryUpdate, Field, GetRequest, - SetRequest, View, -}; +use databroker_proto::kuksa::val::v1::{datapoint::Value, DataType, Datapoint}; use tracing::debug; use world::{DataBrokerWorld, ValueType}; @@ -75,9 +72,19 @@ fn get_data_entries_from_table( data_entries } -#[given(regex = "^a running Databroker server.*$")] -async fn start_databroker_server(w: &mut DataBrokerWorld, step: &Step) { - w.start_databroker(get_data_entries_from_table(step)).await; +#[given(regex = "^a running Databroker server with authorization (enabled|disabled).*$")] +async fn start_databroker_server(w: &mut DataBrokerWorld, auth: String, step: &Step) { + let authorization_enabled: bool; + if auth == "enabled" { + authorization_enabled = true; + } else if auth == "disabled" { + authorization_enabled = false; + } else { + panic!("Not a known authorization keyword use enabled/disabled!") + } + + w.start_databroker(get_data_entries_from_table(step), authorization_enabled) + .await; assert!(w.broker_client.is_some()) } @@ -90,7 +97,22 @@ async fn a_known_data_entry_has_value( value: String, ) { set_value(w, value_type, path, data_type, value).await; - w.assert_set_response_has_succeeded() + w.assert_set_succeeded() +} + +#[when(expr = "a client uses a token with scope {word}")] +async fn authorize_client(w: &mut DataBrokerWorld, scope: String) { + let token = w.create_token(scope); + w.broker_client + .as_mut() + .and_then(|client| match client.basic_client.set_access_token(token) { + Ok(()) => Some(client), + Err(e) => { + println!("Error: {e}"); + None + } + }) + .expect("no Databroker client available, broker not started?"); } #[when(expr = "a client sets the {word} value of {word} of type {word} to {word}")] @@ -111,42 +133,34 @@ async fn set_value( value: Some(value), }; - let data_entry = match value_type { - ValueType::Current => DataEntry { - path: path.clone(), - value: Some(datapoint), - actuator_target: None, - metadata: None, - }, - ValueType::Target => DataEntry { - path: path.clone(), - value: None, - actuator_target: Some(datapoint), - metadata: None, - }, - }; - let req = SetRequest { - updates: vec![EntryUpdate { - entry: Some(data_entry), - fields: vec![ - Field::ActuatorTarget.into(), - Field::Value.into(), - Field::Path.into(), - ], - }], - }; - match client.set(req).await { - Ok(res) => { - let set_response = res.into_inner(); - debug!( - "response from Databroker [global error: {:?}, Data Entry errors: {:?}]", - set_response.error, set_response.errors - ); - w.current_set_response = Some(set_response); + match value_type { + ValueType::Target => { + match client + .set_target_values(HashMap::from([(path.clone(), datapoint.clone())])) + .await + { + Ok(_) => { + w.current_client_error = None; + } + Err(e) => { + debug!("failed to invoke Databroker's set operation: {:?}", e); + w.current_client_error = Some(e); + } + } } - Err(e) => { - debug!("failed to invoke Databroker's set operation: {:?}", e); - w.current_status = Some(e); + ValueType::Current => { + match client + .set_current_values(HashMap::from([(path.clone(), datapoint.clone())])) + .await + { + Ok(_) => { + w.current_client_error = None; + } + Err(e) => { + debug!("failed to invoke Databroker's set operation: {:?}", e); + w.current_client_error = Some(e); + } + } } } } @@ -157,22 +171,21 @@ async fn get_value(w: &mut DataBrokerWorld, value_type: ValueType, path: String) .broker_client .as_mut() .expect("no Databroker client available, broker not started?"); - let get_request = GetRequest { - entries: vec![EntryRequest { - path: path.to_string(), - view: match value_type { - ValueType::Current => View::CurrentValue.into(), - ValueType::Target => View::TargetValue.into(), - }, - fields: vec![Field::Value.into(), Field::Metadata.into()], - }], - }; - match client.get(get_request).await { - Ok(res) => w.current_get_response = Some(res.into_inner()), - Err(e) => { - debug!("failed to invoke Databroker's get operation: {:?}", e); - w.current_status = Some(e); - } + match value_type { + ValueType::Target => match client.get_target_values(vec![&path]).await { + Ok(res) => w.current_data_entries = Some(res), + Err(e) => { + debug!("failed to invoke Databroker's get operation: {:?}", e); + w.current_client_error = Some(e); + } + }, + ValueType::Current => match client.get_current_values(vec![path]).await { + Ok(res) => w.current_data_entries = Some(res), + Err(e) => { + debug!("failed to invoke Databroker's get operation: {:?}", e); + w.current_client_error = Some(e); + } + }, } } @@ -224,18 +237,12 @@ fn assert_value_is_unspecified(w: &mut DataBrokerWorld, value_type: ValueType, p #[then(regex = r"^the (current|target) value is not found$")] fn assert_value_not_found(w: &mut DataBrokerWorld) { - let error_code = w - .current_get_response - .clone() - .and_then(|res| res.error) - .map(|error| error.code); - - assert_eq!(error_code, Some(404)); + w.assert_response_has_error_code(vec![404]); } #[then(expr = "setting the value for {word} fails with error code {int}")] -fn assert_set_request_failure(w: &mut DataBrokerWorld, path: String, expected_error_code: u32) { - w.assert_set_response_has_error_code(path, expected_error_code) +fn assert_set_request_failure(w: &mut DataBrokerWorld, _path: String, expected_error_code: u32) { + w.assert_response_has_error_code(vec![expected_error_code]) } /// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc @@ -244,6 +251,11 @@ fn assert_request_failure(w: &mut DataBrokerWorld, expected_status_code: i32) { w.assert_status_has_code(expected_status_code) } +#[then(expr = "the set operation succeeds")] +fn assert_set_succeeds(w: &mut DataBrokerWorld) { + w.assert_set_succeeded() +} + #[tokio::main] async fn main() { // databroker::init_logging(); diff --git a/kuksa_databroker/databroker/tests/world/mod.rs b/kuksa_databroker/databroker/tests/world/mod.rs index d758cf5c..18fbe067 100644 --- a/kuksa_databroker/databroker/tests/world/mod.rs +++ b/kuksa_databroker/databroker/tests/world/mod.rs @@ -19,23 +19,26 @@ use std::{ task::{Poll, Waker}, }; +use chrono::Utc; +use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; + +use common::ClientError; +use databroker_proto::kuksa::val::v1::{datapoint::Value, DataEntry}; + use databroker::{ broker, grpc::{self, server::ServerTLS}, permissions, }; -use databroker_proto::kuksa::val::v1::{ - datapoint::Value, val_client::ValClient, DataEntry, GetResponse, SetResponse, -}; + use tokio::net::TcpListener; -use tonic::{ - transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity}, - Code, Status, -}; use tracing::debug; use lazy_static::lazy_static; +use tonic::transport::{Certificate, ClientTlsConfig, Identity}; +use tonic::Code; + #[cfg(feature = "tls")] lazy_static! { pub static ref CERTS: DataBrokerCertificates = DataBrokerCertificates::new(); @@ -55,6 +58,16 @@ pub enum ValueType { Target, } +#[derive(Debug, serde::Serialize)] +struct Token { + sub: String, + iss: String, + aud: Vec, + iat: i64, + exp: i64, + scope: String, +} + impl FromStr for ValueType { type Err = String; @@ -71,6 +84,8 @@ impl FromStr for ValueType { pub struct DataBrokerCertificates { server_identity: Identity, ca_certs: Certificate, + private_key: String, + public_key: String, } #[cfg(feature = "tls")] @@ -87,9 +102,17 @@ impl DataBrokerCertificates { let ca_file = format!("{cert_dir}/CA.pem"); let ca_store = std::fs::read(ca_file).expect("could not read root CA file"); let ca_certs = Certificate::from_pem(ca_store); + let private_key_file = format!("{cert_dir}/jwt/jwt.key"); + let private_key: String = + std::fs::read_to_string(private_key_file).expect("could not read private key file"); + let public_key_file = format!("{cert_dir}/jwt/jwt.key.pub"); + let public_key: String = + std::fs::read_to_string(public_key_file).expect("could not read public key file"); DataBrokerCertificates { server_identity, ca_certs, + private_key, + public_key, } } @@ -115,19 +138,17 @@ struct DataBrokerState { #[derive(cucumber::World, Debug)] #[world(init = Self::new)] pub struct DataBrokerWorld { - pub current_get_response: Option, - pub current_set_response: Option, - pub current_status: Option, - pub broker_client: Option>, + pub current_data_entries: Option>, + pub current_client_error: Option, + pub broker_client: Option, data_broker_state: Arc>, } impl DataBrokerWorld { pub fn new() -> DataBrokerWorld { DataBrokerWorld { - current_get_response: None, - current_set_response: None, - current_status: None, + current_data_entries: Some(vec![]), + current_client_error: None, data_broker_state: Arc::new(Mutex::new(DataBrokerState { running: false, address: None, @@ -145,6 +166,7 @@ impl DataBrokerWorld { broker::ChangeType, broker::EntryType, )>, + authorization_enabled: bool, ) { { let state = self @@ -194,12 +216,22 @@ impl DataBrokerWorld { state.address = Some(addr); } + let mut _authorization = databroker::authorization::Authorization::Disabled; + + if authorization_enabled { + // public key comes from kuksa.val/kuksa_certificates/jwt/jwt.key.pub + match databroker::authorization::Authorization::new(CERTS.public_key.clone()) { + Ok(auth) => _authorization = auth, + Err(e) => println!("Error: {e}"), + } + } + grpc::server::serve_with_incoming_shutdown( listener, data_broker, #[cfg(feature = "tls")] CERTS.server_tls_config(), - databroker::authorization::Authorization::Disabled, + _authorization, poll_fn(|cx| { let mut state = owned_state .lock() @@ -232,21 +264,23 @@ impl DataBrokerWorld { }); debug!("started Databroker [address: {addr}]"); - #[cfg(not(feature = "tls"))] - let client_endpoint = - Endpoint::from_shared(format!("http://{}:{}", addr.ip(), addr.port())) - .expect("cannot create client endpoint"); + + let data_broker_url = format!("http://{}:{}", addr.ip(), addr.port()); + + self.broker_client = match common::to_uri(data_broker_url.clone()) { + Ok(uri) => Some(kuksa::KuksaClient::new(uri)), + Err(e) => { + println!("Error connecting to {data_broker_url}: {e}"); + None + } + }; + #[cfg(feature = "tls")] - let client_endpoint = - Endpoint::from_shared(format!("https://{}:{}", addr.ip(), addr.port())) - .and_then(|conf| conf.tls_config(CERTS.client_tls_config())) - .expect("cannot create client endpoint"); - - self.broker_client = Some( - ValClient::connect(client_endpoint) - .await - .expect("failed to create Databroker client"), - ); + if let Some(client) = self.broker_client.as_mut() { + client + .basic_client + .set_tls_config(CERTS.client_tls_config()); + } } pub fn stop_databroker(&mut self) { @@ -263,11 +297,9 @@ impl DataBrokerWorld { } pub fn get_current_data_entry(&self, path: String) -> Option { - self.current_get_response.clone().and_then(|res| { - res.entries - .into_iter() - .find(|data_entry| data_entry.path == path) - }) + self.current_data_entries + .clone() + .and_then(|res| res.into_iter().find(|data_entry| data_entry.path == path)) } pub fn get_current_value(&self, path: String) -> Option { @@ -284,34 +316,85 @@ impl DataBrokerWorld { /// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc pub fn assert_status_has_code(&self, expected_status_code: i32) { - assert!( - self.current_status.is_some(), - "operation did not result in an error" - ); - if let Some(status) = self.current_status.clone() { - assert_eq!(status.code(), Code::from_i32(expected_status_code)); + match &self.current_client_error { + Some(ClientError::Connection(_)) => panic!("Connection error shall not occur"), + Some(ClientError::Function(_)) => { + panic!("Fucntion has an error that shall not occur") + } + Some(ClientError::Status(status)) => { + assert_eq!(status.code(), Code::from_i32(expected_status_code)) + } + None => panic!("No error, but an errror is expected"), } } - pub fn assert_set_response_has_error_code(&self, path: String, error_code: u32) { - let code = self.current_set_response.clone().and_then(|res| { - res.errors - .into_iter() - .find(|error| error.path == path) - .and_then(|data_entry_error| data_entry_error.error) - .map(|error| error.code) - }); - assert!(code.is_some(), "response contains no error code"); - assert_eq!( - code, - Some(error_code), - "response contains unexpected error code" - ); + pub fn assert_response_has_error_code(&self, error_codes: Vec) { + let mut code = Vec::new(); + + if let Some(client_error) = self.current_client_error.clone() { + match client_error { + ClientError::Connection(_) => panic!("response contains connection error"), + ClientError::Function(e) => { + for element in e { + if !code.contains(&element.code) { + code.push(element.code) + } + } + } + ClientError::Status(_) => panic!("response contains channel error"), + } + + assert!( + !code.is_empty(), + "response contains no error code {:?}", + code + ); + assert_eq!(code, error_codes, "response contains unexpected error code"); + } else { + panic!("response contains no error code"); + } } - pub fn assert_set_response_has_succeeded(&self) { - if let Some(res) = self.current_set_response.clone() { - assert!(res.error.is_none() && res.errors.is_empty()) + pub fn assert_set_succeeded(&self) { + if let Some(error) = self.current_client_error.clone() { + match error { + ClientError::Connection(e) => { + panic!("No connection error {:?} should occcur", e) + } + ClientError::Function(e) => { + panic!("No function error {:?} should occur", e) + } + ClientError::Status(status) => { + panic!("No status error {:?} should occur", status) + } + } } } + + pub fn create_token(&self, scope: String) -> String { + let datetime = Utc::now(); + let timestamp = datetime.timestamp(); + let timestamp_exp = (match datetime.checked_add_months(chrono::Months::new(24)) { + None => panic!("couldn't add 2 years"), + Some(date) => date, + }) + .timestamp(); + // Your payload as a Rust struct or any serializable type + let payload = Token { + sub: "test dev".to_string(), + iss: "integration test instance".to_string(), + aud: vec!["kuksa.val".to_string()], + iat: timestamp, + exp: timestamp_exp, + scope, + }; + + // Create an encoding key from the private key + let encoding_key = EncodingKey::from_rsa_pem(CERTS.private_key.clone().as_bytes()) + .expect("Failed to create encoding key"); + + // Encode the payload using RS256 algorithm + encode(&Header::new(Algorithm::RS256), &payload, &encoding_key) + .expect("Failed to encode JWT") + } }