Skip to main content

rs_matter/dm/clusters/
acl.rs

1/*
2 *
3 *    Copyright (c) 2025-2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! This module contains the implementation of the Access Control cluster and its handler.
19
20use core::num::NonZeroU8;
21
22use crate::acl::{self, AclEntry, AuthMode, MAX_ACL_ENTRIES_PER_FABRIC};
23use crate::dm::{
24    ArrayAttributeRead, ArrayAttributeWrite, AttrDetails, Cluster, Dataver, InvokeContext,
25    ReadContext, WriteContext,
26};
27use crate::error::{Error, ErrorCode};
28use crate::fabric::{Fabric, FabricPersist, Fabrics};
29use crate::tlv::{Nullable, TLVArray, TLVBuilderParent};
30use crate::utils::init::stack_try_pin_init;
31use crate::with;
32
33pub use crate::dm::clusters::decl::access_control::*;
34
35/// The system implementation of a handler for the Access Control Matter cluster.
36#[derive(Debug, Clone)]
37#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38pub struct AclHandler {
39    dataver: Dataver,
40}
41
42impl AclHandler {
43    /// Create a new instance of `AclHandler` with the given `dataver`
44    pub const fn new(dataver: Dataver) -> Self {
45        Self { dataver }
46    }
47
48    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
49    pub const fn adapt(self) -> HandlerAdaptor<Self> {
50        HandlerAdaptor(self)
51    }
52
53    /// For unit-testing
54    /// Read the ACL entries from the fabrics and write them into the builder
55    fn acl<P: TLVBuilderParent>(
56        &self,
57        fabrics: &Fabrics,
58        attr: &AttrDetails,
59        builder: ArrayAttributeRead<
60            AccessControlEntryStructArrayBuilder<P>,
61            AccessControlEntryStructBuilder<P>,
62        >,
63    ) -> Result<P, Error> {
64        let mut acls = fabrics
65            .iter()
66            .filter(|fabric| !attr.fab_filter || fabric.fab_idx().get() == attr.fab_idx)
67            .flat_map(|fabric| fabric.acl_iter().map(|entry| (fabric.fab_idx(), entry)));
68
69        match builder {
70            ArrayAttributeRead::ReadAll(mut builder) => {
71                for (fab_idx, entry) in acls {
72                    builder =
73                        entry.read_into(attr.fab_idx, Some(fab_idx.get()), builder.push()?)?;
74                }
75
76                builder.end()
77            }
78            ArrayAttributeRead::ReadOne(index, builder) => {
79                let Some((fab_idx, entry)) = acls.nth(index as usize) else {
80                    return Err(ErrorCode::ConstraintError.into());
81                };
82
83                entry.read_into(attr.fab_idx, Some(fab_idx.get()), builder)
84            }
85            ArrayAttributeRead::ReadNone(builder) => builder.end(),
86        }
87    }
88
89    /// For unit-testing
90    /// Set the ACL entries in the fabrics
91    fn set_acl(
92        &self,
93        fabric: &mut Fabric,
94        value: ArrayAttributeWrite<
95            TLVArray<'_, AccessControlEntryStruct<'_>>,
96            AccessControlEntryStruct<'_>,
97        >,
98    ) -> Result<(), Error> {
99        match value {
100            ArrayAttributeWrite::Replace(list) => {
101                // Check the well-formedness of the list first & init to check validity
102                let mut count: usize = 0;
103                for entry in &list {
104                    count += 1;
105                    if count > MAX_ACL_ENTRIES_PER_FABRIC {
106                        return Err(ErrorCode::ResourceExhausted)?;
107                    }
108                    let entry = entry?;
109                    // Init a dummy to propagate failures for bad inputs
110                    stack_try_pin_init!(let _processed =? AclEntry::init_with(fabric.fab_idx(), &entry));
111                }
112
113                // Now add everything once we know all are valid
114                fabric.acl_remove_all();
115                for entry in &list {
116                    // unwrap! calls below can't fail because we already checked that the entry is well-formed
117                    // and the length of the list is within the limit
118                    let entry = unwrap!(entry);
119                    unwrap!(fabric.acl_add_init(AclEntry::init_with(fabric.fab_idx(), &entry)));
120                }
121            }
122            ArrayAttributeWrite::Add(entry) => {
123                fabric.acl_add_init(AclEntry::init_with(fabric.fab_idx(), &entry))?;
124            }
125            ArrayAttributeWrite::Update(index, entry) => {
126                fabric
127                    .acl_update_init(index as _, AclEntry::init_with(fabric.fab_idx(), &entry))?;
128            }
129            ArrayAttributeWrite::Remove(index) => {
130                fabric.acl_remove(index as _)?;
131            }
132        }
133
134        Ok(())
135    }
136}
137
138impl ClusterHandler for AclHandler {
139    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required)).with_cmds(with!());
140
141    fn dataver(&self) -> u32 {
142        self.dataver.get()
143    }
144
145    fn dataver_changed(&self) {
146        self.dataver.changed();
147    }
148
149    fn acl<P: TLVBuilderParent>(
150        &self,
151        ctx: impl ReadContext,
152        builder: ArrayAttributeRead<
153            AccessControlEntryStructArrayBuilder<P>,
154            AccessControlEntryStructBuilder<P>,
155        >,
156    ) -> Result<P, Error> {
157        ctx.exchange()
158            .with_state(|state| self.acl(&state.fabrics, ctx.attr(), builder))
159    }
160
161    fn subjects_per_access_control_entry(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
162        Ok(acl::MAX_SUBJECTS_PER_ACL_ENTRY as _)
163    }
164
165    fn targets_per_access_control_entry(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
166        Ok(acl::MAX_TARGETS_PER_ACL_ENTRY as _)
167    }
168
169    fn access_control_entries_per_fabric(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
170        Ok(acl::MAX_ACL_ENTRIES_PER_FABRIC as _)
171    }
172
173    fn set_acl(
174        &self,
175        ctx: impl WriteContext,
176        value: ArrayAttributeWrite<
177            TLVArray<'_, AccessControlEntryStruct<'_>>,
178            AccessControlEntryStruct<'_>,
179        >,
180    ) -> Result<(), Error> {
181        let mut persist = FabricPersist::new(ctx.kv());
182
183        let accessor = ctx.exchange().accessor()?;
184        let admin_node_id: Nullable<u64> = match accessor.peer_node_id() {
185            Some(id) => Nullable::some(id),
186            None => Nullable::none(),
187        };
188        let admin_passcode_id: Nullable<u16> =
189            if matches!(accessor.auth_mode(), Some(AuthMode::Pase)) {
190                Nullable::some(0u16)
191            } else {
192                Nullable::none()
193            };
194
195        let fab_idx = NonZeroU8::new(ctx.attr().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;
196
197        // We emit `AccessControlEntryChanged` events while still holding the
198        // matter state lock so that we can compare the *old* and *new* ACL
199        // contents and produce one event per entry change. `Events::push` (used
200        // by the emit path) takes its own independent lock, so this is safe.
201        ctx.exchange().with_state(|state| {
202            let fabric = state.fabrics.fabric_mut(fab_idx)?;
203
204            match value {
205                ArrayAttributeWrite::Replace(list) => {
206                    // Snapshot old entries so we can diff against the new list per index
207                    // (Matter Core spec mandates one event per entry change,
208                    // with `LatestValue` populated). `MAX_ACL_ENTRIES_PER_FABRIC` is small
209                    // (default 4), so a stack-allocated snapshot is cheap.
210                    let mut old_entries: heapless::Vec<AclEntry, MAX_ACL_ENTRIES_PER_FABRIC> =
211                        heapless::Vec::new();
212                    for e in fabric.acl_iter() {
213                        let _ = old_entries.push(e.clone());
214                    }
215
216                    self.set_acl(fabric, ArrayAttributeWrite::Replace(list))?;
217
218                    let new_count = fabric.acl_iter().count();
219                    let old_count = old_entries.len();
220
221                    // Per-index Changed (overlap) and Added (new tail) events.
222                    for (i, entry) in fabric.acl_iter().enumerate() {
223                        let change = if i < old_count {
224                            ChangeTypeEnum::Changed
225                        } else {
226                            ChangeTypeEnum::Added
227                        };
228                        emit_acl_entry_changed(
229                            &ctx,
230                            admin_node_id.clone(),
231                            admin_passcode_id.clone(),
232                            change,
233                            entry,
234                            fab_idx.get(),
235                        )?;
236                    }
237
238                    // Removed events for entries that fell off the end of the list.
239                    // `LatestValue` for a removal is the entry's contents just before removal.
240                    for old_entry in old_entries.iter().skip(new_count) {
241                        emit_acl_entry_changed(
242                            &ctx,
243                            admin_node_id.clone(),
244                            admin_passcode_id.clone(),
245                            ChangeTypeEnum::Removed,
246                            old_entry,
247                            fab_idx.get(),
248                        )?;
249                    }
250                }
251                ArrayAttributeWrite::Add(entry) => {
252                    let old_count = fabric.acl_iter().count();
253                    self.set_acl(fabric, ArrayAttributeWrite::Add(entry))?;
254                    if let Some(new_entry) = fabric.acl_iter().nth(old_count) {
255                        emit_acl_entry_changed(
256                            &ctx,
257                            admin_node_id.clone(),
258                            admin_passcode_id.clone(),
259                            ChangeTypeEnum::Added,
260                            new_entry,
261                            fab_idx.get(),
262                        )?;
263                    }
264                }
265                ArrayAttributeWrite::Update(index, entry) => {
266                    let idx = index as usize;
267                    self.set_acl(fabric, ArrayAttributeWrite::Update(index, entry))?;
268                    if let Some(new_entry) = fabric.acl_iter().nth(idx) {
269                        emit_acl_entry_changed(
270                            &ctx,
271                            admin_node_id.clone(),
272                            admin_passcode_id.clone(),
273                            ChangeTypeEnum::Changed,
274                            new_entry,
275                            fab_idx.get(),
276                        )?;
277                    }
278                }
279                ArrayAttributeWrite::Remove(index) => {
280                    let idx = index as usize;
281                    let removed = fabric.acl_iter().nth(idx).cloned();
282                    self.set_acl(fabric, ArrayAttributeWrite::Remove(index))?;
283                    if let Some(old_entry) = removed {
284                        emit_acl_entry_changed(
285                            &ctx,
286                            admin_node_id.clone(),
287                            admin_passcode_id.clone(),
288                            ChangeTypeEnum::Removed,
289                            &old_entry,
290                            fab_idx.get(),
291                        )?;
292                    }
293                }
294            }
295
296            // NOTE: Not sure this is a spec-compliant behavor:
297            // If the failsafe is armed for our fabric, we'll NOT persist the groups changes until commissioning is complete.
298            // And we'll LOSE those changes if the failsafe times out before commissioning completes.
299            if !state.failsafe.is_armed_for(fab_idx.get()) {
300                persist.store(fabric)?;
301            }
302
303            Ok(())
304        })?;
305
306        persist.run()
307    }
308
309    fn handle_review_fabric_restrictions<P: TLVBuilderParent>(
310        &self,
311        _ctx: impl InvokeContext,
312        _request: ReviewFabricRestrictionsRequest<'_>,
313        _response: ReviewFabricRestrictionsResponseBuilder<P>,
314    ) -> Result<P, Error> {
315        // Only necessary with MNGD feature (ManagedDevice)
316        unimplemented!()
317    }
318}
319
320/// Emit one `AccessControlEntryChanged` event with the given change type and
321/// the entry's contents serialized into `LatestValue`.
322///
323/// Callers pass in the `admin_node_id` / `admin_passcode_id` derived from the
324/// requesting accessor (CASE → node id, PASE → passcode id 0). For changes
325/// originating from internal flows that do not have a requester (e.g. the
326/// auto-created admin entry on AddNOC, which runs over PASE), pass null/0.
327///
328/// The event is emitted on endpoint 0 (the AccessControl cluster always lives
329/// there), via `emit_for` so the helper can be used from cluster handlers
330/// other than ACL itself (e.g. from the OperationalCredentials handler when
331/// AddNOC seeds the initial admin entry).
332pub(crate) fn emit_acl_entry_changed<E>(
333    emitter: E,
334    admin_node_id: Nullable<u64>,
335    admin_passcode_id: Nullable<u16>,
336    change_type: ChangeTypeEnum,
337    entry: &AclEntry,
338    fab_idx: u8,
339) -> Result<(), Error>
340where
341    E: crate::dm::EventEmitter,
342{
343    AccessControlEntryChanged::emit_for(emitter, 0, |tw| {
344        let inner = tw
345            .admin_node_id(admin_node_id)?
346            .admin_passcode_id(admin_passcode_id)?
347            .change_type(change_type)?
348            .latest_value()?
349            .non_null()?;
350
351        // `read_into` populates the full `AccessControlEntryStruct` for the
352        // requested fabric. We pass `fab_idx == fab_idx` so that all
353        // fabric-sensitive fields are included in the event payload.
354        let parent = entry.read_into(fab_idx, Some(fab_idx), inner)?;
355
356        parent.fabric_index(Some(fab_idx))?.end()
357    })?;
358
359    Ok(())
360}
361
362#[cfg(test)]
363mod tests {
364    use core::cell::Cell;
365    use core::num::NonZeroU8;
366
367    use crate::acl::{AclEntry, AuthMode};
368    use crate::dm::clusters::acl::{
369        AccessControlEntryStruct, AccessControlEntryStructArrayBuilder, Dataver,
370    };
371    use crate::dm::{
372        ArrayAttributeRead, ArrayAttributeWrite, AttrDetails, AttrReadReplyInstance, Privilege,
373        ReadReply, ReadReplyInstance, Reply,
374    };
375    use crate::fabric::Fabrics;
376    use crate::tlv::{get_root_node_struct, TLVElement, TLVTag, TLVWriteParent, ToTLV};
377    use crate::utils::storage::WriteBuf;
378
379    use super::AclHandler;
380
381    use crate::acl::tests::{FAB_1, FAB_2};
382
383    #[test]
384    /// Add an ACL entry
385    fn acl_cluster_add() {
386        let mut buf: [u8; 100] = [0; 100];
387        let mut tw = WriteBuf::new(&mut buf);
388
389        let mut fabrics = Fabrics::new();
390
391        // Add fabric with ID 1
392        unwrap!(fabrics.add_with_post_init(|_| Ok(())));
393
394        let acl = AclHandler::new(Dataver::new(0));
395
396        let new = AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case);
397
398        unwrap!(new.to_tlv(&TLVTag::Anonymous, &mut tw));
399        let data = unwrap!(get_root_node_struct(tw.as_slice()));
400
401        // Test, ACL has fabric index 2, but the accessing fabric is 1
402        //    the fabric index in the TLV should be ignored and the ACL should be created with entry 1
403        acl_add(&acl, &mut fabrics, &data, FAB_1);
404
405        let verifier = AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case);
406        for fabric in fabrics.iter() {
407            for a in fabric.acl_iter() {
408                assert_eq!(*a, verifier);
409            }
410        }
411    }
412
413    #[test]
414    /// - The listindex used for edit should be relative to the current fabric
415    fn acl_cluster_edit() {
416        let mut buf: [u8; 100] = [0; 100];
417        let mut tw = WriteBuf::new(&mut buf);
418
419        let mut fabrics = Fabrics::new();
420
421        // Add fabric with ID 1
422        fabrics.add_with_post_init(|_| Ok(())).unwrap();
423
424        // Add fabric with ID 2
425        fabrics.add_with_post_init(|_| Ok(())).unwrap();
426
427        // Add 3 ACLs, belonging to fabric index 2, 1 and 2, in that order
428        let mut verifier = [
429            AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case),
430            AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case),
431            AclEntry::new(Some(FAB_2), Privilege::ADMIN, AuthMode::Case),
432        ];
433        for i in &verifier {
434            fabrics
435                .fabric_mut(i.fab_idx.unwrap())
436                .unwrap()
437                .acl_add(i.clone())
438                .unwrap();
439        }
440        let acl = AclHandler::new(Dataver::new(0));
441
442        let new = AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case);
443        new.to_tlv(&TLVTag::Anonymous, &mut tw).unwrap();
444        let data = get_root_node_struct(tw.as_slice()).unwrap();
445
446        // Test, Edit Fabric 2's index 1 - with accessing fabric as 2 - allow
447        acl_edit(&acl, &mut fabrics, 1, &data, FAB_2);
448        // Fabric 2's index 1, is actually our index 2, update the verifier
449        verifier[2] = new;
450
451        // Also validate in the fabrics that the entries are in the right order
452        assert_eq!(fabrics.get(FAB_1).unwrap().acl_iter().count(), 1);
453        assert_eq!(
454            fabrics.get(FAB_1).unwrap().acl_iter().next().unwrap(),
455            &verifier[1]
456        );
457        assert_eq!(fabrics.get(FAB_2).unwrap().acl_iter().count(), 2);
458        assert_eq!(
459            fabrics.get(FAB_2).unwrap().acl_iter().next().unwrap(),
460            &verifier[0]
461        );
462        assert_eq!(
463            fabrics.get(FAB_2).unwrap().acl_iter().nth(1).unwrap(),
464            &verifier[2]
465        );
466    }
467
468    #[test]
469    /// - The listindex used for delete should be relative to the current fabric
470    fn acl_cluster_delete() {
471        let mut fabrics = Fabrics::new();
472
473        // Add fabric with ID 1
474        fabrics.add_with_post_init(|_| Ok(())).unwrap();
475
476        // Add fabric with ID 2
477        fabrics.add_with_post_init(|_| Ok(())).unwrap();
478
479        // Add 3 ACLs, belonging to fabric index 2, 1 and 2, in that order
480        let input = [
481            AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case),
482            AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case),
483            AclEntry::new(Some(FAB_2), Privilege::ADMIN, AuthMode::Case),
484        ];
485        for i in &input {
486            fabrics
487                .fabric_mut(i.fab_idx.unwrap())
488                .unwrap()
489                .acl_add(i.clone())
490                .unwrap();
491        }
492        let acl = AclHandler::new(Dataver::new(0));
493
494        // Test: delete Fabric 1's index 0
495        acl_remove(&acl, &mut fabrics, 0, FAB_1);
496
497        let verifier = [input[0].clone(), input[2].clone()];
498        // Also validate in the fabrics that the entries are in the right order
499        let mut index = 0;
500        for fabric in fabrics.iter() {
501            for a in fabric.acl_iter() {
502                assert_eq!(*a, verifier[index]);
503                index += 1;
504            }
505        }
506    }
507
508    #[test]
509    /// - acl read with and without fabric filtering
510    fn acl_cluster_read() {
511        let mut buf: [u8; 100] = [0; 100];
512        let mut writebuf = WriteBuf::new(&mut buf);
513
514        let mut fabrics = Fabrics::new();
515
516        // Add fabric with ID 1
517        fabrics.add_with_post_init(|_| Ok(())).unwrap();
518
519        // Add fabric with ID 2
520        fabrics.add_with_post_init(|_| Ok(())).unwrap();
521
522        // Add 3 ACLs, belonging to fabric index 2, 1 and 2, in that order
523        let input = [
524            AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case),
525            AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case),
526            AclEntry::new(Some(FAB_2), Privilege::ADMIN, AuthMode::Case),
527        ];
528        for i in input {
529            fabrics
530                .fabric_mut(i.fab_idx.unwrap())
531                .unwrap()
532                .acl_add(i)
533                .unwrap();
534        }
535        let acl = AclHandler::new(Dataver::new(0));
536
537        // Test 1, all 3 entries are read in the response without fabric filtering
538        {
539            let attr = AttrDetails {
540                endpoint_id: 0,
541                cluster_id: 0,
542                attr_id: 0,
543                list_index: None,
544                list_chunked: false,
545                fab_idx: 1,
546                fab_filter: false,
547                dataver: None,
548                wildcard: false,
549                array: false,
550                cluster_status: Cell::new(0),
551            };
552
553            acl_read(&acl, &fabrics, &attr, &mut writebuf);
554            assert_eq!(
555                &[
556                    21, 53, 1, 36, 0, 0, 55, 1, 36, 2, 0, 36, 3, 0, 36, 4, 0, 24, 54, 2, 21, 36, 1,
557                    1, 36, 2, 2, 52, 3, 52, 4, 36, 254, 1, 24, 21, 36, 254, 2, 24, 21, 36, 254, 2,
558                    24, 24, 24, 24
559                ],
560                writebuf.as_slice()
561            );
562        }
563        writebuf.reset();
564
565        // Test 2, only single entry is read in the response with fabric filtering and fabric idx 1
566        {
567            let attr = AttrDetails {
568                endpoint_id: 0,
569                cluster_id: 0,
570                attr_id: 0,
571                list_index: None,
572                list_chunked: false,
573                fab_idx: 1,
574                fab_filter: true,
575                dataver: None,
576                wildcard: false,
577                array: false,
578                cluster_status: Cell::new(0),
579            };
580
581            acl_read(&acl, &fabrics, &attr, &mut writebuf);
582            assert_eq!(
583                &[
584                    21, 53, 1, 36, 0, 0, 55, 1, 36, 2, 0, 36, 3, 0, 36, 4, 0, 24, 54, 2, 21, 36, 1,
585                    1, 36, 2, 2, 52, 3, 52, 4, 36, 254, 1, 24, 24, 24, 24
586                ],
587                writebuf.as_slice()
588            );
589        }
590        writebuf.reset();
591
592        // Test 3, only single entry is read in the response with fabric filtering and fabric idx 2
593        {
594            let attr = AttrDetails {
595                endpoint_id: 0,
596                cluster_id: 0,
597                attr_id: 0,
598                list_index: None,
599                list_chunked: false,
600                fab_idx: 2,
601                fab_filter: true,
602                dataver: None,
603                wildcard: false,
604                array: false,
605                cluster_status: Cell::new(0),
606            };
607
608            acl_read(&acl, &fabrics, &attr, &mut writebuf);
609            assert_eq!(
610                &[
611                    21, 53, 1, 36, 0, 0, 55, 1, 36, 2, 0, 36, 3, 0, 36, 4, 0, 24, 54, 2, 21, 36, 1,
612                    1, 36, 2, 2, 52, 3, 52, 4, 36, 254, 2, 24, 21, 36, 1, 5, 36, 2, 2, 52, 3, 52,
613                    4, 36, 254, 2, 24, 24, 24, 24
614                ],
615                writebuf.as_slice()
616            );
617        }
618    }
619
620    fn acl_read(acl: &AclHandler, fabrics: &Fabrics, attr: &AttrDetails, tw: &mut WriteBuf<'_>) {
621        let encoder = ReadReplyInstance::new(attr, &mut *tw);
622        let mut writer = unwrap!(unwrap!(encoder.with_dataver(acl.dataver.get())));
623        let build_root = TLVWriteParent::new((), writer.writer());
624        unwrap!(acl.acl(
625            fabrics,
626            attr,
627            ArrayAttributeRead::ReadAll(unwrap!(AccessControlEntryStructArrayBuilder::new(
628                build_root,
629                &AttrReadReplyInstance::<WriteBuf>::TAG
630            )))
631        ));
632
633        unwrap!(writer.complete());
634    }
635
636    fn acl_add(acl: &AclHandler, fabrics: &mut Fabrics, data: &TLVElement<'_>, fab_idx: NonZeroU8) {
637        unwrap!(acl.set_acl(
638            fabrics.fabric_mut(fab_idx).unwrap(),
639            ArrayAttributeWrite::Add(AccessControlEntryStruct::new(data.clone())),
640        ));
641    }
642
643    fn acl_edit(
644        acl: &AclHandler,
645        fabrics: &mut Fabrics,
646        index: u16,
647        data: &TLVElement<'_>,
648        fab_idx: NonZeroU8,
649    ) {
650        unwrap!(acl.set_acl(
651            fabrics.fabric_mut(fab_idx).unwrap(),
652            ArrayAttributeWrite::Update(index, AccessControlEntryStruct::new(data.clone())),
653        ));
654    }
655
656    fn acl_remove(acl: &AclHandler, fabrics: &mut Fabrics, index: u16, fab_idx: NonZeroU8) {
657        unwrap!(acl.set_acl(
658            fabrics.fabric_mut(fab_idx).unwrap(),
659            ArrayAttributeWrite::Remove(index)
660        ));
661    }
662}