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::endpoints::ROOT_ENDPOINT_ID;
24use crate::dm::{
25    ArrayAttributeRead, ArrayAttributeWrite, AttrDetails, Cluster, Dataver, HandlerContext,
26    InvokeContext, NodeId, ReadContext, WriteContext,
27};
28use crate::error::{Error, ErrorCode};
29use crate::fabric::{Fabric, FabricPersist, Fabrics};
30use crate::tlv::{Nullable, TLVArray, TLVBuilderParent};
31use crate::utils::init::stack_try_pin_init;
32use crate::with;
33
34pub use crate::dm::clusters::decl::access_control::*;
35
36/// The system implementation of a handler for the Access Control Matter cluster.
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
39pub struct AclHandler {
40    dataver: Dataver,
41}
42
43impl AclHandler {
44    /// Create a new instance of `AclHandler` with the given `dataver`
45    pub const fn new(dataver: Dataver) -> Self {
46        Self { dataver }
47    }
48
49    /// Adapt the handler instance to the generic `rs-matter` `Handler` trait
50    pub const fn adapt(self) -> HandlerAdaptor<Self> {
51        HandlerAdaptor(self)
52    }
53
54    /// For unit-testing
55    /// Read the ACL entries from the fabrics and write them into the builder
56    fn acl<P: TLVBuilderParent>(
57        &self,
58        fabrics: &Fabrics,
59        attr: &AttrDetails,
60        builder: ArrayAttributeRead<
61            AccessControlEntryStructArrayBuilder<P>,
62            AccessControlEntryStructBuilder<P>,
63        >,
64    ) -> Result<P, Error> {
65        let mut acls = fabrics
66            .iter()
67            .filter(|fabric| !attr.fab_filter || fabric.fab_idx().get() == attr.fab_idx)
68            .flat_map(|fabric| fabric.acl_iter().map(|entry| (fabric.fab_idx(), entry)));
69
70        match builder {
71            ArrayAttributeRead::ReadAll(mut builder) => {
72                for (fab_idx, entry) in acls {
73                    builder =
74                        entry.read_into(attr.fab_idx, Some(fab_idx.get()), builder.push()?)?;
75                }
76
77                builder.end()
78            }
79            ArrayAttributeRead::ReadOne(index, builder) => {
80                let Some((fab_idx, entry)) = acls.nth(index as usize) else {
81                    return Err(ErrorCode::ConstraintError.into());
82                };
83
84                entry.read_into(attr.fab_idx, Some(fab_idx.get()), builder)
85            }
86            ArrayAttributeRead::ReadNone(builder) => builder.end(),
87        }
88    }
89
90    /// For unit-testing
91    /// Set the ACL entries in the fabrics
92    fn set_acl(
93        &self,
94        fabric: &mut Fabric,
95        value: ArrayAttributeWrite<
96            TLVArray<'_, AccessControlEntryStruct<'_>>,
97            AccessControlEntryStruct<'_>,
98        >,
99    ) -> Result<(), Error> {
100        match value {
101            ArrayAttributeWrite::Replace(list) => {
102                // Check the well-formedness of the list first & init to check validity
103                let mut count: usize = 0;
104                for entry in &list {
105                    count += 1;
106                    if count > MAX_ACL_ENTRIES_PER_FABRIC {
107                        return Err(ErrorCode::ResourceExhausted.into());
108                    }
109                    let entry = entry?;
110                    // Init a dummy to propagate failures for bad inputs
111                    stack_try_pin_init!(let _processed =? AclEntry::init_with(fabric.fab_idx(), &entry));
112                }
113
114                // Now add everything once we know all are valid
115                fabric.acl_remove_all();
116                for entry in &list {
117                    // unwrap! calls below can't fail because we already checked that the entry is well-formed
118                    // and the length of the list is within the limit
119                    let entry = unwrap!(entry);
120                    unwrap!(fabric.acl_add_init(AclEntry::init_with(fabric.fab_idx(), &entry)));
121                }
122            }
123            ArrayAttributeWrite::Add(entry) => {
124                fabric.acl_add_init(AclEntry::init_with(fabric.fab_idx(), &entry))?;
125            }
126            ArrayAttributeWrite::Update(index, entry) => {
127                fabric
128                    .acl_update_init(index as _, AclEntry::init_with(fabric.fab_idx(), &entry))?;
129            }
130            ArrayAttributeWrite::Remove(index) => {
131                fabric.acl_remove(index as _)?;
132            }
133        }
134
135        Ok(())
136    }
137
138    /// Serialize one synthesized `AuxiliaryACL` entry, applying the same
139    /// fabric-sensitive redaction as the writable-`ACL` read: entries of
140    /// other fabrics expose only their fabric index.
141    #[cfg(feature = "groups")]
142    fn build_auxiliary_entry<P: TLVBuilderParent>(
143        accessing_fab_idx: u8,
144        fab_idx: NonZeroU8,
145        group_id: u16,
146        endpoints: &[crate::dm::EndptId],
147        builder: AccessControlEntryStructBuilder<P>,
148    ) -> Result<P, Error> {
149        use crate::tlv::Nullable;
150
151        let same_fab_idx = accessing_fab_idx == fab_idx.get();
152
153        builder
154            .privilege(same_fab_idx.then_some(AccessControlEntryPrivilegeEnum::Operate))?
155            .auth_mode(same_fab_idx.then_some(AccessControlEntryAuthModeEnum::Group))?
156            .subjects()?
157            .with_some_if(same_fab_idx, |builder| {
158                builder.with_non_null(
159                    Nullable::some([group_id as u64]),
160                    |subjects, mut builder| {
161                        for subject in subjects {
162                            builder = builder.push(subject)?;
163                        }
164
165                        builder.end()
166                    },
167                )
168            })?
169            .targets()?
170            .with_some_if(same_fab_idx, |builder| {
171                builder.with_non_null(Nullable::some(endpoints), |endpoints, mut builder| {
172                    for endpoint in *endpoints {
173                        builder = builder
174                            .push()?
175                            .cluster(Nullable::none())?
176                            .endpoint(Nullable::some(*endpoint))?
177                            .device_type(Nullable::none())?
178                            .end()?;
179                    }
180
181                    builder.end()
182                })
183            })?
184            .auxiliary_type(same_fab_idx.then_some(AccessControlAuxiliaryTypeEnum::Groupcast))?
185            .fabric_index(Some(fab_idx.get()))?
186            .end()
187    }
188}
189
190impl ClusterHandler for AclHandler {
191    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required)).with_cmds(with!());
192
193    fn dataver(&self) -> u32 {
194        self.dataver.get()
195    }
196
197    fn dataver_changed(&self) {
198        self.dataver.changed();
199    }
200
201    fn acl<P: TLVBuilderParent>(
202        &self,
203        ctx: impl ReadContext,
204        builder: ArrayAttributeRead<
205            AccessControlEntryStructArrayBuilder<P>,
206            AccessControlEntryStructBuilder<P>,
207        >,
208    ) -> Result<P, Error> {
209        ctx.exchange()
210            .with_state(|state| self.acl(&state.fabrics, ctx.attr(), builder))
211    }
212
213    fn auxiliary_acl<P: TLVBuilderParent>(
214        &self,
215        ctx: impl ReadContext,
216        builder: ArrayAttributeRead<
217            AccessControlEntryStructArrayBuilder<P>,
218            AccessControlEntryStructBuilder<P>,
219        >,
220    ) -> Result<P, Error> {
221        // The entries are *derived* on the fly from the producing feature's
222        // state - the Groupcast group table - never stored (see the note on
223        // [`CLUSTER_AUX`]). Without the `groups` feature no producer exists,
224        // and the attribute is an empty list.
225        #[cfg(feature = "groups")]
226        {
227            ctx.exchange().with_state(|state| {
228                let attr = ctx.attr();
229
230                // One synthesized entry per (fabric, aux-flagged group,
231                // targets-capacity chunk of its endpoints)
232                let mut entries = state
233                    .fabrics
234                    .iter()
235                    .filter(|fabric| !attr.fab_filter || fabric.fab_idx().get() == attr.fab_idx)
236                    .flat_map(|fabric| {
237                        fabric
238                            .groups()
239                            .iter()
240                            .filter(|entry| entry.has_aux_acl() && !entry.endpoints.is_empty())
241                            .flat_map(move |entry| {
242                                entry
243                                    .endpoints
244                                    .chunks(acl::MAX_TARGETS_PER_ACL_ENTRY)
245                                    .map(move |endpoints| (fabric, entry.group_id, endpoints))
246                            })
247                    });
248
249                match builder {
250                    ArrayAttributeRead::ReadAll(mut builder) => {
251                        for (fabric, group_id, endpoints) in entries {
252                            builder = Self::build_auxiliary_entry(
253                                attr.fab_idx,
254                                fabric.fab_idx(),
255                                group_id,
256                                endpoints,
257                                builder.push()?,
258                            )?;
259                        }
260
261                        builder.end()
262                    }
263                    ArrayAttributeRead::ReadOne(index, builder) => {
264                        let Some((fabric, group_id, endpoints)) = entries.nth(index as usize)
265                        else {
266                            return Err(ErrorCode::ConstraintError.into());
267                        };
268
269                        Self::build_auxiliary_entry(
270                            attr.fab_idx,
271                            fabric.fab_idx(),
272                            group_id,
273                            endpoints,
274                            builder,
275                        )
276                    }
277                    ArrayAttributeRead::ReadNone(builder) => builder.end(),
278                }
279            })
280        }
281
282        #[cfg(not(feature = "groups"))]
283        {
284            let _ = ctx;
285
286            match builder {
287                ArrayAttributeRead::ReadAll(builder) => builder.end(),
288                ArrayAttributeRead::ReadOne(_, _) => Err(ErrorCode::ConstraintError.into()),
289                ArrayAttributeRead::ReadNone(builder) => builder.end(),
290            }
291        }
292    }
293
294    fn subjects_per_access_control_entry(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
295        Ok(acl::MAX_SUBJECTS_PER_ACL_ENTRY as _)
296    }
297
298    fn targets_per_access_control_entry(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
299        Ok(acl::MAX_TARGETS_PER_ACL_ENTRY as _)
300    }
301
302    fn access_control_entries_per_fabric(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
303        Ok(acl::MAX_ACL_ENTRIES_PER_FABRIC as _)
304    }
305
306    fn set_acl(
307        &self,
308        ctx: impl WriteContext,
309        value: ArrayAttributeWrite<
310            TLVArray<'_, AccessControlEntryStruct<'_>>,
311            AccessControlEntryStruct<'_>,
312        >,
313    ) -> Result<(), Error> {
314        let mut persist = FabricPersist::new(ctx.kv());
315
316        let accessor = ctx.accessor()?;
317        let admin_node_id: Nullable<u64> = match accessor.peer_node_id() {
318            Some(id) => Nullable::some(id),
319            None => Nullable::none(),
320        };
321        let admin_passcode_id: Nullable<u16> =
322            if matches!(accessor.auth_mode(), Some(AuthMode::Pase)) {
323                Nullable::some(0u16)
324            } else {
325                Nullable::none()
326            };
327
328        let fab_idx = NonZeroU8::new(ctx.attr().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;
329
330        // We emit `AccessControlEntryChanged` events while still holding the
331        // matter state lock so that we can compare the *old* and *new* ACL
332        // contents and produce one event per entry change. `Events::push` (used
333        // by the emit path) takes its own independent lock, so this is safe.
334        ctx.exchange().with_state(|state| {
335            let fabric = state.fabrics.fabric_mut(fab_idx)?;
336
337            match value {
338                ArrayAttributeWrite::Replace(list) => {
339                    // Snapshot old entries so we can diff against the new list per index
340                    // (Matter Core spec mandates one event per entry change,
341                    // with `LatestValue` populated). `MAX_ACL_ENTRIES_PER_FABRIC` is small
342                    // (default 4), so a stack-allocated snapshot is cheap.
343                    let mut old_entries: heapless::Vec<AclEntry, MAX_ACL_ENTRIES_PER_FABRIC> =
344                        heapless::Vec::new();
345                    for e in fabric.acl_iter() {
346                        let _ = old_entries.push(e.clone());
347                    }
348
349                    self.set_acl(fabric, ArrayAttributeWrite::Replace(list))?;
350
351                    let new_count = fabric.acl_iter().count();
352                    let old_count = old_entries.len();
353
354                    // Per-index Changed (overlap) and Added (new tail) events.
355                    for (i, entry) in fabric.acl_iter().enumerate() {
356                        let change = if i < old_count {
357                            ChangeTypeEnum::Changed
358                        } else {
359                            ChangeTypeEnum::Added
360                        };
361                        emit_acl_entry_changed(
362                            &ctx,
363                            admin_node_id.clone(),
364                            admin_passcode_id.clone(),
365                            change,
366                            entry,
367                            fab_idx.get(),
368                        )?;
369                    }
370
371                    // Removed events for entries that fell off the end of the list.
372                    // `LatestValue` for a removal is the entry's contents just before removal.
373                    for old_entry in old_entries.iter().skip(new_count) {
374                        emit_acl_entry_changed(
375                            &ctx,
376                            admin_node_id.clone(),
377                            admin_passcode_id.clone(),
378                            ChangeTypeEnum::Removed,
379                            old_entry,
380                            fab_idx.get(),
381                        )?;
382                    }
383                }
384                ArrayAttributeWrite::Add(entry) => {
385                    let old_count = fabric.acl_iter().count();
386                    self.set_acl(fabric, ArrayAttributeWrite::Add(entry))?;
387                    if let Some(new_entry) = fabric.acl_iter().nth(old_count) {
388                        emit_acl_entry_changed(
389                            &ctx,
390                            admin_node_id.clone(),
391                            admin_passcode_id.clone(),
392                            ChangeTypeEnum::Added,
393                            new_entry,
394                            fab_idx.get(),
395                        )?;
396                    }
397                }
398                ArrayAttributeWrite::Update(index, entry) => {
399                    let idx = index as usize;
400                    self.set_acl(fabric, ArrayAttributeWrite::Update(index, entry))?;
401                    if let Some(new_entry) = fabric.acl_iter().nth(idx) {
402                        emit_acl_entry_changed(
403                            &ctx,
404                            admin_node_id.clone(),
405                            admin_passcode_id.clone(),
406                            ChangeTypeEnum::Changed,
407                            new_entry,
408                            fab_idx.get(),
409                        )?;
410                    }
411                }
412                ArrayAttributeWrite::Remove(index) => {
413                    let idx = index as usize;
414                    let removed = fabric.acl_iter().nth(idx).cloned();
415                    self.set_acl(fabric, ArrayAttributeWrite::Remove(index))?;
416                    if let Some(old_entry) = removed {
417                        emit_acl_entry_changed(
418                            &ctx,
419                            admin_node_id.clone(),
420                            admin_passcode_id.clone(),
421                            ChangeTypeEnum::Removed,
422                            &old_entry,
423                            fab_idx.get(),
424                        )?;
425                    }
426                }
427            }
428
429            // NOTE: Not sure this is a spec-compliant behavor:
430            // If the failsafe is armed for our fabric, we'll NOT persist the groups changes until commissioning is complete.
431            // And we'll LOSE those changes if the failsafe times out before commissioning completes.
432            if !state.failsafe.is_armed_for(fab_idx.get()) {
433                persist.store(fabric)?;
434            }
435
436            Ok(())
437        })?;
438
439        persist.run()
440    }
441
442    fn handle_review_fabric_restrictions<P: TLVBuilderParent>(
443        &self,
444        _ctx: impl InvokeContext,
445        _request: ReviewFabricRestrictionsRequest<'_>,
446        _response: ReviewFabricRestrictionsResponseBuilder<P>,
447    ) -> Result<P, Error> {
448        // Only necessary with MNGD feature (ManagedDevice)
449        unimplemented!()
450    }
451}
452
453/// `AccessControl` cluster metadata that additionally advertises the
454/// provisional `AUXILIARY` feature and the `AuxiliaryACL` attribute.
455///
456/// Use this in place of [`AclHandler::CLUSTER`] on nodes where features (e.g.
457/// a Groupcast cluster) synthesize auxiliary ACL entries.
458///
459/// At startup, `AclHandler` inspects the node metadata and - when this
460/// feature is advertised - switches the access-control evaluation of
461/// wildcard-target Group-auth entries to exclude the root endpoint, as the
462/// Matter Core spec mandates for nodes with the feature.
463pub const CLUSTER_AUX: Cluster<'static> = FULL_CLUSTER
464    .with_attrs(with!(required; AttributeId::AuxiliaryACL))
465    .with_cmds(with!())
466    .with_features(Feature::AUXILIARY.bits());
467
468/// Notify the data model that the `AuxiliaryACL` attribute changed, and emit
469/// the `AuxiliaryAccessUpdated` event.
470pub fn notify_auxiliary_access_updated(
471    ctx: &impl HandlerContext,
472    admin_node_id: Option<NodeId>,
473    fab_idx: NonZeroU8,
474) -> Result<(), Error> {
475    ctx.notify_attr_changed(
476        ROOT_ENDPOINT_ID,
477        FULL_CLUSTER.id,
478        AttributeId::AuxiliaryACL as _,
479    );
480
481    AuxiliaryAccessUpdated::emit_for(ctx, ROOT_ENDPOINT_ID, |event| {
482        event
483            .admin_node_id(Nullable::new(admin_node_id))?
484            .fabric_index(Some(fab_idx.get()))?
485            .end()
486    })
487    .map(|_event_number| ())
488}
489
490/// Emit one `AccessControlEntryChanged` event with the given change type and
491/// the entry's contents serialized into `LatestValue`.
492pub(crate) fn emit_acl_entry_changed<E>(
493    emitter: E,
494    admin_node_id: Nullable<u64>,
495    admin_passcode_id: Nullable<u16>,
496    change_type: ChangeTypeEnum,
497    entry: &AclEntry,
498    fab_idx: u8,
499) -> Result<(), Error>
500where
501    E: crate::dm::EventEmitter,
502{
503    AccessControlEntryChanged::emit_for(emitter, 0, |tw| {
504        let inner = tw
505            .admin_node_id(admin_node_id)?
506            .admin_passcode_id(admin_passcode_id)?
507            .change_type(change_type)?
508            .latest_value()?
509            .non_null()?;
510
511        // `read_into` populates the full `AccessControlEntryStruct` for the
512        // requested fabric. We pass `fab_idx == fab_idx` so that all
513        // fabric-sensitive fields are included in the event payload.
514        let parent = entry.read_into(fab_idx, Some(fab_idx), inner)?;
515
516        parent.fabric_index(Some(fab_idx))?.end()
517    })?;
518
519    Ok(())
520}
521
522#[cfg(test)]
523mod tests {
524    use core::cell::Cell;
525    use core::num::NonZeroU8;
526
527    use crate::acl::{AclEntry, AuthMode};
528    use crate::dm::clusters::acl::{
529        AccessControlEntryStruct, AccessControlEntryStructArrayBuilder, Dataver,
530    };
531    use crate::dm::{
532        ArrayAttributeRead, ArrayAttributeWrite, AttrDetails, AttrReadReplyInstance, Privilege,
533        ReadReply, ReadReplyInstance, Reply,
534    };
535    use crate::fabric::Fabrics;
536    use crate::tlv::{get_root_node_struct, TLVElement, TLVTag, TLVWriteParent, ToTLV};
537    use crate::utils::storage::WriteBuf;
538
539    use super::AclHandler;
540
541    use crate::acl::tests::{FAB_1, FAB_2};
542
543    #[test]
544    /// Add an ACL entry
545    fn acl_cluster_add() {
546        let mut buf: [u8; 100] = [0; 100];
547        let mut tw = WriteBuf::new(&mut buf);
548
549        let mut fabrics = Fabrics::new();
550
551        // Add fabric with ID 1
552        unwrap!(fabrics.add_with_post_init(|_| Ok(())));
553
554        let acl = AclHandler::new(Dataver::new(0));
555
556        let new = AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case);
557
558        unwrap!(new.to_tlv(&TLVTag::Anonymous, &mut tw));
559        let data = unwrap!(get_root_node_struct(tw.as_slice()));
560
561        // Test, ACL has fabric index 2, but the accessing fabric is 1
562        //    the fabric index in the TLV should be ignored and the ACL should be created with entry 1
563        acl_add(&acl, &mut fabrics, &data, FAB_1);
564
565        let verifier = AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case);
566        for fabric in fabrics.iter() {
567            for a in fabric.acl_iter() {
568                assert_eq!(*a, verifier);
569            }
570        }
571    }
572
573    #[test]
574    /// - The listindex used for edit should be relative to the current fabric
575    fn acl_cluster_edit() {
576        let mut buf: [u8; 100] = [0; 100];
577        let mut tw = WriteBuf::new(&mut buf);
578
579        let mut fabrics = Fabrics::new();
580
581        // Add fabric with ID 1
582        fabrics.add_with_post_init(|_| Ok(())).unwrap();
583
584        // Add fabric with ID 2
585        fabrics.add_with_post_init(|_| Ok(())).unwrap();
586
587        // Add 3 ACLs, belonging to fabric index 2, 1 and 2, in that order
588        let mut verifier = [
589            AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case),
590            AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case),
591            AclEntry::new(Some(FAB_2), Privilege::ADMIN, AuthMode::Case),
592        ];
593        for i in &verifier {
594            fabrics
595                .fabric_mut(i.fab_idx.unwrap())
596                .unwrap()
597                .acl_add(i.clone())
598                .unwrap();
599        }
600        let acl = AclHandler::new(Dataver::new(0));
601
602        let new = AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case);
603        new.to_tlv(&TLVTag::Anonymous, &mut tw).unwrap();
604        let data = get_root_node_struct(tw.as_slice()).unwrap();
605
606        // Test, Edit Fabric 2's index 1 - with accessing fabric as 2 - allow
607        acl_edit(&acl, &mut fabrics, 1, &data, FAB_2);
608        // Fabric 2's index 1, is actually our index 2, update the verifier
609        verifier[2] = new;
610
611        // Also validate in the fabrics that the entries are in the right order
612        assert_eq!(fabrics.get(FAB_1).unwrap().acl_iter().count(), 1);
613        assert_eq!(
614            fabrics.get(FAB_1).unwrap().acl_iter().next().unwrap(),
615            &verifier[1]
616        );
617        assert_eq!(fabrics.get(FAB_2).unwrap().acl_iter().count(), 2);
618        assert_eq!(
619            fabrics.get(FAB_2).unwrap().acl_iter().next().unwrap(),
620            &verifier[0]
621        );
622        assert_eq!(
623            fabrics.get(FAB_2).unwrap().acl_iter().nth(1).unwrap(),
624            &verifier[2]
625        );
626    }
627
628    #[test]
629    /// - The listindex used for delete should be relative to the current fabric
630    fn acl_cluster_delete() {
631        let mut fabrics = Fabrics::new();
632
633        // Add fabric with ID 1
634        fabrics.add_with_post_init(|_| Ok(())).unwrap();
635
636        // Add fabric with ID 2
637        fabrics.add_with_post_init(|_| Ok(())).unwrap();
638
639        // Add 3 ACLs, belonging to fabric index 2, 1 and 2, in that order
640        let input = [
641            AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case),
642            AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case),
643            AclEntry::new(Some(FAB_2), Privilege::ADMIN, AuthMode::Case),
644        ];
645        for i in &input {
646            fabrics
647                .fabric_mut(i.fab_idx.unwrap())
648                .unwrap()
649                .acl_add(i.clone())
650                .unwrap();
651        }
652        let acl = AclHandler::new(Dataver::new(0));
653
654        // Test: delete Fabric 1's index 0
655        acl_remove(&acl, &mut fabrics, 0, FAB_1);
656
657        let verifier = [input[0].clone(), input[2].clone()];
658        // Also validate in the fabrics that the entries are in the right order
659        let mut index = 0;
660        for fabric in fabrics.iter() {
661            for a in fabric.acl_iter() {
662                assert_eq!(*a, verifier[index]);
663                index += 1;
664            }
665        }
666    }
667
668    #[test]
669    /// - acl read with and without fabric filtering
670    fn acl_cluster_read() {
671        let mut buf: [u8; 100] = [0; 100];
672        let mut writebuf = WriteBuf::new(&mut buf);
673
674        let mut fabrics = Fabrics::new();
675
676        // Add fabric with ID 1
677        fabrics.add_with_post_init(|_| Ok(())).unwrap();
678
679        // Add fabric with ID 2
680        fabrics.add_with_post_init(|_| Ok(())).unwrap();
681
682        // Add 3 ACLs, belonging to fabric index 2, 1 and 2, in that order
683        let input = [
684            AclEntry::new(Some(FAB_2), Privilege::VIEW, AuthMode::Case),
685            AclEntry::new(Some(FAB_1), Privilege::VIEW, AuthMode::Case),
686            AclEntry::new(Some(FAB_2), Privilege::ADMIN, AuthMode::Case),
687        ];
688        for i in input {
689            fabrics
690                .fabric_mut(i.fab_idx.unwrap())
691                .unwrap()
692                .acl_add(i)
693                .unwrap();
694        }
695        let acl = AclHandler::new(Dataver::new(0));
696
697        // Test 1, all 3 entries are read in the response without fabric filtering
698        {
699            let attr = AttrDetails {
700                endpoint_id: 0,
701                cluster_id: 0,
702                attr_id: 0,
703                list_index: None,
704                list_chunked: false,
705                fab_idx: 1,
706                fab_filter: false,
707                dataver: None,
708                wildcard: false,
709                array: false,
710                cluster_status: Cell::new(0),
711            };
712
713            acl_read(&acl, &fabrics, &attr, &mut writebuf);
714            assert_eq!(
715                &[
716                    21, 53, 1, 36, 0, 0, 55, 1, 36, 2, 0, 36, 3, 0, 36, 4, 0, 24, 54, 2, 21, 36, 1,
717                    1, 36, 2, 2, 52, 3, 52, 4, 36, 254, 1, 24, 21, 36, 254, 2, 24, 21, 36, 254, 2,
718                    24, 24, 24, 24
719                ],
720                writebuf.as_slice()
721            );
722        }
723        writebuf.reset();
724
725        // Test 2, only single entry is read in the response with fabric filtering and fabric idx 1
726        {
727            let attr = AttrDetails {
728                endpoint_id: 0,
729                cluster_id: 0,
730                attr_id: 0,
731                list_index: None,
732                list_chunked: false,
733                fab_idx: 1,
734                fab_filter: true,
735                dataver: None,
736                wildcard: false,
737                array: false,
738                cluster_status: Cell::new(0),
739            };
740
741            acl_read(&acl, &fabrics, &attr, &mut writebuf);
742            assert_eq!(
743                &[
744                    21, 53, 1, 36, 0, 0, 55, 1, 36, 2, 0, 36, 3, 0, 36, 4, 0, 24, 54, 2, 21, 36, 1,
745                    1, 36, 2, 2, 52, 3, 52, 4, 36, 254, 1, 24, 24, 24, 24
746                ],
747                writebuf.as_slice()
748            );
749        }
750        writebuf.reset();
751
752        // Test 3, only single entry is read in the response with fabric filtering and fabric idx 2
753        {
754            let attr = AttrDetails {
755                endpoint_id: 0,
756                cluster_id: 0,
757                attr_id: 0,
758                list_index: None,
759                list_chunked: false,
760                fab_idx: 2,
761                fab_filter: true,
762                dataver: None,
763                wildcard: false,
764                array: false,
765                cluster_status: Cell::new(0),
766            };
767
768            acl_read(&acl, &fabrics, &attr, &mut writebuf);
769            assert_eq!(
770                &[
771                    21, 53, 1, 36, 0, 0, 55, 1, 36, 2, 0, 36, 3, 0, 36, 4, 0, 24, 54, 2, 21, 36, 1,
772                    1, 36, 2, 2, 52, 3, 52, 4, 36, 254, 2, 24, 21, 36, 1, 5, 36, 2, 2, 52, 3, 52,
773                    4, 36, 254, 2, 24, 24, 24, 24
774                ],
775                writebuf.as_slice()
776            );
777        }
778    }
779
780    fn acl_read(acl: &AclHandler, fabrics: &Fabrics, attr: &AttrDetails, tw: &mut WriteBuf<'_>) {
781        let encoder = ReadReplyInstance::new(attr, &mut *tw);
782        let mut writer = unwrap!(unwrap!(encoder.with_dataver(acl.dataver.get())));
783        let build_root = TLVWriteParent::new((), writer.writer());
784        unwrap!(acl.acl(
785            fabrics,
786            attr,
787            ArrayAttributeRead::ReadAll(unwrap!(AccessControlEntryStructArrayBuilder::new(
788                build_root,
789                &AttrReadReplyInstance::<WriteBuf>::TAG
790            )))
791        ));
792
793        unwrap!(writer.complete());
794    }
795
796    fn acl_add(acl: &AclHandler, fabrics: &mut Fabrics, data: &TLVElement<'_>, fab_idx: NonZeroU8) {
797        unwrap!(acl.set_acl(
798            fabrics.fabric_mut(fab_idx).unwrap(),
799            ArrayAttributeWrite::Add(AccessControlEntryStruct::new(data.clone())),
800        ));
801    }
802
803    fn acl_edit(
804        acl: &AclHandler,
805        fabrics: &mut Fabrics,
806        index: u16,
807        data: &TLVElement<'_>,
808        fab_idx: NonZeroU8,
809    ) {
810        unwrap!(acl.set_acl(
811            fabrics.fabric_mut(fab_idx).unwrap(),
812            ArrayAttributeWrite::Update(index, AccessControlEntryStruct::new(data.clone())),
813        ));
814    }
815
816    fn acl_remove(acl: &AclHandler, fabrics: &mut Fabrics, index: u16, fab_idx: NonZeroU8) {
817        unwrap!(acl.set_acl(
818            fabrics.fabric_mut(fab_idx).unwrap(),
819            ArrayAttributeWrite::Remove(index)
820        ));
821    }
822}