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