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