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