Skip to main content

bacnet_objects/access_control/
credential.rs

1use std::sync::Arc;
2
3use bacnet_encoding::constructed::{
4    encode_assigned_access_rights, encode_credential_authentication_factor,
5};
6use bacnet_types::constructed::{BACnetAssignedAccessRights, BACnetCredentialAuthenticationFactor};
7use bacnet_types::enums::{
8    AccessCredentialDisable, AccessCredentialDisableReason, AuthorizationExemption,
9};
10use bytes::BytesMut;
11
12use super::credential_rules::{self as rules, WindowLimit};
13use super::*;
14use crate::clock::ClockReader;
15
16// AccessCredentialObject (type 32)
17// ---------------------------------------------------------------------------
18
19/// BACnet Access Credential object (type 32).
20///
21/// Represents a credential (card, fob, biometric, etc.) used for access
22/// control. Table 12-40 has no Present_Value row, so the object serves none
23/// (#979).
24///
25/// Credential_Status is derived, not stored (Clause 12.35.8): INACTIVE while
26/// Reason_For_Disable lists anything, ACTIVE otherwise, so it is read-only.
27/// Reason_For_Disable is the sorted union of three sources (#1073):
28///
29/// - the reasons the application raises with
30///   [`add_disable_reason`](Self::add_disable_reason);
31/// - the reason the current Credential_Disable value adds (Clause 12.35.13),
32///   so changing that property drops the reason its previous value added;
33/// - DISABLED_NOT_YET_ACTIVE and DISABLED_EXPIRED, judged against the
34///   database's wall clock each time the list is read, the way the Calendar
35///   follows the date (Clauses 12.35.11 and 12.35.12). Without a clock frame
36///   naming a real moment the window adds nothing.
37///
38/// The network may write Global_Identifier, Credential_Disable,
39/// Activation_Time and Expiration_Time. Authentication_Factors and
40/// Assigned_Access_Rights are BACnetARRAYs the application provisions.
41///
42/// The optional Authorization_Exemptions row (Clause 12.35.25; #1331) is
43/// served once the application sets it with
44/// [`set_authorization_exemptions`](Self::set_authorization_exemptions), and
45/// is read-only over the network. With ACCESS_RIGHTS listed,
46/// [`evaluate_access_rights`] reports the credential exempt from the Access
47/// Rights check.
48pub struct AccessCredentialObject {
49    oid: ObjectIdentifier,
50    name: String,
51    description: String,
52    global_identifier: u32,
53    /// Reasons raised by the application, sorted by value without repeats.
54    local_reasons: Vec<AccessCredentialDisableReason>,
55    credential_disable: AccessCredentialDisable,
56    activation_time: WindowLimit,
57    expiration_time: WindowLimit,
58    authentication_factors: Vec<BACnetCredentialAuthenticationFactor>,
59    assigned_access_rights: Vec<BACnetAssignedAccessRights>,
60    /// Authorization_Exemptions, served only while it holds a list.
61    authorization_exemptions: Option<Vec<AuthorizationExemption>>,
62    status_flags: StatusFlags,
63    reliability: Reliability,
64    /// The database's wall clock, the source of the current moment.
65    clock: Option<Arc<dyn ClockReader>>,
66}
67
68impl AccessCredentialObject {
69    /// Create a new Access Credential object: no disable reasons (so
70    /// ACTIVE), Credential_Disable NONE, an open validity window, no global
71    /// identifier and empty arrays.
72    pub fn new(instance: u32, name: impl Into<String>) -> Result<Self, Error> {
73        let oid = ObjectIdentifier::new(ObjectType::ACCESS_CREDENTIAL, instance)?;
74        Ok(Self {
75            oid,
76            name: name.into(),
77            description: String::new(),
78            global_identifier: 0,
79            local_reasons: Vec::new(),
80            credential_disable: AccessCredentialDisable::NONE,
81            activation_time: WindowLimit::OPEN,
82            expiration_time: WindowLimit::OPEN,
83            authentication_factors: Vec::new(),
84            assigned_access_rights: Vec::new(),
85            authorization_exemptions: None,
86            status_flags: StatusFlags::empty(),
87            reliability: Reliability::NO_FAULT_DETECTED,
88            clock: None,
89        })
90    }
91
92    /// Set Global_Identifier; 0 means none is assigned (Clause 12.35.5).
93    pub fn set_global_identifier(&mut self, value: u32) {
94        self.global_identifier = value;
95    }
96
97    /// Set Credential_Disable. A value outside the four named ones and the
98    /// vendor range 64 to 65535 is refused with VALUE_OUT_OF_RANGE.
99    pub fn set_credential_disable(&mut self, value: AccessCredentialDisable) -> Result<(), Error> {
100        rules::check_credential_disable(value)?;
101        self.credential_disable = value;
102        Ok(())
103    }
104
105    /// Set Activation_Time. Every octet X'FF' leaves the start open; a
106    /// partly specified value is refused with VALUE_OUT_OF_RANGE.
107    pub fn set_activation_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
108        self.activation_time = WindowLimit::new(date, time)?;
109        Ok(())
110    }
111
112    /// Set Expiration_Time, with the same rules as
113    /// [`set_activation_time`](Self::set_activation_time).
114    pub fn set_expiration_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
115        self.expiration_time = WindowLimit::new(date, time)?;
116        Ok(())
117    }
118
119    /// The stored Activation_Time.
120    pub fn activation_time(&self) -> (Date, Time) {
121        self.activation_time.parts()
122    }
123
124    /// The stored Expiration_Time.
125    pub fn expiration_time(&self) -> (Date, Time) {
126        self.expiration_time.parts()
127    }
128
129    /// Raise a disable reason on behalf of the application, such as
130    /// DISABLED_LOCKOUT after too many retries. Raising one already raised
131    /// does nothing.
132    ///
133    /// Refused with VALUE_OUT_OF_RANGE: a value outside the named reasons and
134    /// the vendor range, and DISABLED_NOT_YET_ACTIVE and DISABLED_EXPIRED,
135    /// which follow Activation_Time and Expiration_Time instead.
136    pub fn add_disable_reason(
137        &mut self,
138        reason: AccessCredentialDisableReason,
139    ) -> Result<(), Error> {
140        rules::check_local_reason(reason)?;
141        if let Err(index) = self
142            .local_reasons
143            .binary_search_by_key(&reason.to_raw(), |r| r.to_raw())
144        {
145            self.local_reasons.insert(index, reason);
146        }
147        Ok(())
148    }
149
150    /// Withdraw a reason the application raised; `false` if it wasn't
151    /// raised. Reasons from Credential_Disable or the validity window stay.
152    pub fn remove_disable_reason(&mut self, reason: AccessCredentialDisableReason) -> bool {
153        let before = self.local_reasons.len();
154        self.local_reasons.retain(|r| *r != reason);
155        self.local_reasons.len() != before
156    }
157
158    /// Reason_For_Disable now: every source's reasons, sorted by value
159    /// without repeats.
160    pub fn reason_for_disable(&self) -> Vec<AccessCredentialDisableReason> {
161        let frame = self.clock.as_ref().and_then(|clock| clock.read_clock());
162        let mut reasons = self.local_reasons.clone();
163        reasons.extend(rules::credential_disable_reason(self.credential_disable));
164        reasons.extend(rules::window_reasons(
165            frame,
166            self.activation_time,
167            self.expiration_time,
168        ));
169        reasons.sort_by_key(|r| r.to_raw());
170        reasons.dedup();
171        reasons
172    }
173
174    /// Credential_Status now: INACTIVE while any disable reason holds.
175    pub fn credential_status(&self) -> BinaryPV {
176        if self.reason_for_disable().is_empty() {
177            BinaryPV::ACTIVE
178        } else {
179            BinaryPV::INACTIVE
180        }
181    }
182
183    /// Replace Authentication_Factors. Each element's disable value must be
184    /// named or in the vendor range and its format type one of the 25
185    /// named; otherwise VALUE_OUT_OF_RANGE and nothing changes.
186    pub fn set_authentication_factors(
187        &mut self,
188        factors: Vec<BACnetCredentialAuthenticationFactor>,
189    ) -> Result<(), Error> {
190        factors
191            .iter()
192            .try_for_each(rules::check_authentication_factor)?;
193        self.authentication_factors = factors;
194        Ok(())
195    }
196
197    /// The stored Authentication_Factors.
198    pub fn authentication_factors(&self) -> &[BACnetCredentialAuthenticationFactor] {
199        &self.authentication_factors
200    }
201
202    /// Replace Assigned_Access_Rights. Each element must reference an Access
203    /// Rights object (or carry instance 4194303, the unused marker), and a
204    /// device identifier, if present, must be a Device; otherwise
205    /// VALUE_OUT_OF_RANGE and nothing changes.
206    pub fn set_assigned_access_rights(
207        &mut self,
208        rights: Vec<BACnetAssignedAccessRights>,
209    ) -> Result<(), Error> {
210        rights
211            .iter()
212            .try_for_each(rules::check_assigned_access_rights)?;
213        self.assigned_access_rights = rights;
214        Ok(())
215    }
216
217    /// The stored Assigned_Access_Rights.
218    pub fn assigned_access_rights(&self) -> &[BACnetAssignedAccessRights] {
219        &self.assigned_access_rights
220    }
221
222    /// Set Authorization_Exemptions (Clause 12.35.25), the authorization
223    /// checks this credential is exempt from. `Some` serves the optional row,
224    /// an empty list included, and adds it to Property_List; `None` leaves it
225    /// out, as a new credential does. The list is read-only over the network.
226    ///
227    /// A value outside the seven named checks and the vendor range 64 to 255
228    /// is refused with VALUE_OUT_OF_RANGE, keeping the list set before.
229    pub fn set_authorization_exemptions(
230        &mut self,
231        exemptions: Option<Vec<AuthorizationExemption>>,
232    ) -> Result<(), Error> {
233        if let Some(list) = &exemptions {
234            list.iter()
235                .copied()
236                .try_for_each(rules::check_authorization_exemption)?;
237        }
238        self.authorization_exemptions = exemptions;
239        Ok(())
240    }
241
242    /// The stored Authorization_Exemptions, `None` while the row is left out.
243    pub fn authorization_exemptions(&self) -> Option<&[AuthorizationExemption]> {
244        self.authorization_exemptions.as_deref()
245    }
246}
247
248/// Encode each element of a constructed array as its own framed value.
249fn framed<T>(items: &[T], encode: fn(&mut BytesMut, &T)) -> Vec<PropertyValue> {
250    items
251        .iter()
252        .map(|item| {
253            let mut buf = BytesMut::new();
254            encode(&mut buf, item);
255            PropertyValue::ApplicationData(buf.to_vec())
256        })
257        .collect()
258}
259
260impl BACnetObject for AccessCredentialObject {
261    fn object_identifier(&self) -> ObjectIdentifier {
262        self.oid
263    }
264
265    fn object_name(&self) -> &str {
266        &self.name
267    }
268
269    fn read_property(
270        &self,
271        property: PropertyIdentifier,
272        array_index: Option<u32>,
273    ) -> Result<PropertyValue, Error> {
274        // Table 12-40 has no Out_Of_Service (#1064), and Clause 12.35 holds the
275        // OUT_OF_SERVICE flag FALSE.
276        if let Some(result) =
277            read_common_properties!(self, property, array_index, no_out_of_service)
278        {
279            return result;
280        }
281        match property {
282            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
283                ObjectType::ACCESS_CREDENTIAL.to_raw(),
284            )),
285            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
286                Ok(PropertyValue::Unsigned(self.global_identifier.into()))
287            }
288            p if p == PropertyIdentifier::CREDENTIAL_STATUS => {
289                Ok(PropertyValue::Enumerated(self.credential_status().to_raw()))
290            }
291            p if p == PropertyIdentifier::REASON_FOR_DISABLE => Ok(PropertyValue::List(
292                self.reason_for_disable()
293                    .into_iter()
294                    .map(|reason| PropertyValue::Enumerated(reason.to_raw()))
295                    .collect(),
296            )),
297            p if p == PropertyIdentifier::AUTHENTICATION_FACTORS => common::read_array(
298                framed(
299                    &self.authentication_factors,
300                    encode_credential_authentication_factor,
301                ),
302                array_index,
303            ),
304            p if p == PropertyIdentifier::ACTIVATION_TIME => Ok(self.activation_time.to_property()),
305            p if p == PropertyIdentifier::EXPIRATION_TIME => Ok(self.expiration_time.to_property()),
306            p if p == PropertyIdentifier::CREDENTIAL_DISABLE => {
307                Ok(PropertyValue::Enumerated(self.credential_disable.to_raw()))
308            }
309            p if p == PropertyIdentifier::ASSIGNED_ACCESS_RIGHTS => common::read_array(
310                framed(&self.assigned_access_rights, encode_assigned_access_rights),
311                array_index,
312            ),
313            p if p == PropertyIdentifier::AUTHORIZATION_EXEMPTIONS => {
314                match &self.authorization_exemptions {
315                    Some(list) => Ok(PropertyValue::List(
316                        list.iter()
317                            .map(|exemption| PropertyValue::Enumerated(exemption.to_raw()))
318                            .collect(),
319                    )),
320                    None => Err(common::unknown_property_error()),
321                }
322            }
323            _ => Err(common::unknown_property_error()),
324        }
325    }
326
327    fn write_property(
328        &mut self,
329        property: PropertyIdentifier,
330        array_index: Option<u32>,
331        value: PropertyValue,
332        _priority: Option<u8>,
333    ) -> Result<(), Error> {
334        if let Some(result) = common::write_description(&mut self.description, property, &value) {
335            return result;
336        }
337        match property {
338            p if p == PropertyIdentifier::GLOBAL_IDENTIFIER => {
339                let PropertyValue::Unsigned(raw) = value else {
340                    return Err(common::invalid_data_type_error());
341                };
342                self.global_identifier = common::u64_to_u32(raw)?;
343                Ok(())
344            }
345            p if p == PropertyIdentifier::CREDENTIAL_DISABLE => {
346                let PropertyValue::Enumerated(raw) = value else {
347                    return Err(common::invalid_data_type_error());
348                };
349                self.set_credential_disable(AccessCredentialDisable::from_raw(raw))
350            }
351            p if p == PropertyIdentifier::ACTIVATION_TIME => {
352                self.activation_time = WindowLimit::from_property(value)?;
353                Ok(())
354            }
355            p if p == PropertyIdentifier::EXPIRATION_TIME => {
356                self.expiration_time = WindowLimit::from_property(value)?;
357                Ok(())
358            }
359            // Credential_Status follows Reason_For_Disable (Clause 12.35.8),
360            // so it, like the list itself, is refused as read-only here.
361            _ => Err(crate::common::unhandled_write_error(
362                self.property_metadata().as_ref(),
363                property,
364                array_index,
365            )),
366        }
367    }
368
369    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
370        super::metadata_identity::for_access_credential_object(self)
371    }
372
373    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
374        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
375    }
376
377    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
378        self.clock = clock;
379    }
380}
381
382// ---------------------------------------------------------------------------