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// ---------------------------------------------------------------------------