Skip to main content

bacnet_objects/access_control/
rights_evaluation.rs

1//! Checking a credential against the Access Rights it is assigned (Clause
2//! 12.34.9; #1331).
3//!
4//! The access-control objects store and serve the rules: an Access Rights
5//! object holds Positive_Access_Rules and Negative_Access_Rules, an Access
6//! Credential names its rights in Assigned_Access_Rights, and an Access Zone
7//! names its Entry_Points. [`evaluate_access_rights`] reads them and tells the
8//! application whether the rules let a credential through the Access Point
9//! where it was presented, and if not, which Access_Event value the failure
10//! carries.
11//!
12//! The evaluator is pure. It takes a shared borrow of the database, reads
13//! properties the way a peer would see them (`read_property`), and does no
14//! I/O. It takes no lock and changes nothing: it doesn't set Access_Event or
15//! any other property, and acting on the decision is up to the application.
16//! A server application calls it under the database read guard, as in
17//! `evaluate_access_rights(&*server.database().read().await, credential,
18//! point)`, and drops the guard before acting on the decision.
19//!
20//! [`evaluate_access_rights`] documents each step. Where Clause 12.34.9
21//! leaves the reading to the device, this evaluator takes these:
22//!
23//! - A time range reads an Enumerated by the enumeration of the property it
24//!   names, as `ResolvedEnum::from_property` knows it, since the value alone
25//!   doesn't say. A BACnetBinaryPV, or any property whose type isn't known
26//!   to be another enumeration (the Present_Value of any object among them),
27//!   reads ACTIVE as TRUE and any other number as FALSE.
28//! - A time range whose type can never read TRUE under that reading is
29//!   FALSE at every moment: REAL, Double, strings and the other types the
30//!   clause doesn't name, an enumeration known to be another one
31//!   (Event_State, Reliability and the rest), and a whole array read
32//!   without an index.
33//! - Only a value of a type that can read TRUE, read here and found FALSE,
34//!   puts a rule outside its time range. A time range that is FALSE at every
35//!   moment (the types above, no reference or an unspecified one, a missing
36//!   object or property, a failed read, NULL, index 0, or another device)
37//!   gives a positive rule covering the point no access there at any time:
38//!   DENIED_NO_ACCESS_RIGHTS, not DENIED_OUT_OF_TIME_RANGE.
39//! - A negative rule whose location is ALL denies with
40//!   DENIED_POINT_NO_ACCESS_RIGHTS: it bars this point as much as a rule
41//!   naming the point does, and the clause names a value only for the point
42//!   and zone cases.
43//! - A reference names this device only when it has no device member or
44//!   names the database's own Device (`LocalDevice::is_local`, as the rest
45//!   of the stack reads references). The wildcard Device instance 4194303
46//!   names no device in particular, so a reference carrying it is never
47//!   read, and neither is one naming another device. Such a reference never
48//!   grants access. A negative rule whose location or time range names one
49//!   doesn't hold, so it bars no one: Clause 12.34.9.1 has a reference that
50//!   is unspecified or can't be retrieved evaluate to FALSE.
51//! - Clause 12.35.18 has the device ignore an assignment naming a missing
52//!   object or an object that isn't Access Rights. Leaving out an assignment
53//!   naming another device or the wildcard Device as well is this
54//!   evaluator's own policy. All of them are listed as unresolved, so the
55//!   application can still deny.
56
57use crate::collections::HashMap;
58use crate::prelude::*;
59
60use bacnet_encoding::constructed::{decode_access_rule, decode_assigned_access_rights};
61use bacnet_types::constructed::{
62    BACnetAccessRule, BACnetAssignedAccessRights, BACnetDeviceObjectPropertyReference,
63    BACnetDeviceObjectReference,
64};
65use bacnet_types::enums::{
66    AccessEvent, AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier, AuthorizationExemption,
67    BinaryPV, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, ResolvedEnum,
68};
69use bacnet_types::error::Error;
70use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
71
72use super::rights::unspecified;
73use crate::common;
74use crate::database::{LocalDevice, ObjectDatabase};
75use crate::device_reference::decode_references;
76use crate::traits::BACnetObject;
77
78/// What [`evaluate_access_rights`] found for one credential at one Access
79/// Point.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[non_exhaustive]
82pub struct AccessRightsEvaluation {
83    /// The outcome of the Access Rights check.
84    pub decision: AccessRightsDecision,
85    /// The enabled Assigned_Access_Rights elements that gave no rules
86    /// because their Access Rights object couldn't be read here, in array
87    /// order. The decision leaves them out. Clause 12.35.18 asks for that
88    /// with a missing object or one that isn't Access Rights; for an object
89    /// in another device, or behind the wildcard Device, it is this
90    /// evaluator's policy, since it reads nothing remotely. An application
91    /// that would rather deny while part of a credential's rights is out of
92    /// sight checks that this is empty. Always empty for
93    /// [`AccessRightsDecision::Exempt`], which reads no rights.
94    pub unresolved: Vec<UnresolvedAccessRights>,
95}
96
97/// The outcome of the Access Rights check (Clause 12.34.9.2).
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99#[non_exhaustive]
100pub enum AccessRightsDecision {
101    /// The credential's Authorization_Exemptions lists ACCESS_RIGHTS, so the
102    /// check didn't run and counts as passed (Clause 12.35.25).
103    Exempt,
104    /// No negative rule held, and this positive rule did: the first in
105    /// gathering and array order.
106    #[non_exhaustive]
107    Granted {
108        /// The positive rule that granted access.
109        rule: AccessRulePosition,
110    },
111    /// The check failed.
112    #[non_exhaustive]
113    Denied {
114        /// The Access_Event value Clause 12.34.9.2 gives this failure:
115        /// DENIED_POINT_NO_ACCESS_RIGHTS or DENIED_ZONE_NO_ACCESS_RIGHTS
116        /// for a negative rule, DENIED_OUT_OF_TIME_RANGE, or
117        /// DENIED_NO_ACCESS_RIGHTS. The evaluator doesn't write it.
118        access_event: AccessEvent,
119        /// The rule behind the denial: the negative rule that held, or, for
120        /// DENIED_OUT_OF_TIME_RANGE, the first positive rule that covered
121        /// the point while a value read for its time range was FALSE. `None`
122        /// for DENIED_NO_ACCESS_RIGHTS.
123        rule: Option<AccessRulePosition>,
124    },
125}
126
127impl AccessRightsDecision {
128    /// Whether the check let the credential through: granted or exempt.
129    pub fn allows(&self) -> bool {
130        matches!(self, Self::Exempt | Self::Granted { .. })
131    }
132
133    /// The Access_Event value of a denial, `None` otherwise.
134    pub fn access_event(&self) -> Option<AccessEvent> {
135        match self {
136            Self::Denied { access_event, .. } => Some(*access_event),
137            _ => None,
138        }
139    }
140
141    /// The rule the decision names: the granting rule, or a denial's
142    /// rule when it has one.
143    pub fn rule(&self) -> Option<AccessRulePosition> {
144        match self {
145            Self::Granted { rule } => Some(*rule),
146            Self::Denied { rule, .. } => *rule,
147            Self::Exempt => None,
148        }
149    }
150}
151
152/// Where an access rule sits: its Access Rights object, its array and its
153/// place in that array.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155#[non_exhaustive]
156pub struct AccessRulePosition {
157    /// The Access Rights object holding the rule.
158    pub access_rights: ObjectIdentifier,
159    /// Which of the object's two rule arrays holds it.
160    pub kind: AccessRuleKind,
161    /// The rule's one-based array index, the index a ReadProperty of
162    /// [`kind.property()`](AccessRuleKind::property) names it by.
163    pub index: u32,
164}
165
166/// Which rule array of an Access Rights object a rule comes from.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168#[non_exhaustive]
169pub enum AccessRuleKind {
170    /// Positive_Access_Rules, the rules that grant access.
171    Positive,
172    /// Negative_Access_Rules, the rules that deny it.
173    Negative,
174}
175
176impl AccessRuleKind {
177    /// The property holding the rules of this kind.
178    pub fn property(self) -> PropertyIdentifier {
179        match self {
180            Self::Positive => PropertyIdentifier::POSITIVE_ACCESS_RULES,
181            Self::Negative => PropertyIdentifier::NEGATIVE_ACCESS_RULES,
182        }
183    }
184}
185
186/// An enabled Assigned_Access_Rights element that gave no rules.
187#[derive(Debug, Clone, PartialEq, Eq)]
188#[non_exhaustive]
189pub struct UnresolvedAccessRights {
190    /// The element's one-based index in Assigned_Access_Rights.
191    pub index: u32,
192    /// The reference the element holds.
193    pub reference: BACnetDeviceObjectReference,
194    /// Why it gave no rules.
195    pub reason: UnresolvedReason,
196}
197
198/// Why an Assigned_Access_Rights element gave no rules.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200#[non_exhaustive]
201pub enum UnresolvedReason {
202    /// It names an object in another device, which this evaluator doesn't
203    /// read.
204    Remote,
205    /// Its device member is the wildcard Device instance 4194303, which
206    /// names no device in particular, so there is no device to ask.
207    WildcardDevice,
208    /// It names an object type other than Access Rights.
209    NotAccessRights,
210    /// The database holds no object with that identifier.
211    Missing,
212    /// The Access Rights object's Enable or rule arrays didn't read as their
213    /// datatypes. The built-in object always serves them; this covers an
214    /// application's own object.
215    Unreadable,
216}
217
218/// Check the Access Rights rules of `credential`, presented at `point`, as
219/// Clause 12.34.9.2 describes.
220///
221/// # Steps
222///
223/// 1. **Exemption.** When the credential's Authorization_Exemptions lists
224///    ACCESS_RIGHTS, the check doesn't run and counts as passed (Clause
225///    12.35.25): the decision is [`AccessRightsDecision::Exempt`], and no
226///    rule is read. A credential that doesn't serve the optional row is not
227///    exempt.
228/// 2. **Gathering.** Assigned_Access_Rights is walked in array order.
229///    - An element whose own enable flag is FALSE is skipped, and so is the
230///      unused marker, instance 4194303 in the object and in the device when
231///      it names one (Clause 12.35.18).
232///    - An element naming a missing object or an object that isn't Access
233///      Rights gives no rules: Clause 12.35.18 has the device ignore it.
234///      Neither does one naming another device or the wildcard Device;
235///      that is this evaluator's policy, since it reads nothing remotely.
236///      Each such element is listed
237///      in [`AccessRightsEvaluation::unresolved`], so that an application can
238///      deny when part of a credential's rights is out of its sight.
239///    - An Access Rights object whose Enable is FALSE gives no rules either
240///      (Clause 12.34.8), and isn't listed: it was found.
241/// 3. **Negative rules first.** Each enabled negative rule of each gathered
242///    object is tried, objects in gathering order and rules in array order,
243///    before any positive rule is (Clause 12.34.9.2). The first one that
244///    holds denies access, with the Access_Event its location gives (see
245///    below).
246/// 4. **Positive rules.** Otherwise the first enabled positive rule that
247///    holds, in the same order, grants access.
248/// 5. **No rule held.** When some enabled positive rule covered this point
249///    and a value read here for its time range was FALSE, the credential
250///    could pass here at another time, so the denial is
251///    DENIED_OUT_OF_TIME_RANGE. Otherwise it is DENIED_NO_ACCESS_RIGHTS,
252///    including when every rule covering the point has a time range that is
253///    FALSE at every moment (see below).
254///
255/// A rule holds when its enable flag is TRUE, its location covers the point
256/// and its time range is TRUE (Clause 12.34.9.1).
257///
258/// # Locations
259///
260/// - ALL covers every point, whatever the location reference holds.
261/// - SPECIFIED with an Access Point covers that point only.
262/// - SPECIFIED with an Access Zone covers the point when the zone's
263///   Entry_Points names it in this device. Each zone's list is read once
264///   per evaluation.
265/// - Anything else covers nothing: no reference, an unspecified one, one to
266///   another device or the wildcard Device, a missing zone, another object
267///   type, or a specifier outside the two named values.
268///
269/// A negative rule that holds denies with DENIED_POINT_NO_ACCESS_RIGHTS when
270/// its location names the point and DENIED_ZONE_NO_ACCESS_RIGHTS when it
271/// names a zone. Clause 12.34.9.2 names no value for a negative rule covering
272/// every location; such a rule bars passage through this point as much as
273/// one naming it, so this evaluator reports DENIED_POINT_NO_ACCESS_RIGHTS.
274///
275/// # Time ranges
276///
277/// ALWAYS is TRUE. SPECIFIED reads the referenced property from this
278/// database, honouring the reference's array index, and turns the value into
279/// TRUE or FALSE:
280///
281/// - BOOLEAN: its value.
282/// - Unsigned: TRUE unless zero.
283/// - INTEGER: TRUE above zero.
284/// - Enumerated: read by the property's enumeration
285///   ([`ResolvedEnum::from_property`]), since the value alone doesn't name
286///   one. For a BACnetBinaryPV, or any property whose type isn't known to
287///   be another enumeration (the Present_Value of any object among them),
288///   ACTIVE (1) is TRUE and any other number FALSE. Clause 12.34.9.1 leaves
289///   types other than the four it names to the device, so reading these as
290///   BACnetBinaryPV is a local choice.
291///
292/// A FALSE from those values is read here, so a positive rule with one is
293/// outside its time range now and may hold at another time. A time range
294/// that can't read TRUE at any moment is FALSE at every moment instead:
295///
296/// - a type that never reads TRUE under these rules: REAL, Double, strings
297///   and the other types the clause doesn't name, an enumeration known to
298///   be another one (Event_State, Reliability and the rest), and a whole
299///   array read without an index (the same local choice);
300/// - a specifier outside the two named values, SPECIFIED with no reference,
301///   an unspecified reference, or one naming another device or the wildcard
302///   Device;
303/// - a missing object or a property it doesn't serve, an index on a
304///   property that isn't an array, index 0 (an array's size is no
305///   time-range value) or one past the end, any other failed read, and NULL.
306///
307/// Such a rule never makes a denial DENIED_OUT_OF_TIME_RANGE.
308///
309/// # Which device a reference names
310///
311/// A reference names this device when it has no device member or names the
312/// Device the database speaks for ([`ObjectDatabase::local_device`]), the
313/// reading the rest of the stack takes. Anything else, the wildcard Device
314/// instance 4194303 included, names no device this evaluator can read: such
315/// a time range is FALSE at every moment, such a location covers nothing,
316/// and such an Assigned_Access_Rights element is listed as unresolved. So
317/// such a reference never grants access. A negative rule naming one doesn't
318/// hold either, so it bars no one: Clause 12.34.9.1 has a reference that is
319/// unspecified or can't be retrieved evaluate to FALSE, whichever kind of
320/// rule holds it. An application that would rather deny can act on the
321/// unresolved list. A database holding no Device has no Device of its own,
322/// so then only a reference with no device member is local.
323///
324/// # Not covered
325///
326/// The Access Rights check is one of several authorization checks. This
327/// evaluator doesn't judge Accompaniment (Clause 12.34.11), the credential's
328/// status, validity window or threat authority, the point's authorization
329/// mode, occupancy or passback, and it doesn't record the outcome on the
330/// point.
331///
332/// # Errors
333///
334/// OBJECT / UNKNOWN_OBJECT, before any rule is read, when
335/// `credential` names no Access Credential in `db` or `point` no Access
336/// Point. An error reading or decoding the credential's
337/// Assigned_Access_Rights is passed on; the built-in credential always
338/// serves it whole.
339///
340/// # Example
341///
342/// ```
343/// use bacnet_objects::access_control::{
344///     evaluate_access_rights, AccessCredentialObject, AccessPointObject, AccessRightsObject,
345/// };
346/// use bacnet_objects::database::ObjectDatabase;
347/// use bacnet_types::constructed::{BACnetAccessRule, BACnetAssignedAccessRights};
348/// use bacnet_types::enums::{AccessEvent, ObjectType};
349/// use bacnet_types::primitives::ObjectIdentifier;
350///
351/// let door = ObjectIdentifier::new(ObjectType::ACCESS_POINT, 1)?;
352/// let staff = ObjectIdentifier::new(ObjectType::ACCESS_RIGHTS, 1)?;
353/// let card = ObjectIdentifier::new(ObjectType::ACCESS_CREDENTIAL, 1)?;
354///
355/// // Staff may pass Point 1 at any time.
356/// let mut rights = AccessRightsObject::new(1, "STAFF")?;
357/// rights.set_positive_access_rules([BACnetAccessRule::new(None, Some(door.into()), true)])?;
358/// let mut credential = AccessCredentialObject::new(1, "CARD-1")?;
359/// credential.set_assigned_access_rights(vec![BACnetAssignedAccessRights {
360///     assigned_access_rights: staff.into(),
361///     enable: true,
362/// }])?;
363///
364/// let mut db = ObjectDatabase::new();
365/// db.add(Box::new(AccessPointObject::new(1, "DOOR-1")?))?;
366/// db.add(Box::new(AccessPointObject::new(2, "DOOR-2")?))?;
367/// db.add(Box::new(rights))?;
368/// db.add(Box::new(credential))?;
369///
370/// assert!(evaluate_access_rights(&db, card, door)?.decision.allows());
371/// let elsewhere = ObjectIdentifier::new(ObjectType::ACCESS_POINT, 2)?;
372/// let evaluation = evaluate_access_rights(&db, card, elsewhere)?;
373/// assert_eq!(
374///     evaluation.decision.access_event(),
375///     Some(AccessEvent::DENIED_NO_ACCESS_RIGHTS)
376/// );
377/// # Ok::<(), bacnet_types::error::Error>(())
378/// ```
379pub fn evaluate_access_rights(
380    db: &ObjectDatabase,
381    credential: ObjectIdentifier,
382    point: ObjectIdentifier,
383) -> Result<AccessRightsEvaluation, Error> {
384    let holder = object_of_type(db, credential, ObjectType::ACCESS_CREDENTIAL)?;
385    object_of_type(db, point, ObjectType::ACCESS_POINT)?;
386    if exempt(holder) {
387        return Ok(AccessRightsEvaluation {
388            decision: AccessRightsDecision::Exempt,
389            unresolved: Vec::new(),
390        });
391    }
392    let mut scope = Scope {
393        db,
394        local: db.local_device(),
395        point,
396        zones: HashMap::new(),
397    };
398    let (gathered, unresolved) = scope.gather(&assigned_access_rights(holder)?);
399    Ok(AccessRightsEvaluation {
400        decision: scope.decide(&gathered),
401        unresolved,
402    })
403}
404
405/// The object `oid` names in `db`, when it is of `object_type`.
406fn object_of_type(
407    db: &ObjectDatabase,
408    oid: ObjectIdentifier,
409    object_type: ObjectType,
410) -> Result<&dyn BACnetObject, Error> {
411    db.get(&oid)
412        .filter(|_| oid.object_type() == object_type)
413        .ok_or_else(|| common::protocol_error(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT))
414}
415
416/// Whether the credential serves an Authorization_Exemptions list holding
417/// ACCESS_RIGHTS.
418fn exempt(credential: &dyn BACnetObject) -> bool {
419    let access_rights = PropertyValue::Enumerated(AuthorizationExemption::ACCESS_RIGHTS.to_raw());
420    matches!(
421        credential.read_property(PropertyIdentifier::AUTHORIZATION_EXEMPTIONS, None),
422        Ok(PropertyValue::List(exemptions)) if exemptions.contains(&access_rights)
423    )
424}
425
426/// The credential's Assigned_Access_Rights, decoded from the value it serves.
427pub(super) fn assigned_access_rights(
428    credential: &dyn BACnetObject,
429) -> Result<Vec<BACnetAssignedAccessRights>, Error> {
430    let value = credential.read_property(PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS, None)?;
431    // Each element opens with the reference's frame, context tag [0].
432    common::decode_elements(
433        &value,
434        |tag| tag.is_opening_tag(0),
435        decode_assigned_access_rights,
436    )
437}
438
439/// The enabled rules of one gathered Access Rights object.
440struct Gathered {
441    oid: ObjectIdentifier,
442    negative: Vec<BACnetAccessRule>,
443    positive: Vec<BACnetAccessRule>,
444}
445
446impl Gathered {
447    fn rules(&self, kind: AccessRuleKind) -> &[BACnetAccessRule] {
448        match kind {
449            AccessRuleKind::Positive => &self.positive,
450            AccessRuleKind::Negative => &self.negative,
451        }
452    }
453}
454
455/// Every enabled rule of `kind`, objects in gathering order and rules in
456/// array order, with its position.
457fn enabled_rules(
458    gathered: &[Gathered],
459    kind: AccessRuleKind,
460) -> impl Iterator<Item = (AccessRulePosition, &BACnetAccessRule)> {
461    gathered.iter().flat_map(move |rights| {
462        (1..)
463            .zip(rights.rules(kind))
464            .filter(|(_, rule)| rule.enable)
465            .map(move |(index, rule)| {
466                let position = AccessRulePosition {
467                    access_rights: rights.oid,
468                    kind,
469                    index,
470                };
471                (position, rule)
472            })
473    })
474}
475
476/// What a rule's location matched: the kind of place decides the
477/// Access_Event a negative rule denies with.
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479enum Place {
480    /// Location-Specifier ALL.
481    Everywhere,
482    /// The Access Point itself.
483    Point,
484    /// An Access Zone the point enters.
485    Zone,
486}
487
488impl Place {
489    /// The Access_Event of a negative rule that held here (Clause
490    /// 12.34.9.2). A rule for every location bars this point as well, so it
491    /// is reported as the point's denial: a local reading, since the clause
492    /// names only the point and zone cases.
493    fn denial(self) -> AccessEvent {
494        match self {
495            Place::Zone => AccessEvent::DENIED_ZONE_NO_ACCESS_RIGHTS,
496            Place::Point | Place::Everywhere => AccessEvent::DENIED_POINT_NO_ACCESS_RIGHTS,
497        }
498    }
499}
500
501/// A rule's time range, as far as this database can tell (Clause
502/// 12.34.9.1).
503#[derive(Debug, Clone, Copy, PartialEq, Eq)]
504enum TimeRange {
505    /// ALWAYS, or a value read here as TRUE.
506    True,
507    /// A value read here as FALSE: the rule may hold at another time.
508    False,
509    /// FALSE at every moment: no usable reference, a failed read, NULL,
510    /// index 0, or a type that never reads TRUE (see
511    /// [`evaluate_access_rights`]).
512    Never,
513}
514
515/// One evaluation: the database, its own Device, the point where the
516/// credential was presented, and what each zone's Entry_Points said about
517/// that point, read once per evaluation however many rules name the zone.
518struct Scope<'a> {
519    db: &'a ObjectDatabase,
520    local: LocalDevice,
521    point: ObjectIdentifier,
522    zones: HashMap<ObjectIdentifier, bool>,
523}
524
525impl Scope<'_> {
526    /// Whether a reference with this device member names this device: no
527    /// member, or the database's own Device. The wildcard Device names none
528    /// in particular, so it isn't local.
529    fn is_local(&self, device: Option<ObjectIdentifier>) -> bool {
530        self.local.is_local(device)
531    }
532
533    /// The enabled Access Rights objects the enabled elements of
534    /// `assigned` name, in order, and the elements that gave no rules.
535    fn gather(
536        &self,
537        assigned: &[BACnetAssignedAccessRights],
538    ) -> (Vec<Gathered>, Vec<UnresolvedAccessRights>) {
539        let mut gathered = Vec::new();
540        let mut unresolved = Vec::new();
541        for (index, element) in (1..).zip(assigned) {
542            let reference = &element.assigned_access_rights;
543            if !element.enable
544                || unspecified(reference.object_identifier, reference.device_identifier)
545            {
546                continue;
547            }
548            match self.resolve(reference) {
549                Ok(Some(rights)) => gathered.push(rights),
550                // An Access Rights object with Enable FALSE.
551                Ok(None) => {}
552                Err(reason) => unresolved.push(UnresolvedAccessRights {
553                    index,
554                    reference: reference.clone(),
555                    reason,
556                }),
557            }
558        }
559        (gathered, unresolved)
560    }
561
562    /// The rules of the Access Rights object `reference` names: `None`
563    /// while its Enable is FALSE, or why it gives none.
564    fn resolve(
565        &self,
566        reference: &BACnetDeviceObjectReference,
567    ) -> Result<Option<Gathered>, UnresolvedReason> {
568        let oid = reference.object_identifier;
569        if reference.device_identifier.is_some_and(is_wildcard_device) {
570            return Err(UnresolvedReason::WildcardDevice);
571        }
572        if !self.is_local(reference.device_identifier) {
573            return Err(UnresolvedReason::Remote);
574        }
575        if oid.object_type() != ObjectType::ACCESS_RIGHTS {
576            return Err(UnresolvedReason::NotAccessRights);
577        }
578        let object = self.db.get(&oid).ok_or(UnresolvedReason::Missing)?;
579        // Table 12-39's Enable is property 133, LOG_ENABLE in the enum.
580        match object.read_property(PropertyIdentifier::LOG_ENABLE, None) {
581            Ok(PropertyValue::Boolean(true)) => {}
582            Ok(PropertyValue::Boolean(false)) => return Ok(None),
583            _ => return Err(UnresolvedReason::Unreadable),
584        }
585        let rules = |kind: AccessRuleKind| {
586            object
587                .read_property(kind.property(), None)
588                .and_then(|value| {
589                    // A rule opens with its time-range specifier, context tag [0].
590                    common::decode_elements(&value, |tag| tag.is_context(0), decode_access_rule)
591                })
592                .map_err(|_| UnresolvedReason::Unreadable)
593        };
594        Ok(Some(Gathered {
595            oid,
596            negative: rules(AccessRuleKind::Negative)?,
597            positive: rules(AccessRuleKind::Positive)?,
598        }))
599    }
600
601    /// The decision over the gathered rules (Clause 12.34.9.2).
602    fn decide(&mut self, gathered: &[Gathered]) -> AccessRightsDecision {
603        for (position, rule) in enabled_rules(gathered, AccessRuleKind::Negative) {
604            if let Some(place) = self.location(rule)
605                && self.time_range(rule) == TimeRange::True
606            {
607                return AccessRightsDecision::Denied {
608                    access_event: place.denial(),
609                    rule: Some(position),
610                };
611            }
612        }
613        let mut out_of_time = None;
614        for (position, rule) in enabled_rules(gathered, AccessRuleKind::Positive) {
615            if self.location(rule).is_some() {
616                match self.time_range(rule) {
617                    TimeRange::True => return AccessRightsDecision::Granted { rule: position },
618                    TimeRange::False => {
619                        out_of_time.get_or_insert(position);
620                    }
621                    TimeRange::Never => {}
622                }
623            }
624        }
625        match out_of_time {
626            Some(position) => AccessRightsDecision::Denied {
627                access_event: AccessEvent::DENIED_OUT_OF_TIME_RANGE,
628                rule: Some(position),
629            },
630            None => AccessRightsDecision::Denied {
631                access_event: AccessEvent::DENIED_NO_ACCESS_RIGHTS,
632                rule: None,
633            },
634        }
635    }
636
637    /// What the rule's location matched at this point, if anything.
638    fn location(&mut self, rule: &BACnetAccessRule) -> Option<Place> {
639        match rule.location_specifier {
640            AccessRuleLocationSpecifier::ALL => Some(Place::Everywhere),
641            AccessRuleLocationSpecifier::SPECIFIED => {
642                let reference = rule.location.as_ref()?;
643                let oid = reference.object_identifier;
644                if unspecified(oid, reference.device_identifier)
645                    || !self.is_local(reference.device_identifier)
646                {
647                    return None;
648                }
649                match oid.object_type() {
650                    ObjectType::ACCESS_POINT => (oid == self.point).then_some(Place::Point),
651                    ObjectType::ACCESS_ZONE => self.enters(oid).then_some(Place::Zone),
652                    _ => None,
653                }
654            }
655            _ => None,
656        }
657    }
658
659    /// Whether the zone `zone` names the point among its Entry_Points,
660    /// read once per evaluation.
661    fn enters(&mut self, zone: ObjectIdentifier) -> bool {
662        if let Some(&enters) = self.zones.get(&zone) {
663            return enters;
664        }
665        let enters = self.read_entry_points(zone);
666        self.zones.insert(zone, enters);
667        enters
668    }
669
670    /// Whether the zone's Entry_Points, as it serves them now, names the
671    /// point in this device.
672    fn read_entry_points(&self, zone: ObjectIdentifier) -> bool {
673        let Some(zone) = self.db.get(&zone) else {
674            return false;
675        };
676        let Ok(value) = zone.read_property(PropertyIdentifier::ENTRY_POINTS, None) else {
677            return false;
678        };
679        decode_references::<BACnetDeviceObjectReference>(&value).is_ok_and(|entries| {
680            entries.iter().any(|entry| {
681                entry.object_identifier == self.point && self.is_local(entry.device_identifier)
682            })
683        })
684    }
685
686    /// The rule's time range (Clause 12.34.9.1).
687    fn time_range(&self, rule: &BACnetAccessRule) -> TimeRange {
688        match rule.time_range_specifier {
689            AccessRuleTimeRangeSpecifier::ALWAYS => TimeRange::True,
690            AccessRuleTimeRangeSpecifier::SPECIFIED => rule
691                .time_range
692                .as_ref()
693                .map_or(TimeRange::Never, |reference| {
694                    self.read_time_range(reference)
695                }),
696            _ => TimeRange::Never,
697        }
698    }
699
700    /// The time range the property `reference` names, read here.
701    fn read_time_range(&self, reference: &BACnetDeviceObjectPropertyReference) -> TimeRange {
702        let oid = reference.object_identifier;
703        if unspecified(oid, reference.device_identifier)
704            || !self.is_local(reference.device_identifier)
705        {
706            return TimeRange::Never;
707        }
708        let Some(object) = self.db.get(&oid) else {
709            return TimeRange::Never;
710        };
711        let property = PropertyIdentifier::from_raw(reference.property_identifier);
712        let index = reference.property_array_index;
713        // ReadProperty's gate: only an array takes an index. Index 0 reads
714        // the array's size, which is no time-range value.
715        if index == Some(0) || (index.is_some() && !object.is_array_property(property)) {
716            return TimeRange::Never;
717        }
718        match object.read_property(property, index) {
719            Ok(value) => time_range_value(property, &value),
720            Err(_) => TimeRange::Never,
721        }
722    }
723}
724
725/// A time-range value read from `property` (Clause 12.34.9.1, and the local
726/// choices in the module documentation).
727fn time_range_value(property: PropertyIdentifier, value: &PropertyValue) -> TimeRange {
728    let holds = match *value {
729        PropertyValue::Boolean(value) => value,
730        PropertyValue::Unsigned(value) => value != 0,
731        PropertyValue::Signed(value) => value > 0,
732        PropertyValue::Enumerated(raw) => match ResolvedEnum::from_property(property, raw) {
733            ResolvedEnum::BinaryPV(state) => state == BinaryPV::ACTIVE,
734            // A property whose type isn't known to be another enumeration,
735            // the Present_Value of any object among them, reads as one.
736            ResolvedEnum::Unknown(raw) => raw == BinaryPV::ACTIVE.to_raw(),
737            // Another enumeration never reads TRUE.
738            _ => return TimeRange::Never,
739        },
740        // NULL, and the types that never read TRUE: REAL, Double, strings,
741        // a whole array and the rest.
742        _ => return TimeRange::Never,
743    };
744    if holds {
745        TimeRange::True
746    } else {
747        TimeRange::False
748    }
749}
750
751/// Whether a device member is the wildcard Device instance 4194303.
752fn is_wildcard_device(device: ObjectIdentifier) -> bool {
753    device.object_type() == ObjectType::DEVICE
754        && device.instance_number() == ObjectIdentifier::WILDCARD_INSTANCE
755}
756
757#[cfg(test)]
758#[path = "rights_evaluation_tests.rs"]
759mod tests;
760
761#[cfg(test)]
762#[path = "rights_evaluation_term_tests.rs"]
763mod term_tests;
764
765#[cfg(test)]
766#[path = "rights_evaluation_order_tests.rs"]
767mod order_tests;
768
769#[cfg(test)]
770#[path = "rights_evaluation_scenario_tests.rs"]
771mod scenario_tests;