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,
85 Unknown(u8),
87}
88
89impl AclAuthMode {
90 #[allow(clippy::cast_possible_truncation)]
91 fn to_raw(self) -> u8 {
94 match self {
95 Self::Pase => 1,
96 Self::Case => 2,
97 Self::Group => 3,
98 Self::Unknown(v) => v,
99 }
100 }
101
102 fn from_raw(v: u8) -> Self {
103 match v {
104 1 => Self::Pase,
105 2 => Self::Case,
106 3 => Self::Group,
107 o => Self::Unknown(o),
108 }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Default)]
120#[non_exhaustive]
121pub struct AclTarget {
122 pub cluster: Option<u32>,
124 pub endpoint: Option<u16>,
126 pub device_type: Option<u32>,
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
135#[non_exhaustive]
136pub struct AclEntry {
137 pub privilege: AclPrivilege,
139 pub auth_mode: AclAuthMode,
141 pub subjects: Option<Vec<u64>>,
144 pub targets: Option<Vec<AclTarget>>,
146 pub fabric_index: Option<u8>,
149}
150
151impl AclTarget {
152 #[must_use]
158 pub fn new(cluster: Option<u32>, endpoint: Option<u16>, device_type: Option<u32>) -> Self {
159 Self {
160 cluster,
161 endpoint,
162 device_type,
163 }
164 }
165}
166
167impl AclEntry {
168 #[must_use]
176 pub fn new(
177 privilege: AclPrivilege,
178 auth_mode: AclAuthMode,
179 subjects: Option<Vec<u64>>,
180 targets: Option<Vec<AclTarget>>,
181 ) -> Self {
182 Self {
183 privilege,
184 auth_mode,
185 subjects,
186 targets,
187 fabric_index: None,
188 }
189 }
190}
191
192fn struct_members(v: &Value) -> Option<&[(Tag, Value)]> {
195 match v {
196 Value::Structure(m) | Value::List(m) => Some(m),
197 _ => None,
198 }
199}
200
201fn ctx(members: &[(Tag, Value)], tag: u8) -> Option<&Value> {
202 members
203 .iter()
204 .find(|(t, _)| *t == Tag::Context(tag))
205 .map(|(_, v)| v)
206}
207
208fn opt_u64_list(v: Option<&Vec<u64>>) -> Value {
209 match v {
210 None => Value::Null,
211 Some(xs) => Value::Array(xs.iter().map(|x| Value::Uint(*x)).collect()),
212 }
213}
214
215fn target_value(t: &AclTarget) -> Value {
216 Value::Structure(vec![
217 (
218 Tag::Context(TAG_TARGET_CLUSTER),
219 t.cluster.map_or(Value::Null, |c| Value::Uint(u64::from(c))),
220 ),
221 (
222 Tag::Context(TAG_TARGET_ENDPOINT),
223 t.endpoint
224 .map_or(Value::Null, |e| Value::Uint(u64::from(e))),
225 ),
226 (
227 Tag::Context(TAG_TARGET_DEVICE_TYPE),
228 t.device_type
229 .map_or(Value::Null, |d| Value::Uint(u64::from(d))),
230 ),
231 ])
232}
233
234pub(crate) fn acl_entry_value(e: &AclEntry) -> Value {
241 let mut m = vec![
242 (
243 Tag::Context(TAG_PRIVILEGE),
244 Value::Uint(u64::from(e.privilege.to_raw())),
245 ),
246 (
247 Tag::Context(TAG_AUTH_MODE),
248 Value::Uint(u64::from(e.auth_mode.to_raw())),
249 ),
250 (
251 Tag::Context(TAG_SUBJECTS),
252 opt_u64_list(e.subjects.as_ref()),
253 ),
254 (
255 Tag::Context(TAG_TARGETS),
256 match &e.targets {
257 None => Value::Null,
258 Some(ts) => Value::Array(ts.iter().map(target_value).collect()),
259 },
260 ),
261 ];
262 if let Some(fi) = e.fabric_index {
263 m.push((Tag::Context(TAG_FABRIC_INDEX), Value::Uint(u64::from(fi))));
264 }
265 Value::Structure(m)
266}
267
268fn parse_target(v: &Value) -> Option<AclTarget> {
271 let m = struct_members(v)?;
272 #[allow(clippy::cast_possible_truncation)]
273 Some(AclTarget {
277 cluster: match ctx(m, TAG_TARGET_CLUSTER) {
278 Some(Value::Uint(u)) => Some(*u as u32),
279 _ => None,
280 },
281 endpoint: match ctx(m, TAG_TARGET_ENDPOINT) {
282 Some(Value::Uint(u)) => Some(*u as u16),
283 _ => None,
284 },
285 device_type: match ctx(m, TAG_TARGET_DEVICE_TYPE) {
286 Some(Value::Uint(u)) => Some(*u as u32),
287 _ => None,
288 },
289 })
290}
291
292fn parse_entry(v: &Value) -> Option<AclEntry> {
293 let m = struct_members(v)?;
294 #[allow(clippy::cast_possible_truncation)]
295 Some(AclEntry {
299 privilege: AclPrivilege::from_raw(match ctx(m, TAG_PRIVILEGE)? {
300 Value::Uint(u) => *u as u8,
301 _ => return None,
302 }),
303 auth_mode: AclAuthMode::from_raw(match ctx(m, TAG_AUTH_MODE)? {
304 Value::Uint(u) => *u as u8,
305 _ => return None,
306 }),
307 subjects: match ctx(m, TAG_SUBJECTS) {
308 Some(Value::Array(a)) => Some(
309 a.iter()
310 .filter_map(|x| {
311 if let Value::Uint(u) = x {
312 Some(*u)
313 } else {
314 None
315 }
316 })
317 .collect(),
318 ),
319 _ => None,
320 },
321 targets: match ctx(m, TAG_TARGETS) {
322 Some(Value::Array(a)) => Some(a.iter().filter_map(parse_target).collect()),
323 _ => None,
324 },
325 fabric_index: match ctx(m, TAG_FABRIC_INDEX) {
326 Some(Value::Uint(u)) => Some(*u as u8),
327 _ => None,
328 },
329 })
330}
331
332pub(crate) fn parse_acl(reports: &[(AttributePath, Value)]) -> Vec<AclEntry> {
340 for (path, value) in reports {
341 if path.cluster == ACCESS_CONTROL_CLUSTER && path.attribute == ATTR_ACL {
342 if let Value::Array(items) = value {
343 return items.iter().filter_map(parse_entry).collect();
344 }
345 }
346 }
347 Vec::new()
348}
349
350pub(crate) fn acl_retains_admin(entries: &[AclEntry], our_node_id: u64) -> bool {
362 entries.iter().any(|e| {
363 e.privilege == AclPrivilege::Administer
364 && e.auth_mode == AclAuthMode::Case
365 && match &e.subjects {
366 None => true,
367 Some(s) => s.contains(&our_node_id),
368 }
369 })
370}
371
372#[cfg(test)]
375#[allow(clippy::unwrap_used)] mod tests {
377 use super::*;
378 use matter_codec::{TlvReader, TlvWriter};
379
380 fn admin(node: u64) -> AclEntry {
381 AclEntry {
382 privilege: AclPrivilege::Administer,
383 auth_mode: AclAuthMode::Case,
384 subjects: Some(vec![node]),
385 targets: None,
386 fabric_index: None,
387 }
388 }
389
390 #[test]
391 fn entry_value_uses_spec_tags() {
392 let v = acl_entry_value(&admin(0x1234));
393 let Value::Structure(m) = v else {
394 panic!("expected Structure")
395 };
396 assert_eq!(m[0], (Tag::Context(1), Value::Uint(5)));
398 assert_eq!(m[1], (Tag::Context(2), Value::Uint(2)));
400 assert_eq!(
402 m[2],
403 (Tag::Context(3), Value::Array(vec![Value::Uint(0x1234)]))
404 );
405 assert_eq!(m[3], (Tag::Context(4), Value::Null));
407 assert!(m.iter().all(|(t, _)| *t != Tag::Context(254)));
409 }
410
411 #[test]
412 fn lockout_guard_truth_table() {
413 assert!(acl_retains_admin(&[admin(7)], 7));
415
416 let wild = AclEntry {
418 subjects: None,
419 ..admin(0)
420 };
421 assert!(acl_retains_admin(&[wild], 7));
422
423 assert!(!acl_retains_admin(&[admin(9)], 7));
425
426 assert!(!acl_retains_admin(&[], 7));
428
429 let op = AclEntry {
431 privilege: AclPrivilege::Operate,
432 ..admin(7)
433 };
434 assert!(!acl_retains_admin(&[op], 7));
435
436 let pase = AclEntry {
438 auth_mode: AclAuthMode::Pase,
439 ..admin(7)
440 };
441 assert!(!acl_retains_admin(&[pase], 7));
442 }
443
444 #[test]
445 fn parse_acl_roundtrips_through_codec() {
446 let entries = [
448 admin(7),
449 AclEntry {
450 privilege: AclPrivilege::Operate,
451 auth_mode: AclAuthMode::Case,
452 subjects: Some(vec![1, 2]),
453 targets: Some(vec![AclTarget {
454 cluster: Some(6),
455 endpoint: Some(1),
456 device_type: None,
457 }]),
458 fabric_index: Some(1),
459 },
460 ];
461
462 let arr = Value::Array(entries.iter().map(acl_entry_value).collect());
463
464 let mut buf = Vec::new();
465 TlvWriter::new(&mut buf)
466 .write_value(Tag::Anonymous, &arr)
467 .unwrap();
468
469 let (_, decoded) = TlvReader::new(&buf).read_value().unwrap();
471
472 let path = AttributePath {
473 endpoint: 0,
474 cluster: ACCESS_CONTROL_CLUSTER,
475 attribute: ATTR_ACL,
476 };
477 let parsed = parse_acl(&[(path, decoded)]);
478
479 assert_eq!(parsed.len(), 2);
480 assert_eq!(parsed[0].privilege, AclPrivilege::Administer);
481 assert_eq!(parsed[0].auth_mode, AclAuthMode::Case);
482 assert_eq!(parsed[0].subjects, Some(vec![7]));
483 assert_eq!(parsed[0].targets, None);
484
485 assert_eq!(parsed[1].privilege, AclPrivilege::Operate);
486 let targets = parsed[1].targets.as_ref().unwrap();
487 assert_eq!(targets.len(), 1);
488 assert_eq!(targets[0].cluster, Some(6));
489 assert_eq!(targets[0].endpoint, Some(1));
490 assert_eq!(targets[0].device_type, None);
491 assert_eq!(parsed[1].fabric_index, Some(1));
492 }
493
494 #[test]
495 fn constructors_build_writable_entries() {
496 let t = AclTarget::new(Some(6), Some(1), None);
497 assert_eq!(t.cluster, Some(6));
498 let e = AclEntry::new(
499 AclPrivilege::Administer,
500 AclAuthMode::Case,
501 Some(vec![7]),
502 Some(vec![t]),
503 );
504 assert_eq!(e.privilege, AclPrivilege::Administer);
505 assert_eq!(e.subjects, Some(vec![7]));
506 assert_eq!(e.fabric_index, None);
508 assert!(matches!(acl_entry_value(&e), Value::Structure(_)));
510 }
511}