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;