Skip to main content

rs_matter/
acl.rs

1/*
2 *
3 *    Copyright (c) 2022-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 `rs-matter` Access Control List (ACL)
19
20use core::fmt::Display;
21use core::num::NonZeroU8;
22use core::ops::RangeInclusive;
23
24use cfg_if::cfg_if;
25
26use num_derive::FromPrimitive;
27
28use crate::dm::clusters::acl::{
29    AccessControlAuxiliaryTypeEnum, AccessControlEntryAuthModeEnum,
30    AccessControlEntryPrivilegeEnum, AccessControlEntryStruct, AccessControlEntryStructBuilder,
31};
32use crate::dm::{Access, ClusterId, DeviceType, EndptId, NodeId, Privilege};
33use crate::error::{Error, ErrorCode};
34use crate::im::GenericPath;
35use crate::tlv::{FromTLV, Nullable, TLVBuilderParent, TLVElement, TLVTag, TLVWrite, ToTLV, TLV};
36use crate::transport::session::{Session, SessionMode, MAX_CAT_IDS_PER_NOC};
37use crate::utils::init::{init, Init, IntoFallibleInit};
38use crate::utils::storage::Vec;
39use crate::Matter;
40
41cfg_if! {
42    if #[cfg(feature = "max-subjects-per-acl-32")] {
43        /// Max subjects per ACL entry
44        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 32;
45    } else if #[cfg(feature = "max-subjects-per-acl-16")] {
46        /// Max subjects per ACL entry
47        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 16;
48    } else if #[cfg(feature = "max-subjects-per-acl-8")] {
49        /// Max subjects per ACL entry
50        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 8;
51    } else if #[cfg(feature = "max-subjects-per-acl-7")] {
52        /// Max subjects per ACL entry
53        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 7;
54    } else if #[cfg(feature = "max-subjects-per-acl-6")] {
55        /// Max subjects per ACL entry
56        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 6;
57    } else if #[cfg(feature = "max-subjects-per-acl-5")] {
58        /// Max subjects per ACL entry
59        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 5;
60    } else if #[cfg(feature = "max-subjects-per-acl-4")] {
61        /// Max subjects per ACL entry
62        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 4;
63    } else if #[cfg(feature = "max-subjects-per-acl-3")] {
64        /// Max subjects per ACL entry
65        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 3;
66    } else if #[cfg(feature = "max-subjects-per-acl-2")] {
67        /// Max subjects per ACL entry
68        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 2;
69    } else if #[cfg(feature = "max-subjects-per-acl-1")] {
70        /// Max subjects per ACL entry
71        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 1;
72    } else {
73        /// Max subjects per ACL entry
74        pub const MAX_SUBJECTS_PER_ACL_ENTRY: usize = 4;
75    }
76}
77
78cfg_if! {
79    if #[cfg(feature = "max-targets-per-acl-32")] {
80        /// Max targets per ACL entry
81        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 32;
82    } else if #[cfg(feature = "max-targets-per-acl-16")] {
83        /// Max targets per ACL entry
84        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 16;
85    } else if #[cfg(feature = "max-targets-per-acl-8")] {
86        /// Max targets per ACL entry
87        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 8;
88    } else if #[cfg(feature = "max-targets-per-acl-7")] {
89        /// Max targets per ACL entry
90        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 7;
91    } else if #[cfg(feature = "max-targets-per-acl-6")] {
92        /// Max targets per ACL entry
93        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 6;
94    } else if #[cfg(feature = "max-targets-per-acl-5")] {
95        /// Max targets per ACL entry
96        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 5;
97    } else if #[cfg(feature = "max-targets-per-acl-4")] {
98        /// Max targets per ACL entry
99        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 4;
100    } else if #[cfg(feature = "max-targets-per-acl-3")] {
101        /// Max targets per ACL entry
102        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 3;
103    } else if #[cfg(feature = "max-targets-per-acl-2")] {
104        /// Max targets per ACL entry
105        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 2;
106    } else if #[cfg(feature = "max-targets-per-acl-1")] {
107        /// Max targets per ACL entry
108        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 1;
109    } else {
110        /// Max targets per ACL entry
111        pub const MAX_TARGETS_PER_ACL_ENTRY: usize = 3;
112    }
113}
114
115cfg_if! {
116    if #[cfg(feature = "max-acls-per-fabric-32")] {
117        /// Max ACL entries per fabric
118        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 32;
119    } else if #[cfg(feature = "max-acls-per-fabric-16")] {
120        /// Max ACL entries per fabric
121        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 16;
122    } else if #[cfg(feature = "max-acls-per-fabric-8")] {
123        /// Max ACL entries per fabric
124        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 8;
125    } else if #[cfg(feature = "max-acls-per-fabric-7")] {
126        /// Max ACL entries per fabric
127        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 7;
128    } else if #[cfg(feature = "max-acls-per-fabric-6")] {
129        /// Max ACL entries per fabric
130        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 6;
131    } else if #[cfg(feature = "max-acls-per-fabric-5")] {
132        /// Max ACL entries per fabric
133        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 5;
134    } else if #[cfg(feature = "max-acls-per-fabric-4")] {
135        /// Max ACL entries per fabric
136        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 4;
137    } else if #[cfg(feature = "max-acls-per-fabric-3")] {
138        /// Max ACL entries per fabric
139        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 3;
140    } else if #[cfg(feature = "max-acls-per-fabric-2")] {
141        /// Max ACL entries per fabric
142        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 2;
143    } else if #[cfg(feature = "max-acls-per-fabric-1")] {
144        /// Max ACL entries per fabric
145        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 1;
146    } else {
147        /// Max ACL entries per fabric
148        pub const MAX_ACL_ENTRIES_PER_FABRIC: usize = 4;
149    }
150}
151
152/// An enum modeling the different authentication modes
153// TODO: Check if this and the SessionMode can be combined into some generic data structure
154#[derive(FromPrimitive, Copy, Clone, PartialEq, Debug)]
155#[cfg_attr(feature = "defmt", derive(defmt::Format))]
156#[repr(u8)]
157pub enum AuthMode {
158    /// PASE authentication
159    Pase = AccessControlEntryAuthModeEnum::PASE as _,
160    /// CASE authentication
161    Case = AccessControlEntryAuthModeEnum::CASE as _,
162    /// Group authentication
163    Group = AccessControlEntryAuthModeEnum::Group as _,
164}
165
166impl FromTLV<'_> for AuthMode {
167    fn from_tlv(t: &TLVElement) -> Result<Self, Error>
168    where
169        Self: Sized,
170    {
171        Ok(AccessControlEntryAuthModeEnum::from_tlv(t)?.into())
172    }
173}
174
175impl ToTLV for AuthMode {
176    fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
177        AccessControlEntryAuthModeEnum::from(*self).to_tlv(tag, &mut tw)
178    }
179
180    fn tlv_iter(&self, tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
181        TLV::u8(tag, AccessControlEntryAuthModeEnum::from(*self) as _).into_tlv_iter()
182    }
183}
184
185impl From<AuthMode> for AccessControlEntryAuthModeEnum {
186    fn from(value: AuthMode) -> Self {
187        match value {
188            AuthMode::Pase => AccessControlEntryAuthModeEnum::PASE,
189            AuthMode::Case => AccessControlEntryAuthModeEnum::CASE,
190            AuthMode::Group => AccessControlEntryAuthModeEnum::Group,
191        }
192    }
193}
194
195impl From<AccessControlEntryAuthModeEnum> for AuthMode {
196    fn from(value: AccessControlEntryAuthModeEnum) -> Self {
197        match value {
198            AccessControlEntryAuthModeEnum::PASE => AuthMode::Pase,
199            AccessControlEntryAuthModeEnum::CASE => AuthMode::Case,
200            AccessControlEntryAuthModeEnum::Group => AuthMode::Group,
201        }
202    }
203}
204
205/// An accessor can have as many identities: one node id and up to MAX_CAT_IDS_PER_NOC
206const MAX_ACCESSOR_SUBJECTS: usize = 1 + MAX_CAT_IDS_PER_NOC;
207
208/// The CAT Prefix used in Subjects
209pub const NOC_CAT_SUBJECT_PREFIX: u64 = 0xFFFF_FFFD_0000_0000;
210pub const NOC_CAT_SUBJECT_MASK: u64 = 0xFFFF_FFFF_0000_0000;
211
212const NOC_CAT_ID_MASK: u64 = 0xFFFF_0000;
213const NOC_CAT_VERSION_MASK: u64 = 0xFFFF;
214
215/// The Node ID min range
216const NODE_ID_RANGE: RangeInclusive<u64> = 1..=0xFFFF_FFEF_FFFF_FFFF;
217
218/// Is this identifier a NOC CAT
219pub(crate) fn is_noc_cat(id: u64) -> bool {
220    ((id & NOC_CAT_SUBJECT_MASK) == NOC_CAT_SUBJECT_PREFIX)
221        && ((id & (NOC_CAT_ID_MASK | NOC_CAT_VERSION_MASK)) > 0)
222}
223
224/// Get the 16-bit NOC CAT id from the identifier
225fn get_noc_cat_id(id: u64) -> u64 {
226    (id & NOC_CAT_ID_MASK) >> 16
227}
228
229/// Get the 16-bit NOC CAT version from the identifier
230fn get_noc_cat_version(id: u64) -> u64 {
231    id & NOC_CAT_VERSION_MASK
232}
233
234/// Generate CAT that is embeddedable in the NoC
235/// This only generates the 32-bit CAT ID
236pub fn gen_noc_cat(id: u16, version: u16) -> u32 {
237    ((id as u32) << 16) | version as u32
238}
239
240/// Is this identifier a node id
241pub(crate) fn is_node(id: u64) -> bool {
242    NODE_ID_RANGE.contains(&id)
243}
244
245/// The Subjects that identify the Accessor
246pub struct AccessorSubjects([u64; MAX_ACCESSOR_SUBJECTS]);
247
248impl AccessorSubjects {
249    /// Create a new AccessorSubjects object
250    /// The first subject is the node id
251    pub fn new(id: u64) -> Self {
252        let mut a = Self(Default::default());
253        a.0[0] = id;
254        a
255    }
256
257    /// Add a CAT id to the AccessorSubjects
258    pub fn add_catid(&mut self, subject: u32) -> Result<(), Error> {
259        for (i, val) in self.0.iter().enumerate() {
260            if *val == 0 {
261                self.0[i] = NOC_CAT_SUBJECT_PREFIX | (subject as u64);
262                return Ok(());
263            }
264        }
265        Err(ErrorCode::ResourceExhausted.into())
266    }
267
268    /// Match the acl_subject with any of the current subjects
269    /// If a NOC CAT is specified, CAT aware matching is also performed
270    pub fn matches(&self, acl_subject: u64) -> bool {
271        for v in self.0.iter() {
272            if *v == 0 {
273                continue;
274            }
275
276            if *v == acl_subject {
277                return true;
278            } else {
279                // NOC CAT match
280                if is_noc_cat(*v)
281                    && is_noc_cat(acl_subject)
282                    && (get_noc_cat_id(*v) == get_noc_cat_id(acl_subject))
283                    && (get_noc_cat_version(*v) >= get_noc_cat_version(acl_subject))
284                {
285                    return true;
286                }
287            }
288        }
289
290        false
291    }
292}
293
294impl Display for AccessorSubjects {
295    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::result::Result<(), core::fmt::Error> {
296        write!(f, "[")?;
297        for i in self.0 {
298            if is_noc_cat(i) {
299                write!(f, "CAT({} - {})", get_noc_cat_id(i), get_noc_cat_version(i))?;
300            } else if i != 0 {
301                write!(f, "{}, ", i)?;
302            }
303        }
304        write!(f, "]")
305    }
306}
307
308#[cfg(feature = "defmt")]
309impl defmt::Format for AccessorSubjects {
310    fn format(&self, f: defmt::Formatter) {
311        defmt::write!(f, "[");
312        for i in self.0 {
313            if is_noc_cat(i) {
314                defmt::write!(f, "CAT({} - {})", get_noc_cat_id(i), get_noc_cat_version(i));
315            } else if i != 0 {
316                defmt::write!(f, "{}, ", i);
317            }
318        }
319        defmt::write!(f, "]")
320    }
321}
322
323/// The Accessor Object
324pub struct Accessor<'a> {
325    /// The fabric index of the accessor
326    pub(crate) fab_idx: u8,
327    /// Whether AUX ACL was enabled at the time this accessor was instantiated
328    aux_acl_enabled: bool,
329    /// Accessor's subject: could be node-id, NoC CAT, group id
330    subjects: AccessorSubjects,
331    /// The auth mode of this session. Might be `None` for plain-text sessions
332    auth_mode: Option<AuthMode>,
333    // Necessary so as to get access to the fabric manager to perform the access check in AccessReq::allow()
334    // as well as for a few other ACL related operations.
335    matter: &'a Matter<'a>,
336}
337
338impl<'a> Accessor<'a> {
339    /// Create a new Accessor object for the given session
340    pub fn for_session(session: &Session, matter: &'a Matter<'a>, aux_acl_enabled: bool) -> Self {
341        match session.get_session_mode() {
342            SessionMode::Case {
343                fab_idx, cat_ids, ..
344            } => {
345                let mut subject =
346                    AccessorSubjects::new(session.get_peer_node_id().unwrap_or_default());
347                for i in *cat_ids {
348                    if i != 0 {
349                        let _ = subject.add_catid(i);
350                    }
351                }
352                Accessor::new(
353                    fab_idx.get(),
354                    aux_acl_enabled,
355                    subject,
356                    Some(AuthMode::Case),
357                    matter,
358                )
359            }
360            SessionMode::Pase { fab_idx } => Accessor::new(
361                *fab_idx,
362                aux_acl_enabled,
363                AccessorSubjects::new(1),
364                Some(AuthMode::Pase),
365                matter,
366            ),
367            SessionMode::Group { fab_idx, group_id } => Accessor::new(
368                fab_idx.get(),
369                aux_acl_enabled,
370                AccessorSubjects::new(*group_id as u64),
371                Some(AuthMode::Group),
372                matter,
373            ),
374            SessionMode::PlainText => {
375                Accessor::new(0, aux_acl_enabled, AccessorSubjects::new(1), None, matter)
376            }
377        }
378    }
379
380    /// Create a new Accessor object
381    ///
382    /// # Arguments
383    /// - `fab_idx`: The fabric index of the accessor (0 means no fabric index)
384    /// - `aux_acl_enabled`: Whether AUX ACL was enabled at the time this accessor was instantiated
385    /// - `subjects`: The subjects of the accessor
386    /// - `auth_mode`: The auth mode of the accessor
387    /// - `matter`: The Matter instance
388    pub const fn new(
389        fab_idx: u8,
390        aux_acl_enabled: bool,
391        subjects: AccessorSubjects,
392        auth_mode: Option<AuthMode>,
393        matter: &'a Matter<'a>,
394    ) -> Self {
395        Self {
396            fab_idx,
397            aux_acl_enabled,
398            subjects,
399            auth_mode,
400            matter,
401        }
402    }
403
404    pub fn fab_idx(&self) -> Result<NonZeroU8, Error> {
405        NonZeroU8::new(self.fab_idx).ok_or(ErrorCode::UnsupportedAccess.into())
406    }
407
408    /// Return whether AUX ACL was enabled at the time this accessor was instantiated
409    pub const fn aux_acl_enabled(&self) -> bool {
410        self.aux_acl_enabled
411    }
412
413    /// Return the subjects of the accessor
414    pub const fn subjects(&self) -> &AccessorSubjects {
415        &self.subjects
416    }
417
418    /// Return the auth mode of the accessor
419    pub const fn auth_mode(&self) -> Option<AuthMode> {
420        self.auth_mode
421    }
422
423    /// Return whether the given endpoint is accessible for this accessor.
424    ///
425    /// For group sessions, only endpoints that are members of the group are accessible.
426    /// For all other session types, all endpoints are accessible.
427    pub fn is_endpoint_accessible(&self, endpoint_id: EndptId) -> bool {
428        if self.auth_mode != Some(AuthMode::Group) {
429            return true;
430        }
431
432        // A group session reaches an endpoint only if that endpoint is a member
433        // of the session's group.
434        #[cfg(feature = "groups")]
435        {
436            let group_id = self.subjects.0[0] as u16;
437
438            let Some(fab_idx) = core::num::NonZeroU8::new(self.fab_idx) else {
439                return false;
440            };
441
442            self.matter.with_state(|state| {
443                let Some(fabric) = state.fabrics.get(fab_idx) else {
444                    return false;
445                };
446
447                fabric
448                    .groups()
449                    .get(group_id)
450                    .is_some_and(|e| e.endpoints.contains(&endpoint_id))
451            })
452        }
453
454        // Without multicast group support there are no group sessions, so
455        // `auth_mode` above is never `Group` and this point is unreachable. Deny
456        // anyway (fail closed): if a `Group` auth mode ever did reach here, it must
457        // not silently grant access to an endpoint.
458        #[cfg(not(feature = "groups"))]
459        {
460            let _ = endpoint_id;
461            false
462        }
463    }
464
465    /// Return the Operational Node ID of the accessor, if any
466    pub fn node_id(&self) -> Option<NodeId> {
467        let fab_idx = NonZeroU8::new(self.fab_idx)?;
468
469        self.matter
470            .with_state(|state| state.fabrics.get(fab_idx).map(|fabric| fabric.node_id()))
471    }
472
473    /// Return the peer node ID (admin node ID) for CASE sessions, or None for PASE/other sessions.
474    pub fn peer_node_id(&self) -> Option<u64> {
475        if matches!(self.auth_mode, Some(AuthMode::Case)) {
476            let id = self.subjects.0[0];
477            if is_node(id) {
478                Some(id)
479            } else {
480                None
481            }
482        } else {
483            None
484        }
485    }
486}
487
488/// Access Descriptor Object
489#[derive(Debug)]
490#[cfg_attr(feature = "defmt", derive(defmt::Format))]
491pub struct AccessDesc<'a> {
492    /// The object to be acted upon
493    path: GenericPath,
494    /// The target permissions
495    target_perms: Option<Access>,
496    // The operation being done
497    operation: Access,
498    /// The device types of the endpoint hosting `path`. Used by ACL `Target`
499    /// entries that filter by `DeviceType` (Matter Core spec).
500    /// Empty when the access target's endpoint is unknown / not yet expanded.
501    device_types: &'a [DeviceType],
502}
503
504/// Access Request Object
505pub struct AccessReq<'a> {
506    /// The accessor requesting access
507    accessor: &'a Accessor<'a>,
508    /// The object being accessed
509    object: AccessDesc<'a>,
510}
511
512impl<'a> AccessReq<'a> {
513    /// Create an access request object.
514    ///
515    /// An access request specifies the _accessor_ attempting to access _path_
516    /// with _operation_. `device_types` lists the device types declared by
517    /// the endpoint that hosts `path`; pass an empty slice when this is not
518    /// applicable (e.g. for unit tests that don't exercise `DeviceType` ACL
519    /// targets).
520    pub const fn new(
521        accessor: &'a Accessor<'a>,
522        path: GenericPath,
523        operation: Access,
524        device_types: &'a [DeviceType],
525    ) -> Self {
526        AccessReq {
527            accessor,
528            object: AccessDesc {
529                path,
530                target_perms: None,
531                operation,
532                device_types,
533            },
534        }
535    }
536
537    /// Return the accessor of the request
538    pub fn accessor(&self) -> &Accessor<'_> {
539        self.accessor
540    }
541
542    /// Return the operation of the request
543    pub fn operation(&self) -> Access {
544        self.object.operation
545    }
546
547    /// Add target's permissions to the request
548    ///
549    /// The permissions that are associated with the target (identified by the
550    /// path in the AccessReq) are added to the request
551    pub fn set_target_perms(&mut self, perms: Access) {
552        self.object.target_perms = Some(perms);
553    }
554
555    /// Check if access is allowed
556    ///
557    /// This checks all the ACL list to identify if any of the ACLs provides the
558    /// _accessor_ the necessary privileges to access the target as per its
559    /// permissions
560    pub fn allow(&self) -> bool {
561        self.accessor.matter.with_state(|state| {
562            let allow = state.fabrics.allow(self, self.accessor.aux_acl_enabled());
563
564            #[cfg(feature = "groups")]
565            let allow = allow || self.allow_groupcast_auxiliary(&state.fabrics);
566
567            allow
568        })
569    }
570
571    /// Check whether access is granted by an auxiliary ACL entry synthesized
572    /// from the Groupcast group table: a Group-auth accessor whose group has
573    /// `HasAuxiliaryACL` set is granted the `Operate` privilege on the
574    /// group's endpoints (see the Groupcast Auxiliary ACL Handling section of
575    /// the Matter Core spec).
576    #[cfg(feature = "groups")]
577    fn allow_groupcast_auxiliary(&self, fabrics: &crate::fabric::Fabrics) -> bool {
578        if !self.accessor.aux_acl_enabled() {
579            return false;
580        }
581
582        if self.accessor.auth_mode != Some(AuthMode::Group) {
583            return false;
584        }
585
586        let Ok(fab_idx) = self.accessor.fab_idx() else {
587            return false;
588        };
589
590        let Some(fabric) = fabrics.get(fab_idx) else {
591            return false;
592        };
593
594        // Synthesized entries always carry concrete endpoint targets
595        let Some(endpoint) = self.object.path.endpoint else {
596            return false;
597        };
598
599        let granted = fabric.groups().iter().any(|entry| {
600            entry.has_aux_acl()
601                && entry.endpoints.contains(&endpoint)
602                && self.accessor.subjects.matches(entry.group_id as u64)
603        });
604
605        granted
606            && self
607                .object
608                .target_perms
609                .is_some_and(|access| access.is_ok(self.object.operation, Privilege::OPERATE))
610    }
611}
612
613/// The target object
614#[derive(FromTLV, ToTLV, Clone, Debug, PartialEq)]
615#[cfg_attr(feature = "defmt", derive(defmt::Format))]
616pub struct Target {
617    pub cluster: Option<ClusterId>,
618    pub endpoint: Option<EndptId>,
619    pub device_type: Option<u32>,
620}
621
622impl Target {
623    /// Create a new target object
624    pub const fn new(
625        endpoint: Option<EndptId>,
626        cluster: Option<ClusterId>,
627        device_type: Option<u32>,
628    ) -> Self {
629        Self {
630            cluster,
631            endpoint,
632            device_type,
633        }
634    }
635}
636
637/// The ACL entry object
638#[derive(ToTLV, FromTLV, Clone, Debug, PartialEq)]
639#[cfg_attr(feature = "defmt", derive(defmt::Format))]
640#[tlvargs(start = 1)]
641pub struct AclEntry {
642    /// The privilege of the entry
643    privilege: Privilege,
644    /// The auth mode of the entry
645    auth_mode: AuthMode,
646    /// The subjects of the entry
647    subjects: Nullable<Vec<u64, MAX_SUBJECTS_PER_ACL_ENTRY>>,
648    /// The targets of the entry
649    targets: Nullable<Vec<Target, MAX_TARGETS_PER_ACL_ENTRY>>,
650    // TODO: Figure out what this is, document and use
651    auxiliary_type: Option<AccessControlAuxiliaryTypeEnum>,
652    // Note that this field will always be `Some(NN)` when the entry is persisted in storage,
653    // however, it will be `None` when the entry is coming from the other peer.
654    #[tagval(crate::im::encoding::FABRIC_INDEX_TAG)]
655    pub fab_idx: Option<NonZeroU8>,
656}
657
658impl AclEntry {
659    /// Create a new ACL entry object
660    pub const fn new(
661        fab_idx: Option<NonZeroU8>,
662        privilege: Privilege,
663        auth_mode: AuthMode,
664    ) -> Self {
665        Self {
666            fab_idx,
667            privilege,
668            auth_mode,
669            subjects: Nullable::none(),
670            targets: Nullable::none(),
671            auxiliary_type: None,
672        }
673    }
674
675    /// Return an initializer for an ACL entry object
676    /// using the given fabric index, privilege and auth mode as input
677    pub fn init(
678        fab_idx: Option<NonZeroU8>,
679        privilege: Privilege,
680        auth_mode: AuthMode,
681    ) -> impl Init<Self> {
682        init!(Self {
683            fab_idx,
684            privilege,
685            auth_mode,
686            subjects <- Nullable::init_none(),
687            targets <- Nullable::init_none(),
688            auxiliary_type: None,
689        })
690    }
691
692    /// Return an initializer for an ACL entry object
693    /// using the given fabric index and TLV entry struct as input
694    pub fn init_with<'a>(
695        fab_idx: NonZeroU8,
696        entry: &'a AccessControlEntryStruct<'a>,
697    ) -> impl Init<Self, Error> + 'a {
698        Self::init(Some(fab_idx), Privilege::empty(), AuthMode::Pase)
699            .into_fallible()
700            .chain(|e| {
701                let auth_mode = entry.auth_mode().map_err(|_| ErrorCode::ConstraintError)?.ok_or(ErrorCode::ConstraintError)?;
702                let privilege = entry.privilege().map_err(|_| ErrorCode::ConstraintError)?.ok_or(ErrorCode::ConstraintError)?;
703                let subjects = entry.subjects().map_err(|_| ErrorCode::ConstraintError)?.ok_or(ErrorCode::ConstraintError)?;
704                let targets = entry.targets().map_err(|_| ErrorCode::ConstraintError)?.ok_or(ErrorCode::ConstraintError)?;
705                let auxiliary_type = entry.auxiliary_type().map_err(|_| ErrorCode::ConstraintError)?;
706
707                // Per the Matter Core spec, the `AuxiliaryType` field SHALL
708                // NOT be present in entries in the (writable) `ACL`
709                // attribute - it is reserved for the server-generated
710                // entries in `AuxiliaryACL`.
711                if auxiliary_type.is_some() {
712                    Err(ErrorCode::ConstraintError)?;
713                }
714
715                if
716                    // As per spec, PASE auth mode is reserved for future use
717                    matches!(auth_mode, AccessControlEntryAuthModeEnum::PASE)
718                    // As per spec, Group auth mode cannot have Admin privilege
719                    || matches!(auth_mode, AccessControlEntryAuthModeEnum::Group) && matches!(privilege, AccessControlEntryPrivilegeEnum::Administer)
720                {
721                    Err(ErrorCode::ConstraintError)?;
722                }
723
724                e.privilege = privilege.into();
725                e.auth_mode = auth_mode.into();
726
727                // Start with null subjects and targets
728                // so that we can keep those to null if we receive empty subjects' array or empty targets' array
729                // This is what the YAML tests expect
730                e.subjects.clear();
731                e.targets.clear();
732
733                if let Some(subjects) = subjects.into_option() {
734                    for subject in subjects {
735                        if e.subjects.is_none() {
736                            // Initialize our subjects to non-null lazily, only if we have at least one incoming subject
737                            // This ensures that if the incoming subjects is empty, we keep our subjects as null
738                            // which is what the YAML tests expect, even if we internally treat null and empty subjects the same way
739                            e.subjects.reinit(Nullable::init_some(Vec::init()));
740                        }
741
742                        let esubjects = unwrap!(e.subjects.as_opt_mut());
743
744                        let subject = subject?;
745
746                        if matches!(auth_mode, AccessControlEntryAuthModeEnum::CASE) && !is_node(subject) && !is_noc_cat(subject) {
747                            // As per spec, CASE auth mode only allows node ids and NOC CATs as subjects
748                            Err(ErrorCode::ConstraintError)?;
749                        }
750
751                        if matches!(auth_mode, AccessControlEntryAuthModeEnum::Group) {
752                            // Per Matter Core spec: for Group auth mode, the
753                            // subject SHALL be a valid 16-bit Group ID. Group ID 0 is reserved
754                            // and MUST NOT be used; values larger than `u16::MAX` are also invalid.
755                            if subject == 0 || subject > u16::MAX as u64 {
756                                Err(ErrorCode::ConstraintError)?;
757                            }
758                        }
759
760                        // As per spec, on too many subjects we should return a FAILURE status code
761                        // `ErrorCode::BufferTooSmall` translates to a generic FAILURE status code
762                        esubjects
763                            .push(subject)
764                            .map_err(|_| ErrorCode::BufferTooSmall)?;
765                    }
766                }
767
768                if let Some(targets) = targets.into_option() {
769                    for target in targets {
770                        if e.targets.is_none() {
771                            // Initialize our targets to non-null lazily, only if we have at least one incoming target
772                            // This ensures that if the incoming targets is empty, we keep our targets as null
773                            // which is what the YAML tests expect, even if we internally treat null and empty targets the same way
774                            e.targets.reinit(Nullable::init_some(Vec::init()));
775                        }
776
777                        let etargets = unwrap!(e.targets.as_opt_mut());
778
779                        let target = target?;
780
781                        // Matter Core spec (AccessControlTargetStruct):
782                        // - At least one of cluster, endpoint or deviceType SHALL be present.
783                        // - If endpoint is present, deviceType SHALL NOT be present (and vice
784                        //   versa). cluster may be combined with either endpoint or deviceType.
785                        let has_endpoint = target.endpoint()?.is_some();
786                        let has_cluster = target.cluster()?.is_some();
787                        let has_device_type = target.device_type()?.is_some();
788
789                        if (!has_endpoint && !has_cluster && !has_device_type)
790                            || (has_endpoint && has_device_type)
791                        {
792                            Err(ErrorCode::ConstraintError)?;
793                        }
794
795                        // As per spec, on too many targets we should return a FAILURE status code
796                        // `ErrorCode::BufferTooSmall` translates to a generic FAILURE status code
797                        etargets
798                            .push(Target::new(
799                                target.endpoint()?.into_option(),
800                                target.cluster()?.into_option(),
801                                target.device_type()?.into_option(),
802                            ))
803                            .map_err(|_| ErrorCode::BufferTooSmall)?;
804                    }
805                }
806
807                Ok(())
808            })
809    }
810
811    /// Return the data of the ACL entry object
812    /// into the provided TLV builder
813    pub fn read_into<P: TLVBuilderParent>(
814        &self,
815        accessing_fab_idx: u8,
816        fab_idx: Option<u8>,
817        builder: AccessControlEntryStructBuilder<P>,
818    ) -> Result<P, Error> {
819        let same_fab_idx = Some(accessing_fab_idx) == fab_idx;
820
821        builder
822            .privilege(same_fab_idx.then(|| self.privilege.into()))?
823            .auth_mode(same_fab_idx.then(|| self.auth_mode.into()))?
824            .subjects()?
825            .with_some_if(same_fab_idx, |builder| {
826                builder.with_non_null(self.subjects(), |subjects, mut builder| {
827                    for subject in *subjects {
828                        builder = builder.push(subject)?;
829                    }
830
831                    builder.end()
832                })
833            })?
834            .targets()?
835            .with_some_if(same_fab_idx, |builder| {
836                builder.with_non_null(self.targets(), |targets, mut builder| {
837                    for target in *targets {
838                        builder = builder
839                            .push()?
840                            .cluster(Nullable::new(target.cluster))?
841                            .endpoint(Nullable::new(target.endpoint))?
842                            .device_type(Nullable::new(target.device_type))?
843                            .end()?;
844                    }
845
846                    builder.end()
847                })
848            })?
849            .auxiliary_type(self.auxiliary_type())?
850            .fabric_index(fab_idx)?
851            .end()
852    }
853
854    /// Normalize the ACL entry by converting non-null but empty
855    /// subjects/targets to null, as the spec and YAML tests expect
856    pub fn normalize(&mut self) {
857        if self
858            .subjects
859            .as_opt_ref()
860            .map(|subjects| subjects.is_empty())
861            .unwrap_or(false)
862        {
863            self.subjects.clear();
864        }
865
866        if self
867            .targets
868            .as_opt_ref()
869            .map(|targets| targets.is_empty())
870            .unwrap_or(false)
871        {
872            self.targets.clear();
873        }
874    }
875
876    /// Return the auth mode of the ACL entry
877    pub fn auth_mode(&self) -> AuthMode {
878        self.auth_mode
879    }
880
881    /// Return the subjects of the ACL entry
882    pub fn subjects(&self) -> Nullable<&[u64]> {
883        Nullable::new(self.subjects.as_opt_ref().map(|v| v.as_slice()))
884    }
885
886    /// Return the targets of the ACL entry
887    pub fn targets(&self) -> Nullable<&[Target]> {
888        Nullable::new(self.targets.as_opt_ref().map(|v| v.as_slice()))
889    }
890
891    pub fn auxiliary_type(&self) -> Option<AccessControlAuxiliaryTypeEnum> {
892        self.auxiliary_type
893    }
894
895    /// Check if the ACL entry allows access to the given accessor and object
896    ///
897    /// `aux_acl_enabled` conveys whether the node advertises the Access Control
898    /// cluster's `AUXILIARY` feature, which changes how wildcard-target
899    /// Group-auth entries are evaluated - see [`Self::match_access_desc`].
900    pub fn allow(&self, req: &AccessReq, aux_acl_enabled: bool) -> bool {
901        self.match_accessor(req.accessor) && self.match_access_desc(&req.object, aux_acl_enabled)
902    }
903
904    /// Add a subject to the ACL entry
905    pub fn add_subject(&mut self, subject: u64) -> Result<(), Error> {
906        if self.subjects.is_none() {
907            self.subjects.reinit(Nullable::init_some(Vec::init()));
908        }
909
910        unwrap!(self.subjects.as_opt_mut())
911            .push(subject)
912            .map_err(|_| ErrorCode::ResourceExhausted.into())
913    }
914
915    /// Add a CAT id to the ACL entry
916    pub fn add_subject_catid(&mut self, cat_id: u32) -> Result<(), Error> {
917        self.add_subject(NOC_CAT_SUBJECT_PREFIX | cat_id as u64)
918    }
919
920    /// Add a target to the ACL entry
921    pub fn add_target(&mut self, target: Target) -> Result<(), Error> {
922        if self.targets.is_none() {
923            self.targets.reinit(Nullable::init_some(Vec::init()));
924        }
925
926        unwrap!(self.targets.as_opt_mut())
927            .push(target)
928            .map_err(|_| ErrorCode::ResourceExhausted.into())
929    }
930
931    fn match_accessor(&self, accessor: &Accessor) -> bool {
932        if Some(self.auth_mode) != accessor.auth_mode {
933            return false;
934        }
935
936        let allow = self.subjects().as_opt_ref().is_none_or(|subjects| {
937            // Subjects array null or empty implies allow for all subjects
938            // Otherwise, check if the accessor's subject matches any of the ACL entry's subjects
939            subjects.is_empty() || subjects.iter().any(|s| accessor.subjects.matches(*s))
940        });
941
942        // true if both are true
943        allow
944            && self
945                .fab_idx
946                .map(|fab_idx| fab_idx.get() == accessor.fab_idx)
947                .unwrap_or(false)
948    }
949
950    fn match_access_desc(&self, object: &AccessDesc, aux_acl_enabled: bool) -> bool {
951        // Per the Matter Core spec, when the Access Control cluster's
952        // `AUXILIARY` feature is advertised, an empty (wildcard) targets
953        // list on a Group-auth entry grants access to all endpoints
954        // *except* the root endpoint; without the feature it covers the
955        // whole node. (Explicitly-listed targets are unaffected.)
956        if aux_acl_enabled
957            && matches!(self.auth_mode, AuthMode::Group)
958            && object.path.endpoint == Some(crate::dm::endpoints::ROOT_ENDPOINT_ID)
959            && self
960                .targets
961                .as_opt_ref()
962                .is_none_or(|targets| targets.is_empty())
963        {
964            return false;
965        }
966
967        let allow = self.targets.as_opt_ref().is_none_or(|targets| {
968            // Targets array null or empty implies allow for all targets
969            // Otherwise, check if the target matches any of the ACL entry's targets
970            targets.is_empty()
971                || targets.iter().any(|t| {
972                    let endpoint_match = t.endpoint.is_none() || t.endpoint == object.path.endpoint;
973                    let cluster_match = t.cluster.is_none() || t.cluster == object.path.cluster;
974                    // When `Target.device_type` is set, the access target's endpoint
975                    // must declare a matching device type in its `DeviceTypeList`
976                    // (Matter Core spec).
977                    let device_type_match = match t.device_type {
978                        Some(dt) => object
979                            .device_types
980                            .iter()
981                            .any(|endpoint_dt| endpoint_dt.dtype as u32 == dt),
982                        None => true,
983                    };
984                    endpoint_match && cluster_match && device_type_match
985                })
986        });
987
988        if allow {
989            // Check that the object's access allows this operation with this privilege
990            if let Some(access) = object.target_perms {
991                access.is_ok(object.operation, self.privilege)
992            } else {
993                false
994            }
995        } else {
996            false
997        }
998    }
999}
1000
1001#[cfg(test)]
1002#[allow(clippy::bool_assert_comparison)]
1003pub(crate) mod tests {
1004    use core::num::NonZeroU8;
1005
1006    use crate::acl::{gen_noc_cat, AccessorSubjects};
1007    use crate::dm::{Access, Privilege};
1008    use crate::error::Error;
1009    use crate::im::GenericPath;
1010    use crate::test::test_matter;
1011    use crate::Matter;
1012
1013    use super::{AccessReq, Accessor, AclEntry, AuthMode, Target};
1014
1015    pub(crate) const FAB_1: NonZeroU8 = match NonZeroU8::new(1) {
1016        Some(f) => f,
1017        None => ::core::unreachable!(),
1018    };
1019
1020    pub(crate) const FAB_2: NonZeroU8 = match NonZeroU8::new(2) {
1021        Some(f) => f,
1022        None => ::core::unreachable!(),
1023    };
1024
1025    fn add_fabric(matter: &Matter<'_>) {
1026        matter.with_state(|state| {
1027            // Add fabric with ID 1
1028            state.fabrics.add_with_post_init(|_| Ok(())).unwrap();
1029        })
1030    }
1031
1032    fn add_acl(matter: &Matter<'_>, fab_idx: NonZeroU8, entry: AclEntry) -> Result<usize, Error> {
1033        matter.with_state(|state| state.fabrics.fabric_mut(fab_idx)?.acl_add(entry))
1034    }
1035
1036    fn remove_all_acl(matter: &Matter<'_>, fab_idx: NonZeroU8) {
1037        matter.with_state(|state| state.fabrics.fabric_mut(fab_idx).unwrap().acl_remove_all())
1038    }
1039
1040    #[test]
1041    fn test_basic_empty_subject_target() {
1042        let matter = test_matter();
1043        let accessor = Accessor::new(
1044            0,
1045            false,
1046            AccessorSubjects::new(112233),
1047            Some(AuthMode::Pase),
1048            &matter,
1049        );
1050        let path = GenericPath::new(Some(1), Some(1234), None);
1051        let mut req_pase = AccessReq::new(&accessor, path, Access::READ, &[]);
1052        req_pase.set_target_perms(Access::RWVA);
1053
1054        // Always allow for PASE sessions
1055        assert!(req_pase.allow());
1056
1057        let accessor = Accessor::new(
1058            2,
1059            false,
1060            AccessorSubjects::new(112233),
1061            Some(AuthMode::Case),
1062            &matter,
1063        );
1064        let path = GenericPath::new(Some(1), Some(1234), None);
1065        let mut req = AccessReq::new(&accessor, path, Access::READ, &[]);
1066        req.set_target_perms(Access::RWVA);
1067
1068        // Default deny for CASE
1069        assert_eq!(req.allow(), false);
1070
1071        // Add fabric with ID 1
1072        add_fabric(&matter);
1073
1074        // Deny adding invalid auth mode (PASE is reserved for future)
1075        let new = AclEntry::new(None, Privilege::VIEW, AuthMode::Pase);
1076        assert!(add_acl(&matter, FAB_1, new).is_err());
1077
1078        // Deny for fab idx mismatch
1079        let new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1080        assert_eq!(add_acl(&matter, FAB_1, new).unwrap(), 0);
1081        assert_eq!(req.allow(), false);
1082
1083        // Always allow for PASE sessions
1084        assert!(req_pase.allow());
1085
1086        // Add fabric with ID 2
1087        add_fabric(&matter);
1088
1089        // Allow
1090        let new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1091        assert_eq!(add_acl(&matter, FAB_2, new).unwrap(), 0);
1092        assert_eq!(req.allow(), true);
1093    }
1094
1095    #[test]
1096    fn test_subject() {
1097        let matter = test_matter();
1098
1099        // Add fabric with ID 1
1100        add_fabric(&matter);
1101
1102        let accessor = Accessor::new(
1103            1,
1104            false,
1105            AccessorSubjects::new(112233),
1106            Some(AuthMode::Case),
1107            &matter,
1108        );
1109        let path = GenericPath::new(Some(1), Some(1234), None);
1110        let mut req = AccessReq::new(&accessor, path, Access::READ, &[]);
1111        req.set_target_perms(Access::RWVA);
1112
1113        // Deny for subject mismatch
1114        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1115        new.add_subject(112232).unwrap();
1116        assert_eq!(add_acl(&matter, FAB_1, new).unwrap(), 0);
1117        assert_eq!(req.allow(), false);
1118
1119        // Allow for subject match - target is wildcard
1120        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1121        new.add_subject(112233).unwrap();
1122        assert_eq!(add_acl(&matter, FAB_1, new).unwrap(), 1);
1123        assert_eq!(req.allow(), true);
1124    }
1125
1126    #[test]
1127    fn test_cat() {
1128        let matter = test_matter();
1129
1130        // Add fabric with ID 1
1131        add_fabric(&matter);
1132
1133        let allow_cat = 0xABCD;
1134        let disallow_cat = 0xCAFE;
1135        let v2 = 2;
1136        let v3 = 3;
1137        // Accessor has nodeif and CAT 0xABCD_0002
1138        let mut subjects = AccessorSubjects::new(112233);
1139        subjects.add_catid(gen_noc_cat(allow_cat, v2)).unwrap();
1140
1141        let accessor = Accessor::new(1, false, subjects, Some(AuthMode::Case), &matter);
1142        let path = GenericPath::new(Some(1), Some(1234), None);
1143        let mut req = AccessReq::new(&accessor, path, Access::READ, &[]);
1144        req.set_target_perms(Access::RWVA);
1145
1146        // Deny for CAT id mismatch
1147        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1148        new.add_subject_catid(gen_noc_cat(disallow_cat, v2))
1149            .unwrap();
1150        add_acl(&matter, FAB_1, new).unwrap();
1151        assert_eq!(req.allow(), false);
1152
1153        // Deny of CAT version mismatch
1154        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1155        new.add_subject_catid(gen_noc_cat(allow_cat, v3)).unwrap();
1156        add_acl(&matter, FAB_1, new).unwrap();
1157        assert_eq!(req.allow(), false);
1158
1159        // Allow for CAT match
1160        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1161        new.add_subject_catid(gen_noc_cat(allow_cat, v2)).unwrap();
1162        add_acl(&matter, FAB_1, new).unwrap();
1163        assert_eq!(req.allow(), true);
1164    }
1165
1166    #[test]
1167    fn test_cat_version() {
1168        let matter = test_matter();
1169
1170        // Add fabric with ID 1
1171        add_fabric(&matter);
1172
1173        let allow_cat = 0xABCD;
1174        let disallow_cat = 0xCAFE;
1175        let v2 = 2;
1176        let v3 = 3;
1177        // Accessor has nodeif and CAT 0xABCD_0003
1178        let mut subjects = AccessorSubjects::new(112233);
1179        subjects.add_catid(gen_noc_cat(allow_cat, v3)).unwrap();
1180
1181        let accessor = Accessor::new(1, false, subjects, Some(AuthMode::Case), &matter);
1182        let path = GenericPath::new(Some(1), Some(1234), None);
1183        let mut req = AccessReq::new(&accessor, path, Access::READ, &[]);
1184        req.set_target_perms(Access::RWVA);
1185
1186        // Deny for CAT id mismatch
1187        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1188        new.add_subject_catid(gen_noc_cat(disallow_cat, v2))
1189            .unwrap();
1190        add_acl(&matter, FAB_1, new).unwrap();
1191        assert_eq!(req.allow(), false);
1192
1193        // Allow for CAT match and version more than ACL version
1194        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1195        new.add_subject_catid(gen_noc_cat(allow_cat, v2)).unwrap();
1196        add_acl(&matter, FAB_1, new).unwrap();
1197        assert_eq!(req.allow(), true);
1198    }
1199
1200    #[test]
1201    fn test_target() {
1202        let matter = test_matter();
1203
1204        // Add fabric with ID 1
1205        add_fabric(&matter);
1206
1207        let accessor = Accessor::new(
1208            1,
1209            false,
1210            AccessorSubjects::new(112233),
1211            Some(AuthMode::Case),
1212            &matter,
1213        );
1214        let path = GenericPath::new(Some(1), Some(1234), None);
1215        let mut req = AccessReq::new(&accessor, path, Access::READ, &[]);
1216        req.set_target_perms(Access::RWVA);
1217
1218        // Deny for target mismatch
1219        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1220        new.add_target(Target {
1221            cluster: Some(2),
1222            endpoint: Some(4567),
1223            device_type: None,
1224        })
1225        .unwrap();
1226        add_acl(&matter, FAB_1, new).unwrap();
1227        assert_eq!(req.allow(), false);
1228
1229        // Allow for cluster match - subject wildcard
1230        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1231        new.add_target(Target {
1232            cluster: Some(1234),
1233            endpoint: None,
1234            device_type: None,
1235        })
1236        .unwrap();
1237        add_acl(&matter, FAB_1, new).unwrap();
1238        assert_eq!(req.allow(), true);
1239
1240        // Clean state
1241        remove_all_acl(&matter, FAB_1);
1242
1243        // Allow for endpoint match - subject wildcard
1244        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1245        new.add_target(Target {
1246            cluster: None,
1247            endpoint: Some(1),
1248            device_type: None,
1249        })
1250        .unwrap();
1251        add_acl(&matter, FAB_1, new).unwrap();
1252        assert_eq!(req.allow(), true);
1253
1254        // Clean state
1255        remove_all_acl(&matter, FAB_1);
1256
1257        // Allow for exact match
1258        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1259        new.add_target(Target {
1260            cluster: Some(1234),
1261            endpoint: Some(1),
1262            device_type: None,
1263        })
1264        .unwrap();
1265        new.add_subject(112233).unwrap();
1266        add_acl(&matter, FAB_1, new).unwrap();
1267        assert_eq!(req.allow(), true);
1268    }
1269
1270    #[test]
1271    fn test_privilege() {
1272        let matter = test_matter();
1273
1274        // Add fabric with ID 1
1275        add_fabric(&matter);
1276
1277        let accessor = Accessor::new(
1278            1,
1279            false,
1280            AccessorSubjects::new(112233),
1281            Some(AuthMode::Case),
1282            &matter,
1283        );
1284        let path = GenericPath::new(Some(1), Some(1234), None);
1285
1286        // Create an Exact Match ACL with View privilege
1287        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1288        new.add_target(Target {
1289            cluster: Some(1234),
1290            endpoint: Some(1),
1291            device_type: None,
1292        })
1293        .unwrap();
1294        new.add_subject(112233).unwrap();
1295        add_acl(&matter, FAB_1, new).unwrap();
1296
1297        // Write on an RWVA without admin access - deny
1298        let mut req = AccessReq::new(&accessor, path.clone(), Access::WRITE, &[]);
1299        req.set_target_perms(Access::RWVA);
1300        assert_eq!(req.allow(), false);
1301
1302        // Create an Exact Match ACL with Admin privilege
1303        let mut new = AclEntry::new(None, Privilege::ADMIN, AuthMode::Case);
1304        new.add_target(Target {
1305            cluster: Some(1234),
1306            endpoint: Some(1),
1307            device_type: None,
1308        })
1309        .unwrap();
1310        new.add_subject(112233).unwrap();
1311        add_acl(&matter, FAB_1, new).unwrap();
1312
1313        // Write on an RWVA with admin access - allow
1314        let mut req = AccessReq::new(&accessor, path, Access::WRITE, &[]);
1315        req.set_target_perms(Access::RWVA);
1316        assert_eq!(req.allow(), true);
1317    }
1318
1319    #[test]
1320    fn test_delete_for_fabric() {
1321        let matter = test_matter();
1322
1323        // Add fabric with ID 1
1324        add_fabric(&matter);
1325
1326        // Add fabric with ID 2
1327        add_fabric(&matter);
1328
1329        let path = GenericPath::new(Some(1), Some(1234), None);
1330        let accessor2 = Accessor::new(
1331            1,
1332            false,
1333            AccessorSubjects::new(112233),
1334            Some(AuthMode::Case),
1335            &matter,
1336        );
1337        let mut req1 = AccessReq::new(&accessor2, path.clone(), Access::READ, &[]);
1338        req1.set_target_perms(Access::RWVA);
1339        let accessor3 = Accessor::new(
1340            2,
1341            false,
1342            AccessorSubjects::new(112233),
1343            Some(AuthMode::Case),
1344            &matter,
1345        );
1346        let mut req2 = AccessReq::new(&accessor3, path, Access::READ, &[]);
1347        req2.set_target_perms(Access::RWVA);
1348
1349        // Allow for subject match - target is wildcard - Fabric idx 2
1350        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1351        new.add_subject(112233).unwrap();
1352        assert_eq!(add_acl(&matter, FAB_1, new).unwrap(), 0);
1353
1354        // Allow for subject match - target is wildcard - Fabric idx 3
1355        let mut new = AclEntry::new(None, Privilege::VIEW, AuthMode::Case);
1356        new.add_subject(112233).unwrap();
1357        assert_eq!(add_acl(&matter, FAB_2, new).unwrap(), 0);
1358
1359        // Req for Fabric idx 1 gets denied, and that for Fabric idx 2 is allowed
1360        assert_eq!(req1.allow(), true);
1361        assert_eq!(req2.allow(), true);
1362        remove_all_acl(&matter, FAB_1);
1363        assert_eq!(req1.allow(), false);
1364        assert_eq!(req2.allow(), true);
1365    }
1366
1367    /// With the `AUXILIARY` feature advertised, a wildcard-target Group-auth
1368    /// entry no longer covers the root endpoint (but explicit targets and
1369    /// other endpoints are unaffected).
1370    #[test]
1371    fn test_aux_wildcard_group_excludes_root_endpoint() {
1372        let matter = test_matter();
1373        add_fabric(&matter);
1374
1375        const GROUP_ID: u64 = 0x12AB;
1376
1377        // A regular (writable) ACL entry: Group auth, wildcard targets
1378        let mut entry = AclEntry::new(None, Privilege::OPERATE, AuthMode::Group);
1379        entry.add_subject(GROUP_ID).unwrap();
1380        add_acl(&matter, FAB_1, entry).unwrap();
1381
1382        let accessor = Accessor::new(
1383            FAB_1.get(),
1384            false,
1385            AccessorSubjects::new(GROUP_ID),
1386            Some(AuthMode::Group),
1387            &matter,
1388        );
1389
1390        let ep0 = GenericPath::new(Some(0), Some(1234), None);
1391        let ep1 = GenericPath::new(Some(1), Some(1234), None);
1392
1393        // Without the feature: the wildcard covers the whole node
1394        for path in [ep0.clone(), ep1.clone()] {
1395            let mut req = AccessReq::new(&accessor, path, Access::WRITE, &[]);
1396            req.set_target_perms(Access::WO);
1397            assert!(req.allow());
1398        }
1399
1400        let accessor = Accessor::new(
1401            FAB_1.get(),
1402            true,
1403            AccessorSubjects::new(GROUP_ID),
1404            Some(AuthMode::Group),
1405            &matter,
1406        );
1407
1408        // With the feature: the root endpoint is excluded...
1409        let mut req = AccessReq::new(&accessor, ep0.clone(), Access::WRITE, &[]);
1410        req.set_target_perms(Access::WO);
1411        assert!(!req.allow());
1412
1413        // ...other endpoints are unaffected...
1414        let mut req = AccessReq::new(&accessor, ep1, Access::WRITE, &[]);
1415        req.set_target_perms(Access::WO);
1416        assert!(req.allow());
1417
1418        // ...and an entry explicitly targeting the root endpoint still works.
1419        remove_all_acl(&matter, FAB_1);
1420        let mut entry = AclEntry::new(None, Privilege::OPERATE, AuthMode::Group);
1421        entry.add_subject(GROUP_ID).unwrap();
1422        entry.add_target(Target::new(Some(0), None, None)).unwrap();
1423        add_acl(&matter, FAB_1, entry).unwrap();
1424
1425        let mut req = AccessReq::new(&accessor, ep0, Access::WRITE, &[]);
1426        req.set_target_perms(Access::WO);
1427        assert!(req.allow());
1428    }
1429}