1use matter_codec::{Tag, Value};
6use matter_interaction::AttributePath;
7
8pub(crate) const ACCESS_CONTROL_CLUSTER: u32 = 0x001F;
10pub(crate) const ATTR_ACL: u32 = 0x0000;
12
13const TAG_PRIVILEGE: u8 = 1;
15const TAG_AUTH_MODE: u8 = 2;
16const TAG_SUBJECTS: u8 = 3;
17const TAG_TARGETS: u8 = 4;
18const TAG_FABRIC_INDEX: u8 = 254;
19
20const TAG_TARGET_CLUSTER: u8 = 0;
22const TAG_TARGET_ENDPOINT: u8 = 1;
23const TAG_TARGET_DEVICE_TYPE: u8 = 2;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum AclPrivilege {
32 View,
34 ProxyView,
36 Operate,
38 Manage,
40 Administer,
42 Unknown(u8),
44}
45
46impl AclPrivilege {
47 #[allow(clippy::cast_possible_truncation)]
48 fn to_raw(self) -> u8 {
51 match self {
52 Self::View => 1,
53 Self::ProxyView => 2,
54 Self::Operate => 3,
55 Self::Manage => 4,
56 Self::Administer => 5,
57 Self::Unknown(v) => v,
58 }
59 }
60
61 fn from_raw(v: u8) -> Self {
62 match v {
63 1 => Self::View,
64 2 => Self::ProxyView,
65 3 => Self::Operate,
66 4 => Self::Manage,
67 5 => Self::Administer,
68 o => Self::Unknown(o),
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum AclAuthMode {
79 Pase,
81 Case,
83 Group,
91 Unknown(u8),
93}
94
95impl AclAuthMode {
96 #[allow(clippy::cast_possible_truncation)]
97 fn to_raw(self) -> u8 {
100 match self {
101 Self::Pase => 1,
102 Self::Case => 2,
103 Self::Group => 3,
104 Self::Unknown(v) => v,
105 }
106 }
107
108 fn from_raw(v: u8) -> Self {
109 match v {
110 1 => Self::Pase,
111 2 => Self::Case,
112 3 => Self::Group,
113 o => Self::Unknown(o),
114 }
115 }
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Default)]
126#[non_exhaustive]
127pub struct AclTarget {
128 pub cluster: Option<u32>,
130 pub endpoint: Option<u16>,
132 pub device_type: Option<u32>,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq)]
141#[non_exhaustive]
142pub struct AclEntry {
143 pub privilege: AclPrivilege,
145 pub auth_mode: AclAuthMode,
147 pub subjects: Option<Vec<u64>>,
150 pub targets: Option<Vec<AclTarget>>,
152 pub fabric_index: Option<u8>,
155}
156
157impl AclTarget {
158 #[must_use]
164 pub fn new(cluster: Option<u32>, endpoint: Option<u16>, device_type: Option<u32>) -> Self {
165 Self {
166 cluster,
167 endpoint,
168 device_type,
169 }
170 }
171}
172
173impl AclEntry {
174 #[must_use]
182 pub fn new(
183 privilege: AclPrivilege,
184 auth_mode: AclAuthMode,
185 subjects: Option<Vec<u64>>,
186 targets: Option<Vec<AclTarget>>,
187 ) -> Self {
188 Self {
189 privilege,
190 auth_mode,
191 subjects,
192 targets,
193 fabric_index: None,
194 }
195 }
196}
197
198fn struct_members(v: &Value) -> Option<&[(Tag, Value)]> {
201 match v {
202 Value::Structure(m) | Value::List(m) => Some(m),
203 _ => None,
204 }
205}
206
207fn ctx(members: &[(Tag, Value)], tag: u8) -> Option<&Value> {
208 members
209 .iter()
210 .find(|(t, _)| *t == Tag::Context(tag))
211 .map(|(_, v)| v)
212}
213
214fn opt_u64_list(v: Option<&Vec<u64>>) -> Value {
215 match v {
216 None => Value::Null,
217 Some(xs) => Value::Array(xs.iter().map(|x| Value::Uint(*x)).collect()),
218 }
219}
220
221fn target_value(t: &AclTarget) -> Value {
222 Value::Structure(vec![
223 (
224 Tag::Context(TAG_TARGET_CLUSTER),
225 t.cluster.map_or(Value::Null, |c| Value::Uint(u64::from(c))),
226 ),
227 (
228 Tag::Context(TAG_TARGET_ENDPOINT),
229 t.endpoint
230 .map_or(Value::Null, |e| Value::Uint(u64::from(e))),
231 ),
232 (
233 Tag::Context(TAG_TARGET_DEVICE_TYPE),
234 t.device_type
235 .map_or(Value::Null, |d| Value::Uint(u64::from(d))),
236 ),
237 ])
238}
239
240pub(crate) fn acl_entry_value(e: &AclEntry) -> Value {
247 let mut m = vec![
248 (
249 Tag::Context(TAG_PRIVILEGE),
250 Value::Uint(u64::from(e.privilege.to_raw())),
251 ),
252 (
253 Tag::Context(TAG_AUTH_MODE),
254 Value::Uint(u64::from(e.auth_mode.to_raw())),
255 ),
256 (
257 Tag::Context(TAG_SUBJECTS),
258 opt_u64_list(e.subjects.as_ref()),
259 ),
260 (
261 Tag::Context(TAG_TARGETS),
262 match &e.targets {
263 None => Value::Null,
264 Some(ts) => Value::Array(ts.iter().map(target_value).collect()),
265 },
266 ),
267 ];
268 if let Some(fi) = e.fabric_index {
269 m.push((Tag::Context(TAG_FABRIC_INDEX), Value::Uint(u64::from(fi))));
270 }
271 Value::Structure(m)
272}
273
274fn parse_target(v: &Value) -> Option<AclTarget> {
277 let m = struct_members(v)?;
278 #[allow(clippy::cast_possible_truncation)]
279 Some(AclTarget {
283 cluster: match ctx(m, TAG_TARGET_CLUSTER) {
284 Some(Value::Uint(u)) => Some(*u as u32),
285 _ => None,
286 },
287 endpoint: match ctx(m, TAG_TARGET_ENDPOINT) {
288 Some(Value::Uint(u)) => Some(*u as u16),
289 _ => None,
290 },
291 device_type: match ctx(m, TAG_TARGET_DEVICE_TYPE) {
292 Some(Value::Uint(u)) => Some(*u as u32),
293 _ => None,
294 },
295 })
296}
297
298fn parse_entry(v: &Value) -> Option<AclEntry> {
299 let m = struct_members(v)?;
300 #[allow(clippy::cast_possible_truncation)]
301 Some(AclEntry {
305 privilege: AclPrivilege::from_raw(match ctx(m, TAG_PRIVILEGE)? {
306 Value::Uint(u) => *u as u8,
307 _ => return None,
308 }),
309 auth_mode: AclAuthMode::from_raw(match ctx(m, TAG_AUTH_MODE)? {
310 Value::Uint(u) => *u as u8,
311 _ => return None,
312 }),
313 subjects: match ctx(m, TAG_SUBJECTS) {
314 Some(Value::Array(a)) => Some(
315 a.iter()
316 .filter_map(|x| {
317 if let Value::Uint(u) = x {
318 Some(*u)
319 } else {
320 None
321 }
322 })
323 .collect(),
324 ),
325 _ => None,
326 },
327 targets: match ctx(m, TAG_TARGETS) {
328 Some(Value::Array(a)) => Some(a.iter().filter_map(parse_target).collect()),
329 _ => None,
330 },
331 fabric_index: match ctx(m, TAG_FABRIC_INDEX) {
332 Some(Value::Uint(u)) => Some(*u as u8),
333 _ => None,
334 },
335 })
336}
337
338pub(crate) fn parse_acl(reports: &[(AttributePath, Value)]) -> Vec<AclEntry> {
346 for (path, value) in reports {
347 if path.cluster == ACCESS_CONTROL_CLUSTER && path.attribute == ATTR_ACL {
348 if let Value::Array(items) = value {
349 return items.iter().filter_map(parse_entry).collect();
350 }
351 }
352 }
353 Vec::new()
354}
355
356pub(crate) fn acl_retains_admin(entries: &[AclEntry], our_node_id: u64) -> bool {
368 entries.iter().any(|e| {
369 e.privilege == AclPrivilege::Administer
370 && e.auth_mode == AclAuthMode::Case
371 && match &e.subjects {
372 None => true,
373 Some(s) => s.contains(&our_node_id),
374 }
375 })
376}
377
378#[cfg(test)]
381#[allow(clippy::unwrap_used)] mod tests {
383 use super::*;
384 use matter_codec::{TlvReader, TlvWriter};
385
386 fn admin(node: u64) -> AclEntry {
387 AclEntry {
388 privilege: AclPrivilege::Administer,
389 auth_mode: AclAuthMode::Case,
390 subjects: Some(vec![node]),
391 targets: None,
392 fabric_index: None,
393 }
394 }
395
396 #[test]
397 fn entry_value_uses_spec_tags() {
398 let v = acl_entry_value(&admin(0x1234));
399 let Value::Structure(m) = v else {
400 panic!("expected Structure")
401 };
402 assert_eq!(m[0], (Tag::Context(1), Value::Uint(5)));
404 assert_eq!(m[1], (Tag::Context(2), Value::Uint(2)));
406 assert_eq!(
408 m[2],
409 (Tag::Context(3), Value::Array(vec![Value::Uint(0x1234)]))
410 );
411 assert_eq!(m[3], (Tag::Context(4), Value::Null));
413 assert!(m.iter().all(|(t, _)| *t != Tag::Context(254)));
415 }
416
417 #[test]
418 fn lockout_guard_truth_table() {
419 assert!(acl_retains_admin(&[admin(7)], 7));
421
422 let wild = AclEntry {
424 subjects: None,
425 ..admin(0)
426 };
427 assert!(acl_retains_admin(&[wild], 7));
428
429 assert!(!acl_retains_admin(&[admin(9)], 7));
431
432 assert!(!acl_retains_admin(&[], 7));
434
435 let op = AclEntry {
437 privilege: AclPrivilege::Operate,
438 ..admin(7)
439 };
440 assert!(!acl_retains_admin(&[op], 7));
441
442 let pase = AclEntry {
444 auth_mode: AclAuthMode::Pase,
445 ..admin(7)
446 };
447 assert!(!acl_retains_admin(&[pase], 7));
448 }
449
450 #[test]
451 fn parse_acl_roundtrips_through_codec() {
452 let entries = [
454 admin(7),
455 AclEntry {
456 privilege: AclPrivilege::Operate,
457 auth_mode: AclAuthMode::Case,
458 subjects: Some(vec![1, 2]),
459 targets: Some(vec![AclTarget {
460 cluster: Some(6),
461 endpoint: Some(1),
462 device_type: None,
463 }]),
464 fabric_index: Some(1),
465 },
466 ];
467
468 let arr = Value::Array(entries.iter().map(acl_entry_value).collect());
469
470 let mut buf = Vec::new();
471 TlvWriter::new(&mut buf)
472 .write_value(Tag::Anonymous, &arr)
473 .unwrap();
474
475 let (_, decoded) = TlvReader::new(&buf).read_value().unwrap();
477
478 let path = AttributePath {
479 endpoint: 0,
480 cluster: ACCESS_CONTROL_CLUSTER,
481 attribute: ATTR_ACL,
482 };
483 let parsed = parse_acl(&[(path, decoded)]);
484
485 assert_eq!(parsed.len(), 2);
486 assert_eq!(parsed[0].privilege, AclPrivilege::Administer);
487 assert_eq!(parsed[0].auth_mode, AclAuthMode::Case);
488 assert_eq!(parsed[0].subjects, Some(vec![7]));
489 assert_eq!(parsed[0].targets, None);
490
491 assert_eq!(parsed[1].privilege, AclPrivilege::Operate);
492 let targets = parsed[1].targets.as_ref().unwrap();
493 assert_eq!(targets.len(), 1);
494 assert_eq!(targets[0].cluster, Some(6));
495 assert_eq!(targets[0].endpoint, Some(1));
496 assert_eq!(targets[0].device_type, None);
497 assert_eq!(parsed[1].fabric_index, Some(1));
498 }
499
500 #[test]
501 fn constructors_build_writable_entries() {
502 let t = AclTarget::new(Some(6), Some(1), None);
503 assert_eq!(t.cluster, Some(6));
504 let e = AclEntry::new(
505 AclPrivilege::Administer,
506 AclAuthMode::Case,
507 Some(vec![7]),
508 Some(vec![t]),
509 );
510 assert_eq!(e.privilege, AclPrivilege::Administer);
511 assert_eq!(e.subjects, Some(vec![7]));
512 assert_eq!(e.fabric_index, None);
514 assert!(matches!(acl_entry_value(&e), Value::Structure(_)));
516 }
517}