use matter_codec::{Tag, Value};
use matter_interaction::AttributePath;
pub(crate) const ACCESS_CONTROL_CLUSTER: u32 = 0x001F;
pub(crate) const ATTR_ACL: u32 = 0x0000;
const TAG_PRIVILEGE: u8 = 1;
const TAG_AUTH_MODE: u8 = 2;
const TAG_SUBJECTS: u8 = 3;
const TAG_TARGETS: u8 = 4;
const TAG_FABRIC_INDEX: u8 = 254;
const TAG_TARGET_CLUSTER: u8 = 0;
const TAG_TARGET_ENDPOINT: u8 = 1;
const TAG_TARGET_DEVICE_TYPE: u8 = 2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AclPrivilege {
View,
ProxyView,
Operate,
Manage,
Administer,
Unknown(u8),
}
impl AclPrivilege {
#[allow(clippy::cast_possible_truncation)]
fn to_raw(self) -> u8 {
match self {
Self::View => 1,
Self::ProxyView => 2,
Self::Operate => 3,
Self::Manage => 4,
Self::Administer => 5,
Self::Unknown(v) => v,
}
}
fn from_raw(v: u8) -> Self {
match v {
1 => Self::View,
2 => Self::ProxyView,
3 => Self::Operate,
4 => Self::Manage,
5 => Self::Administer,
o => Self::Unknown(o),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AclAuthMode {
Pase,
Case,
Group,
Unknown(u8),
}
impl AclAuthMode {
#[allow(clippy::cast_possible_truncation)]
fn to_raw(self) -> u8 {
match self {
Self::Pase => 1,
Self::Case => 2,
Self::Group => 3,
Self::Unknown(v) => v,
}
}
fn from_raw(v: u8) -> Self {
match v {
1 => Self::Pase,
2 => Self::Case,
3 => Self::Group,
o => Self::Unknown(o),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct AclTarget {
pub cluster: Option<u32>,
pub endpoint: Option<u16>,
pub device_type: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AclEntry {
pub privilege: AclPrivilege,
pub auth_mode: AclAuthMode,
pub subjects: Option<Vec<u64>>,
pub targets: Option<Vec<AclTarget>>,
pub fabric_index: Option<u8>,
}
impl AclTarget {
#[must_use]
pub fn new(cluster: Option<u32>, endpoint: Option<u16>, device_type: Option<u32>) -> Self {
Self {
cluster,
endpoint,
device_type,
}
}
}
impl AclEntry {
#[must_use]
pub fn new(
privilege: AclPrivilege,
auth_mode: AclAuthMode,
subjects: Option<Vec<u64>>,
targets: Option<Vec<AclTarget>>,
) -> Self {
Self {
privilege,
auth_mode,
subjects,
targets,
fabric_index: None,
}
}
}
fn struct_members(v: &Value) -> Option<&[(Tag, Value)]> {
match v {
Value::Structure(m) | Value::List(m) => Some(m),
_ => None,
}
}
fn ctx(members: &[(Tag, Value)], tag: u8) -> Option<&Value> {
members
.iter()
.find(|(t, _)| *t == Tag::Context(tag))
.map(|(_, v)| v)
}
fn opt_u64_list(v: Option<&Vec<u64>>) -> Value {
match v {
None => Value::Null,
Some(xs) => Value::Array(xs.iter().map(|x| Value::Uint(*x)).collect()),
}
}
fn target_value(t: &AclTarget) -> Value {
Value::Structure(vec![
(
Tag::Context(TAG_TARGET_CLUSTER),
t.cluster.map_or(Value::Null, |c| Value::Uint(u64::from(c))),
),
(
Tag::Context(TAG_TARGET_ENDPOINT),
t.endpoint
.map_or(Value::Null, |e| Value::Uint(u64::from(e))),
),
(
Tag::Context(TAG_TARGET_DEVICE_TYPE),
t.device_type
.map_or(Value::Null, |d| Value::Uint(u64::from(d))),
),
])
}
pub(crate) fn acl_entry_value(e: &AclEntry) -> Value {
let mut m = vec![
(
Tag::Context(TAG_PRIVILEGE),
Value::Uint(u64::from(e.privilege.to_raw())),
),
(
Tag::Context(TAG_AUTH_MODE),
Value::Uint(u64::from(e.auth_mode.to_raw())),
),
(
Tag::Context(TAG_SUBJECTS),
opt_u64_list(e.subjects.as_ref()),
),
(
Tag::Context(TAG_TARGETS),
match &e.targets {
None => Value::Null,
Some(ts) => Value::Array(ts.iter().map(target_value).collect()),
},
),
];
if let Some(fi) = e.fabric_index {
m.push((Tag::Context(TAG_FABRIC_INDEX), Value::Uint(u64::from(fi))));
}
Value::Structure(m)
}
fn parse_target(v: &Value) -> Option<AclTarget> {
let m = struct_members(v)?;
#[allow(clippy::cast_possible_truncation)]
Some(AclTarget {
cluster: match ctx(m, TAG_TARGET_CLUSTER) {
Some(Value::Uint(u)) => Some(*u as u32),
_ => None,
},
endpoint: match ctx(m, TAG_TARGET_ENDPOINT) {
Some(Value::Uint(u)) => Some(*u as u16),
_ => None,
},
device_type: match ctx(m, TAG_TARGET_DEVICE_TYPE) {
Some(Value::Uint(u)) => Some(*u as u32),
_ => None,
},
})
}
fn parse_entry(v: &Value) -> Option<AclEntry> {
let m = struct_members(v)?;
#[allow(clippy::cast_possible_truncation)]
Some(AclEntry {
privilege: AclPrivilege::from_raw(match ctx(m, TAG_PRIVILEGE)? {
Value::Uint(u) => *u as u8,
_ => return None,
}),
auth_mode: AclAuthMode::from_raw(match ctx(m, TAG_AUTH_MODE)? {
Value::Uint(u) => *u as u8,
_ => return None,
}),
subjects: match ctx(m, TAG_SUBJECTS) {
Some(Value::Array(a)) => Some(
a.iter()
.filter_map(|x| {
if let Value::Uint(u) = x {
Some(*u)
} else {
None
}
})
.collect(),
),
_ => None,
},
targets: match ctx(m, TAG_TARGETS) {
Some(Value::Array(a)) => Some(a.iter().filter_map(parse_target).collect()),
_ => None,
},
fabric_index: match ctx(m, TAG_FABRIC_INDEX) {
Some(Value::Uint(u)) => Some(*u as u8),
_ => None,
},
})
}
pub(crate) fn parse_acl(reports: &[(AttributePath, Value)]) -> Vec<AclEntry> {
for (path, value) in reports {
if path.cluster == ACCESS_CONTROL_CLUSTER && path.attribute == ATTR_ACL {
if let Value::Array(items) = value {
return items.iter().filter_map(parse_entry).collect();
}
}
}
Vec::new()
}
pub(crate) fn acl_retains_admin(entries: &[AclEntry], our_node_id: u64) -> bool {
entries.iter().any(|e| {
e.privilege == AclPrivilege::Administer
&& e.auth_mode == AclAuthMode::Case
&& match &e.subjects {
None => true,
Some(s) => s.contains(&our_node_id),
}
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use matter_codec::{TlvReader, TlvWriter};
fn admin(node: u64) -> AclEntry {
AclEntry {
privilege: AclPrivilege::Administer,
auth_mode: AclAuthMode::Case,
subjects: Some(vec![node]),
targets: None,
fabric_index: None,
}
}
#[test]
fn entry_value_uses_spec_tags() {
let v = acl_entry_value(&admin(0x1234));
let Value::Structure(m) = v else {
panic!("expected Structure")
};
assert_eq!(m[0], (Tag::Context(1), Value::Uint(5)));
assert_eq!(m[1], (Tag::Context(2), Value::Uint(2)));
assert_eq!(
m[2],
(Tag::Context(3), Value::Array(vec![Value::Uint(0x1234)]))
);
assert_eq!(m[3], (Tag::Context(4), Value::Null));
assert!(m.iter().all(|(t, _)| *t != Tag::Context(254)));
}
#[test]
fn lockout_guard_truth_table() {
assert!(acl_retains_admin(&[admin(7)], 7));
let wild = AclEntry {
subjects: None,
..admin(0)
};
assert!(acl_retains_admin(&[wild], 7));
assert!(!acl_retains_admin(&[admin(9)], 7));
assert!(!acl_retains_admin(&[], 7));
let op = AclEntry {
privilege: AclPrivilege::Operate,
..admin(7)
};
assert!(!acl_retains_admin(&[op], 7));
let pase = AclEntry {
auth_mode: AclAuthMode::Pase,
..admin(7)
};
assert!(!acl_retains_admin(&[pase], 7));
}
#[test]
fn parse_acl_roundtrips_through_codec() {
let entries = [
admin(7),
AclEntry {
privilege: AclPrivilege::Operate,
auth_mode: AclAuthMode::Case,
subjects: Some(vec![1, 2]),
targets: Some(vec![AclTarget {
cluster: Some(6),
endpoint: Some(1),
device_type: None,
}]),
fabric_index: Some(1),
},
];
let arr = Value::Array(entries.iter().map(acl_entry_value).collect());
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.write_value(Tag::Anonymous, &arr)
.unwrap();
let (_, decoded) = TlvReader::new(&buf).read_value().unwrap();
let path = AttributePath {
endpoint: 0,
cluster: ACCESS_CONTROL_CLUSTER,
attribute: ATTR_ACL,
};
let parsed = parse_acl(&[(path, decoded)]);
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].privilege, AclPrivilege::Administer);
assert_eq!(parsed[0].auth_mode, AclAuthMode::Case);
assert_eq!(parsed[0].subjects, Some(vec![7]));
assert_eq!(parsed[0].targets, None);
assert_eq!(parsed[1].privilege, AclPrivilege::Operate);
let targets = parsed[1].targets.as_ref().unwrap();
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].cluster, Some(6));
assert_eq!(targets[0].endpoint, Some(1));
assert_eq!(targets[0].device_type, None);
assert_eq!(parsed[1].fabric_index, Some(1));
}
#[test]
fn constructors_build_writable_entries() {
let t = AclTarget::new(Some(6), Some(1), None);
assert_eq!(t.cluster, Some(6));
let e = AclEntry::new(
AclPrivilege::Administer,
AclAuthMode::Case,
Some(vec![7]),
Some(vec![t]),
);
assert_eq!(e.privilege, AclPrivilege::Administer);
assert_eq!(e.subjects, Some(vec![7]));
assert_eq!(e.fabric_index, None);
assert!(matches!(acl_entry_value(&e), Value::Structure(_)));
}
}