Skip to main content

bacnet_objects/access_control/
rights.rs

1use bacnet_encoding::constructed::encode_access_rule;
2use bacnet_types::constructed::BACnetAccessRule;
3use bacnet_types::enums::{
4    AccessRuleLocationSpecifier, AccessRuleTimeRangeSpecifier, ErrorClass, ErrorCode,
5};
6
7use super::*;
8use crate::durable::DurableWrites;
9
10#[path = "rights_persistence.rs"]
11mod persistence;
12#[path = "rights_saving.rs"]
13mod saving;
14pub use persistence::{AccessRightsPersistence, AccessRightsSnapshot, FileAccessRightsPersistence};
15
16// AccessRightsObject (type 34)
17// ---------------------------------------------------------------------------
18
19/// The most rules either rule array holds, a resource cap. A longer list,
20/// from a setter or a network write, is RESOURCES /
21/// NO_SPACE_TO_WRITE_PROPERTY.
22pub const MAX_ACCESS_RULES: usize = 1024;
23
24/// BACnet Access Rights object (type 34).
25///
26/// Holds the positive and negative access rules that credentials and users
27/// are assigned. Both rule arrays are BACnetARRAYs of `BACnetAccessRule`.
28/// The application provisions them with
29/// [`set_positive_access_rules`](Self::set_positive_access_rules) and
30/// [`set_negative_access_rules`](Self::set_negative_access_rules), and peers
31/// write them whole, one element at a time, or resize them at index 0, with
32/// the setters' checks. Enable (property 133, `LOG_ENABLE` in
33/// `PropertyIdentifier`) switches the whole object: while it is FALSE every
34/// rule in both arrays counts as disabled (Clause 12.34.8). The optional
35/// Accompaniment row (#1393) is served once the application sets it with
36/// [`set_accompaniment`](Self::set_accompaniment). The object stores the
37/// rules, the flag and the accompaniment without evaluating them; the
38/// application checks a credential against the rules with
39/// [`evaluate_access_rights`] (#1331).
40///
41/// # Restarts
42///
43/// An object built with [`new`](Self::new) keeps everything in memory, so a
44/// restart brings back what the application configures. One built with
45/// [`with_persistence`](Self::with_persistence) saves each write of the two
46/// arrays, Enable and Accompaniment in an [`AccessRightsPersistence`] before
47/// serving it, and serves the saved values when built again (#1392). A saved
48/// value wins over the configured one: the setter for a property a write set
49/// ([`property_saved`](Self::property_saved)) checks its argument but leaves
50/// the property alone. To change a saved value, write the property.
51pub struct AccessRightsObject {
52    oid: ObjectIdentifier,
53    name: String,
54    description: String,
55    global_identifier: u64,
56    enable: bool,
57    positive_access_rules: Vec<BACnetAccessRule>,
58    negative_access_rules: Vec<BACnetAccessRule>,
59    /// Accompaniment, served only while it holds a reference.
60    accompaniment: Option<BACnetDeviceObjectReference>,
61    status_flags: StatusFlags,
62    reliability: Reliability,
63    /// Where written rules, Enable and Accompaniment are saved, with
64    /// persistence.
65    storage: Option<saving::Storage>,
66    /// Saved writes taken, so a staged write can tell whether another came
67    /// between.
68    writes: u64,
69    /// The saved properties a write has set, now or before a restart.
70    written: saving::Written,
71}
72
73impl AccessRightsObject {
74    /// Create a new Access Rights object with no access rules, enabled. It
75    /// keeps what peers write in memory only; see
76    /// [`with_persistence`](Self::with_persistence).
77    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
78        let oid = ObjectIdentifier::new(ObjectType::ACCESS_RIGHTS, instance)?;
79        Ok(Self {
80            oid,
81            name: name.into(),
82            description: String::new(),
83            global_identifier: 0,
84            enable: true,
85            positive_access_rules: Vec::new(),
86            negative_access_rules: Vec::new(),
87            accompaniment: None,
88            status_flags: StatusFlags::empty(),
89            reliability: Reliability::NO_FAULT_DETECTED,
90            storage: None,
91            writes: 0,
92            written: saving::Written::default(),
93        })
94    }
95
96    /// Set Enable. FALSE disables every rule in both arrays (Clause
97    /// 12.34.8) and leaves each rule's own enable flag alone, so TRUE again
98    /// brings the rules back as they were. The default, TRUE, is a local
99    /// choice; the standard names none.
100    ///
101    /// This configures the object and is not saved. With persistence, once
102    /// a write has set Enable and it was saved, the saved value wins and
103    /// this does nothing.
104    pub fn set_enable(&mut self, enable: bool) {
105        if !self.keeps_saved(PropertyIdentifier::LOG_ENABLE) {
106            self.enable = enable;
107        }
108    }
109
110    /// The stored Enable flag.
111    pub fn enable(&self) -> bool {
112        self.enable
113    }
114
115    /// Replace Positive_Access_Rules, the rules that grant access.
116    ///
117    /// The whole list is refused, keeping the rules set before, when it holds
118    /// more than [`MAX_ACCESS_RULES`] rules (NO_SPACE_TO_WRITE_PROPERTY), or
119    /// with VALUE_OUT_OF_RANGE when any rule (Clause 12.34.9.1):
120    ///
121    /// - names a device that isn't a Device in either reference (#1285);
122    /// - holds a specifier outside its two named values;
123    /// - is SPECIFIED for a member whose reference is left out;
124    /// - is ALWAYS or ALL for a member whose reference is present and not
125    ///   unspecified;
126    /// - is SPECIFIED for a location that names neither an Access Point nor
127    ///   an Access Zone and isn't unspecified.
128    ///
129    /// A reference is unspecified when its object identifier, and its device
130    /// identifier if it has one, carry instance 4194303. A SPECIFIED member
131    /// may hold one, standing for nothing to match yet. The time range may
132    /// name a property of any object; a Schedule's Present_Value is typical.
133    ///
134    /// This configures the object and is not saved. With persistence, once
135    /// a write has set the array and it was saved, the saved rules win: the
136    /// rules given here are still checked, but not stored.
137    pub fn set_positive_access_rules(
138        &mut self,
139        rules: impl IntoIterator<Item = BACnetAccessRule>,
140    ) -> Result<(), Error> {
141        let rules = checked_rules(rules)?;
142        if !self.keeps_saved(PropertyIdentifier::POSITIVE_ACCESS_RULES) {
143            self.positive_access_rules = rules;
144        }
145        Ok(())
146    }
147
148    /// Replace Negative_Access_Rules, the rules that deny access, with the
149    /// same checks, and the same rule for saved rules, as
150    /// [`set_positive_access_rules`](Self::set_positive_access_rules).
151    pub fn set_negative_access_rules(
152        &mut self,
153        rules: impl IntoIterator<Item = BACnetAccessRule>,
154    ) -> Result<(), Error> {
155        let rules = checked_rules(rules)?;
156        if !self.keeps_saved(PropertyIdentifier::NEGATIVE_ACCESS_RULES) {
157            self.negative_access_rules = rules;
158        }
159        Ok(())
160    }
161
162    /// The stored Positive_Access_Rules.
163    pub fn positive_access_rules(&self) -> &[BACnetAccessRule] {
164        &self.positive_access_rules
165    }
166
167    /// The stored Negative_Access_Rules.
168    pub fn negative_access_rules(&self) -> &[BACnetAccessRule] {
169        &self.negative_access_rules
170    }
171
172    /// Set Accompaniment (Clause 12.34.11), the object a second credential
173    /// presented with the first has to match for these rights to grant
174    /// access: an Access Rights object the second credential holds, the
175    /// Access Credential itself, or the Access User who owns it, here or in
176    /// another device. A reference whose object, and device if it names one,
177    /// carry instance 4194303 asks for no accompaniment. `None` leaves the
178    /// optional property out, as a new object does; once set, the property
179    /// is in Property_List and peers can write it.
180    ///
181    /// Refused with VALUE_OUT_OF_RANGE, keeping the value set before: a
182    /// device member that isn't a Device (#1285), or an object of any other
183    /// type unless the reference asks for no accompaniment.
184    ///
185    /// This configures the object and is not saved. With persistence, once
186    /// a write has set Accompaniment and it was saved, the saved reference
187    /// wins: the one given here is still checked, but not stored, and `None`
188    /// leaves the property in place. To lift a saved requirement, write the
189    /// no-accompaniment reference (instance 4194303), which keeps the row;
190    /// to drop the row itself, remove the storage file, which also drops the
191    /// saved rules and Enable.
192    pub fn set_accompaniment(
193        &mut self,
194        accompaniment: Option<BACnetDeviceObjectReference>,
195    ) -> Result<(), Error> {
196        if let Some(reference) = &accompaniment {
197            check_accompaniment(reference)?;
198        }
199        if !self.keeps_saved(PropertyIdentifier::ACCOMPANIMENT) {
200            self.accompaniment = accompaniment;
201        }
202        Ok(())
203    }
204
205    /// The stored Accompaniment, `None` while the property is left out.
206    pub fn accompaniment(&self) -> Option<&BACnetDeviceObjectReference> {
207        self.accompaniment.as_ref()
208    }
209}
210
211/// Refuse with VALUE_OUT_OF_RANGE an Accompaniment the object can't hold
212/// (see [`AccessRightsObject::set_accompaniment`]). Clause 12.34.11 gives a
213/// meaning to a reference to an Access Rights, Access Credential or Access
214/// User object only, so any other type is refused unless the reference is
215/// unspecified.
216pub(super) fn check_accompaniment(reference: &BACnetDeviceObjectReference) -> Result<(), Error> {
217    crate::device_reference::check_device_member(reference.device_identifier)?;
218    let named = matches!(
219        reference.object_identifier.object_type(),
220        ObjectType::ACCESS_RIGHTS | ObjectType::ACCESS_CREDENTIAL | ObjectType::ACCESS_USER
221    );
222    if named || unspecified(reference.object_identifier, reference.device_identifier) {
223        Ok(())
224    } else {
225        Err(common::value_out_of_range_error())
226    }
227}
228
229/// `rules` collected once they fit [`MAX_ACCESS_RULES`] and every one has
230/// passed [`check_access_rule`].
231pub(super) fn checked_rules(
232    rules: impl IntoIterator<Item = BACnetAccessRule>,
233) -> Result<Vec<BACnetAccessRule>, Error> {
234    let rules: Vec<BACnetAccessRule> = rules.into_iter().collect();
235    check_rule_count(rules.len())?;
236    rules.iter().try_for_each(check_access_rule)?;
237    Ok(rules)
238}
239
240/// Refuse a rule array longer than [`MAX_ACCESS_RULES`] with RESOURCES /
241/// NO_SPACE_TO_WRITE_PROPERTY.
242pub(super) fn check_rule_count(count: usize) -> Result<(), Error> {
243    if count > MAX_ACCESS_RULES {
244        Err(common::protocol_error(
245            ErrorClass::RESOURCES,
246            ErrorCode::NO_SPACE_TO_WRITE_PROPERTY,
247        ))
248    } else {
249        Ok(())
250    }
251}
252
253/// Refuse with VALUE_OUT_OF_RANGE a rule the setters' rules turn away (see
254/// [`AccessRightsObject::set_positive_access_rules`]). The device members go
255/// through the shared `check_device_member` first.
256pub(super) fn check_access_rule(rule: &BACnetAccessRule) -> Result<(), Error> {
257    let time_range_device = rule.time_range.as_ref().and_then(|r| r.device_identifier);
258    crate::device_reference::check_device_member(time_range_device)?;
259    let location_device = rule.location.as_ref().and_then(|r| r.device_identifier);
260    crate::device_reference::check_device_member(location_device)?;
261
262    let time_range_ok = match rule.time_range_specifier {
263        AccessRuleTimeRangeSpecifier::SPECIFIED => rule.time_range.is_some(),
264        AccessRuleTimeRangeSpecifier::ALWAYS => rule
265            .time_range
266            .as_ref()
267            .is_none_or(|r| unspecified(r.object_identifier, r.device_identifier)),
268        _ => false,
269    };
270    let location_ok = match rule.location_specifier {
271        AccessRuleLocationSpecifier::SPECIFIED => rule.location.as_ref().is_some_and(|r| {
272            matches!(
273                r.object_identifier.object_type(),
274                ObjectType::ACCESS_POINT | ObjectType::ACCESS_ZONE
275            ) || unspecified(r.object_identifier, r.device_identifier)
276        }),
277        AccessRuleLocationSpecifier::ALL => rule
278            .location
279            .as_ref()
280            .is_none_or(|r| unspecified(r.object_identifier, r.device_identifier)),
281        _ => false,
282    };
283    if time_range_ok && location_ok {
284        Ok(())
285    } else {
286        Err(common::value_out_of_range_error())
287    }
288}
289
290/// Whether a rule's reference, or Accompaniment, is unspecified: its object,
291/// and its device if it names one, both carry the reserved instance number
292/// 4194303. The rights evaluation reads an unused Assigned_Access_Rights
293/// element by the same rule.
294pub(super) fn unspecified(object: ObjectIdentifier, device: Option<ObjectIdentifier>) -> bool {
295    let unused = |oid: ObjectIdentifier| oid.instance_number() == ObjectIdentifier::MAX_INSTANCE;
296    unused(object) && device.is_none_or(unused)
297}
298
299/// A rule array as `common::read_array` serves it: each rule in its Clause
300/// 21 form.
301fn rule_values(rules: &[BACnetAccessRule]) -> Vec<PropertyValue> {
302    rules
303        .iter()
304        .map(|rule| {
305            let mut buf = BytesMut::new();
306            encode_access_rule(&mut buf, rule);
307            PropertyValue::ApplicationData(buf.to_vec())
308        })
309        .collect()
310}
311
312impl BACnetObject for AccessRightsObject {
313    fn object_identifier(&self) -> ObjectIdentifier {
314        self.oid
315    }
316
317    fn object_name(&self) -> &str {
318        &self.name
319    }
320
321    fn read_property(
322        &self,
323        property: PropertyIdentifier,
324        array_index: Option<u32>,
325    ) -> Result<PropertyValue, Error> {
326        // Table 12-39 has no Out_Of_Service (#1064), and Clause 12.34 holds the
327        // OUT_OF_SERVICE flag FALSE.
328        if let Some(result) =
329            read_common_properties!(self, property, array_index, no_out_of_service)
330        {
331            return result;
332        }
333        match property {
334            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
335                ObjectType::ACCESS_RIGHTS.to_raw(),
336            )),
337            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
338                Ok(PropertyValue::Unsigned(self.global_identifier))
339            }
340            // Table 12-39's Enable row is property 133, which the enum names
341            // after the log objects' Log_Enable.
342            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.enable)),
343            p if p == PropertyIdentifier::POSITIVE_ACCESS_RULES => {
344                common::read_array(rule_values(&self.positive_access_rules), array_index)
345            }
346            p if p == PropertyIdentifier::NEGATIVE_ACCESS_RULES => {
347                common::read_array(rule_values(&self.negative_access_rules), array_index)
348            }
349            p if p == PropertyIdentifier::ACCOMPANIMENT => match &self.accompaniment {
350                Some(reference) => Ok(crate::device_reference::reference_value(reference)),
351                None => Err(common::unknown_property_error()),
352            },
353            _ => Err(common::unknown_property_error()),
354        }
355    }
356
357    fn write_property(
358        &mut self,
359        property: PropertyIdentifier,
360        array_index: Option<u32>,
361        value: PropertyValue,
362        _priority: Option<u8>,
363    ) -> Result<(), Error> {
364        if let Some(result) = common::write_description(&mut self.description, property, &value) {
365            return result;
366        }
367        match property {
368            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
369                if let PropertyValue::Unsigned(v) = value {
370                    self.global_identifier = v;
371                    Ok(())
372                } else {
373                    Err(common::invalid_data_type_error())
374                }
375            }
376            // Table 12-39 makes Enable and both rule arrays R rows and
377            // Accompaniment an O row, so taking their writes is this
378            // implementation's choice (Clause 12.1.2 leaves it open): a head
379            // end provisions access rights over the network. With
380            // persistence, each is saved before it is served.
381            p if p == PropertyIdentifier::LOG_ENABLE
382                || p == PropertyIdentifier::POSITIVE_ACCESS_RULES
383                || p == PropertyIdentifier::NEGATIVE_ACCESS_RULES =>
384            {
385                self.write_saved(property, array_index, value)
386            }
387            // A write can't add the optional row; only the application can.
388            p if p == PropertyIdentifier::ACCOMPANIMENT => {
389                if self.accompaniment.is_none() {
390                    return Err(common::unknown_property_error());
391                }
392                self.write_saved(property, array_index, value)
393            }
394            _ => Err(crate::common::unhandled_write_error(
395                self.property_metadata().as_ref(),
396                property,
397                array_index,
398            )),
399        }
400    }
401
402    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
403        super::metadata_identity::for_access_rights_object(self)
404    }
405
406    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
407        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
408    }
409
410    fn advance_monotonic_time_internal(&mut self, now: std::time::Duration) -> bool {
411        self.expire_staged_write(now);
412        false
413    }
414
415    fn durable_writes_internal(&mut self) -> Option<&mut dyn DurableWrites> {
416        Some(self)
417    }
418}
419
420#[cfg(test)]
421#[path = "rights_tests.rs"]
422mod tests;
423
424#[cfg(test)]
425#[path = "rights_test_storage.rs"]
426mod test_storage;
427
428#[cfg(test)]
429#[path = "rights_persistence_tests.rs"]
430mod persistence_tests;
431
432#[cfg(test)]
433#[path = "rights_staging_tests.rs"]
434mod staging_tests;
435
436#[cfg(test)]
437#[path = "rights_fold_tests.rs"]
438mod fold_tests;
439
440#[cfg(test)]
441#[path = "rights_accompaniment_tests.rs"]
442mod accompaniment_tests;
443
444// ---------------------------------------------------------------------------