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