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