use std::collections::BTreeMap;
use std::fs;
use std::sync::Arc;
use serde::{Serialize, Deserialize};
use shvproto::RpcValue;
use shvrpc::client::ClientConfig;
use url::Url;
pub type SharedBrokerConfig = Arc<BrokerConfig>;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BrokerConfig {
#[serde(default)]
pub name: Option<String>,
pub listen: Vec<Listen>,
#[serde(default)]
pub use_access_db: bool,
#[serde(default)]
pub shv2_compatibility: bool,
#[serde(default)]
pub time_broadcast: bool,
#[serde(default)]
pub data_directory: Option<String>,
#[serde(default)]
pub connections: Vec<BrokerConnectionConfig>,
#[serde(default)]
pub access: AccessConfig,
#[serde(default)]
pub tunnelling: TunnellingConfig,
#[serde(default)]
pub azure: Option<AzureConfig>,
}
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
pub struct AzureConfig {
pub group_mapping: Vec<(String, Vec<String>)>,
pub client_id: String,
pub authorize_url: String,
pub token_url: String,
pub scopes: Vec<String>,
}
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
pub struct TunnellingConfig {
#[serde(default)]
pub enabled:bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum ConnectionKind {
ToParentBroker {shv_root: String},
ToChildBroker {shv_root: String, mount_point: String},
}
impl Default for ConnectionKind {
fn default() -> Self {
ConnectionKind::ToParentBroker { shv_root: "".to_string() }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BrokerConnectionConfig {
pub name: String,
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub connection_kind: ConnectionKind,
pub client: ClientConfig,
}
type DeviceId = String;
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct AccessConfig {
pub users: BTreeMap<String, User>,
pub roles: BTreeMap<String, Role>,
pub mounts: BTreeMap<DeviceId, Mount>,
#[serde(default)]
pub allowed_ips: BTreeMap<String, Vec<ipnet::IpNet>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Listen {
pub url: Url,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct User {
pub password: Password,
pub roles: Vec<String>,
#[serde(default)]
pub deactivated: bool,
}
impl User {
pub(crate) fn to_rpcvalue(&self) -> Result<RpcValue, String> {
let cpon = serde_json::to_string(self).map_err(|e| e.to_string())?;
RpcValue::from_cpon(&cpon).map_err(|e| e.to_string())
}
fn from_v2(user: UserV2) -> Result<Self, String> {
Ok(Self {
password: Password::from_v2(user.password)?,
roles: user.roles,
deactivated: false,
})
}
}
impl TryFrom<&RpcValue> for User {
type Error = String;
fn try_from(value: &RpcValue) -> Result<Self, Self::Error> {
let cpon = value.to_cpon();
match serde_json::from_str(&cpon) {
Ok(user) => { Ok(user) }
Err(e) => {
match UserV2::try_from(cpon.as_str()) {
Ok(user) => { User::from_v2(user) }
Err(_) => { Err(e.to_string()) }
}
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum Password {
Plain(String),
Sha1(String),
}
impl Password {
fn from_v2(password: PasswordV2) -> Result<Self, String> {
let format = password.format.to_lowercase();
match format.as_str() {
"plain" => { Ok(Password::Plain(password.password)) }
"sha1" => { Ok(Password::Sha1(password.password)) }
s => { Err(format!("Invalid password format {s}")) }
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserV2 {
pub password: PasswordV2,
pub roles: Vec<String>,
}
impl TryFrom<&str> for UserV2 {
type Error = String;
fn try_from(cpon: &str) -> Result<Self, Self::Error> {
serde_json::from_str(cpon).map_err(|e| e.to_string())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PasswordV2 {
format: String,
password: String,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum ProfileValue {
String(String),
Int(i64),
Bool(bool),
Map(BTreeMap<String, ProfileValue>),
List(Vec<ProfileValue>),
#[default]
Null,
}
impl ProfileValue {
pub fn merge(&mut self, other: ProfileValue) {
match (self, other) {
(ProfileValue::Map(lhs_map), ProfileValue::Map(rhs_map)) => {
for (key, rhs_val) in rhs_map {
match lhs_map.get_mut(&key) {
Some(lhs_val) => lhs_val.merge(rhs_val),
None => {
lhs_map.insert(key, rhs_val);
}
}
}
}
(ProfileValue::List(lhs_list), ProfileValue::List(rhs_list)) => {
lhs_list.extend_from_slice(&rhs_list);
}
_ => ()
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Role {
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub access: Vec<AccessRule>,
#[serde(default)]
pub profile: Option<ProfileValue>,
}
impl Role {
pub(crate) fn to_rpcvalue(&self) -> Result<RpcValue, String> {
let cpon = serde_json::to_string(self).map_err(|e| e.to_string())?;
RpcValue::from_cpon(&cpon).map_err(|e| e.to_string())
}
}
impl TryFrom<&RpcValue> for Role {
type Error = String;
fn try_from(value: &RpcValue) -> Result<Self, Self::Error> {
let cpon = value.to_cpon();
serde_json::from_str(&cpon).map_err(|e| e.to_string())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct AccessRule {
#[serde(rename = "shvRI")]
pub shv_ri: String,
pub grant: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Mount {
#[serde(rename = "mountPoint")]
pub mount_point: String,
#[serde(default)]
pub description: String,
}
impl Mount {
pub(crate) fn to_rpcvalue(&self) -> Result<RpcValue, String> {
let cpon = serde_json::to_string(self).map_err(|e| e.to_string())?;
RpcValue::from_cpon(&cpon).map_err(|e| e.to_string())
}
}
impl TryFrom<&RpcValue> for Mount {
type Error = String;
fn try_from(value: &RpcValue) -> Result<Self, Self::Error> {
let cpon = value.to_cpon();
serde_json::from_str(&cpon).map_err(|e| e.to_string())
}
}
impl AccessConfig {
pub fn from_file(file_name: &str) -> shvrpc::Result<Self> {
let content = fs::read_to_string(file_name)?;
Ok(serde_yaml::from_str(&content)?)
}
}
impl BrokerConfig {
pub fn from_file(file_name: &str) -> shvrpc::Result<Self> {
let content = fs::read_to_string(file_name)?;
Ok(serde_yaml::from_str(&content)?)
}
}
impl Default for BrokerConfig {
fn default() -> Self {
let child_tcp_broker_config = BrokerConnectionConfig {
name: "TCP-to-child-broker".to_string(),
connection_kind: ConnectionKind::ToChildBroker { shv_root: "".to_string(), mount_point: "".to_string() },
..BrokerConnectionConfig::default()
};
let child_serial_broker_config = BrokerConnectionConfig {
name: "serial-to-child-broker".to_string(),
enabled: false,
connection_kind: ConnectionKind::ToChildBroker { shv_root: "".to_string(), mount_point: "test/serial-brc".to_string() },
client: ClientConfig {
url: Url::parse("serial:/dev/ttyACM0?user=test").expect("Serial default URL must be valid"),
..ClientConfig::default()
},
};
let child_can_broker_config = BrokerConnectionConfig {
name: "CAN-to-child-broker".to_string(),
enabled: false,
connection_kind: ConnectionKind::ToChildBroker { shv_root: "".to_string(), mount_point: "test/serial-brc".to_string() },
client: ClientConfig {
url: Url::parse("can:vcan0?local_address=1&peer_address=2&user=test").expect("CAN default URL must be valid"),
..ClientConfig::default()
},
};
Self {
name: Some("foo".into()),
listen: vec![Listen { url: Url::parse("tcp://localhost:3755").expect("TCP default URL should be valid") }],
use_access_db: false,
shv2_compatibility: false,
time_broadcast: false,
data_directory: None,
connections: vec![
BrokerConnectionConfig {
name: "TCP-to-parent-broker".to_string(),
..BrokerConnectionConfig::default()
},
child_tcp_broker_config,
child_serial_broker_config,
child_can_broker_config,
],
access: AccessConfig {
users: BTreeMap::from([
("admin".to_string(), User { password: Password::Plain("admin".into()), roles: vec!["su".to_string()], deactivated: false }),
("user".to_string(), User { password: Password::Plain("user".into()), roles: vec!["client".to_string()], deactivated: false }),
("test".to_string(), User { password: Password::Plain("test".into()), roles: vec!["tester".to_string()], deactivated: false }),
("viewer".to_string(), User { password: Password::Plain("viewer".into()), roles: ["subscribe", "browse"].iter().map(|s| s.to_string()).collect(), deactivated: false }),
("child-broker".to_string(), User { password: Password::Plain("child-broker".into()), roles: vec!["child-broker".to_string()], deactivated: false }),
("tester".to_string(), User { password: Password::Sha1("ab4d8d2a5f480a137067da17100271cd176607a1".into()), roles: vec!["tester".to_string()], deactivated: false }),
]),
roles: BTreeMap::from([
("su".to_string(), Role {
roles: vec![],
access: vec![
AccessRule { shv_ri: "**:*".into(), grant: "su,dot_local".to_string() },
],
profile: None,
}),
("client".to_string(), Role {
roles: vec!["ping".to_string(), "subscribe".to_string(), "browse".to_string()],
access: vec![
AccessRule { shv_ri: ".broker/currentClient:*".into(), grant: "wr".to_string() },
],
profile: None,
}),
("device".to_string(), Role {
roles: vec!["client".to_string()],
access: vec![],
profile: None,
}),
("child-broker".to_string(), Role {
roles: vec!["device".to_string()],
access: vec![],
profile: None,
}),
("tester".to_string(), Role {
roles: vec!["client".to_string()],
access: vec![
AccessRule { shv_ri: ".app/tunnel:create".into(), grant: "wr".to_string() },
AccessRule { shv_ri: ".app/tunnel:ls".into(), grant: "su".to_string() },
AccessRule { shv_ri: ".app/tunnel:dir".into(), grant: "su".to_string() },
AccessRule { shv_ri: "test/**:*".into(), grant: "cfg".to_string() },
],
profile: None,
}),
("ping".to_string(), Role {
roles: vec![],
access: vec![
AccessRule { shv_ri: ".app:ping".into(), grant: "wr".to_string() },
],
profile: None,
}),
("subscribe".to_string(), Role {
roles: vec![],
access: vec![
AccessRule { shv_ri: ".broker/currentClient:subscribe".into(), grant: "wr".to_string() },
AccessRule { shv_ri: ".broker/currentClient:unsubscribe".into(), grant: "wr".to_string() },
],
profile: None,
}),
("browse".to_string(), Role {
roles: vec![],
access: vec![
AccessRule { shv_ri: "**:*".into(), grant: "bws".to_string() },
],
profile: None,
}),
]),
mounts: BTreeMap::from([
("test-device".into(), Mount{ mount_point: "test/device".to_string(), description: "Testing device mount-point".to_string() }),
("test-child-broker".into(), Mount{ mount_point: "test/child-broker".to_string(), description: "Testing child broker mount-point".to_string() }),
]),
allowed_ips: Default::default(),
},
tunnelling: Default::default(),
azure: Default::default(),
}
}
}
#[cfg(test)]
mod tests {
mod profile_value {
use super::super::*;
fn map(entries: impl IntoIterator<Item = (&'static str, ProfileValue)>) -> ProfileValue {
ProfileValue::Map(entries.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
#[test]
fn merge_simple() {
let mut a = ProfileValue::Int(10);
a.merge(ProfileValue::Int(20));
assert_eq!(a, ProfileValue::Int(10));
}
#[test]
fn merge_null_does_not_replace() {
let mut a = ProfileValue::String("Hello".into());
a.merge(ProfileValue::Null);
assert_eq!(a, ProfileValue::String("Hello".into()));
}
#[test]
fn merge_map_adds_new_keys() {
let mut a = map([
("name", ProfileValue::String("Alice".into())),
]);
let b = map([
("age", ProfileValue::Int(30)),
]);
a.merge(b);
assert_eq!(
a,
map([
("name", ProfileValue::String("Alice".into())),
("age", ProfileValue::Int(30)),
])
);
}
#[test]
fn merge_map_does_not_replace_non_map_values() {
let mut a = map([
("name", ProfileValue::String("Alice".into())),
]);
let b = map([
("name", ProfileValue::String("Bob".into())),
]);
a.merge(b);
assert_eq!(
a,
map([("name", ProfileValue::String("Alice".into()))])
);
}
#[test]
fn merge_map_merges_recursively() {
let mut a = map([
("settings", map([
("dark_mode", ProfileValue::Bool(false)),
("volume", ProfileValue::Int(5)),
])),
]);
let b = map([
("settings", map([
("volume", ProfileValue::Int(10)),
("notifications", ProfileValue::Bool(true)),
])),
]);
a.merge(b);
assert_eq!(
a,
map([(
"settings",
map([
("dark_mode", ProfileValue::Bool(false)),
("volume", ProfileValue::Int(5)),
("notifications", ProfileValue::Bool(true)),
])
)])
);
}
#[test]
fn merge_non_map_not_replaced_by_map() {
let mut a = ProfileValue::Int(5);
let b = map([("key", ProfileValue::Bool(true))]);
a.merge(b.clone());
assert_eq!(a, ProfileValue::Int(5));
}
#[test]
fn merge_map_with_null_value() {
let mut a = map([
("theme", ProfileValue::String("light".into())),
]);
let b = map([
("theme", ProfileValue::Null),
]);
a.merge(b);
assert_eq!(
a,
map([("theme", ProfileValue::String("light".into()))])
);
}
#[test]
fn merge_list_concats() {
let mut a = ProfileValue::List(vec![ProfileValue::Int(1)]);
let b = ProfileValue::List(vec![ProfileValue::Int(2), ProfileValue::Int(3)]);
a.merge(b.clone());
assert_eq!(a, ProfileValue::List(vec![ProfileValue::Int(1), ProfileValue::Int(2), ProfileValue::Int(3)]));
}
#[test]
fn merge_null_into_null_remains_null() {
let mut a = ProfileValue::Null;
a.merge(ProfileValue::Null);
assert_eq!(a, ProfileValue::Null);
}
}
}